Les génériques ?
Des cas d'utilisations
Regardons ailleurs
En PHP
L'écosystème
L'exemple de Symfony
Les génériques permettent de rendre paramétrables les types des classes, interfaces et méthodes...
Écrire du code typé mais sans être spécifiquement lié à un type.
Ajouter une couche d'abstraction sur les types.
Pas de cast (ou documentation)
Optimise et simplifie (?)
Facilite la maintenance du code
Augmente la sécurité du code
Notation pas (toujours) simple
Performances au runtime (?)
Concept important
Souvent natif (Java, Go, Rust, ...)
Très attendu
class Collection
{
public function push(??? $item): void {}
public function pop(): ??? {}
}
class TypedCollection
{
private string $type;
public function push(??? $item): void {}
public function pop(): ??? {}
}
class FooCollection
{
public function push(Foo $item): void {}
public function pop(): Foo {}
}
function add(??? $a, ??? $b): ???
{
return $a + $b;
}
$result = add(1, 42); // is a int
$result = add(2.21, 3.14); // is a float
function transformToArray(??? $a, ??? $b): array
{
return [$a, $b];
}
$result = transformToArray(1, 42); // is a int[]
$result = transformToArray(2.21, 3.14); // is a float[]
function make(string $className): ???
{
return new $className();
}
$result = make(Foo::class); // is a Foo
$result = make(Exception::class); // is an Exception
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
SumIntsOrFloats[string, int64](ints)
SumIntsOrFloats[string, float64](floats)
type Number interface {
int64 | float64
}
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
SumNumbers(ints)
SumNumbers(floats)
public class Box<T> {
private T t;
public void set(T t)
{
this.t = t;
}
public T get()
{
return t;
}
}
Box<Integer> integerBox = new Box<Integer>();
class Util {
public static <T> T findGreatest(T p1, T p2) {
...
}
}
Util.findGreatest(1, 42); //42
Util.findGreatest("symfony", "laravel"); // symfony
function identity(arg: any): any {
return arg;
}
let output1 = identity("myString");
let output2 = identity(42);
function identity<T>(arg: T): T {
return arg;
}
let outputString = identity<string>("myString");
let outputNumber = identity<number>(42);
fn foo<T>(p: T) {
...
}
(parce que Nikita l'a dit)
// i can take every param and return what i want
function identity($value)
{
return $value;
}
$r1 = identity(2023);
$r2 = identity('Symfony Live Paris');
$r3 = identity(true);
$r4 = identity(['Symfony Live Paris']);
echo $r4[0];
Si on utilisait ça ?
function identity(mixed $value): mixed
{
return $value;
}
$r1 = identity(2023);
$r2 = identity('Symfony Live Paris');
$r3 = identity(true);
$r4 = identity(['Symfony Live Paris']);
echo $r4[0];
Les annotations !
la syntaxe sous forme de commentaire
coloration syntaxique quasi inexistante
validation du contenu difficile
...
/*
/**
* @template T
* @param T $value
* @return T
*/
function identity(mixed $value): mixed
{
return $value;
}
$r1 = identity(2023);
$r2 = identity('Symfony Live Paris');
$r3 = identity(true);
$r4 = identity(['Symfony Live Paris']);
echo $r4[0];
/**
* @template T
* @param T $value
* @return T
*/
function identity(mixed $value): mixed
{
return [$value];
}
$r1 = identity(2023);
$r2 = identity('Symfony Live Paris');
$r3 = identity(true);
$r4 = identity(['Symfony Live Paris']);
echo $r1[0]; // 2023
echo $r2[0]; // Symfony Live Paris
echo $r3[0]; // 1
echo $r4[0]; // Warning: Array to string conversion
/**
* @template T
*/
interface Collection
{
/**
* @param T $item
*/
public function push(mixed $item): void;
/**
* @return T
*/
public function pop(): mixed;
}
/**
* @template K
* @template V
*/
interface Collection
{
/**
* @param V $item
*/
public function push(mixed $item): void;
/**
* @return V
*/
public function pop(): mixed;
/**
* @param K $index
* @return V
*/
public function get(mixed $index): mixed;
}
/**
* @template T of \Exception
* @param T $value
* @return T
*/
function identity($value): mixed
{
return $value;
}
$r1 = identity(new LogicException());
assert($r1 instanceof LogicException);
/**
* @template T of \Exception
* @param T $value
* @return T
*/
function identity($value): mixed
{
return $value;
}
$r1 = identity('Hello Symfony Live Paris!');
assert(is_string($r1));
/**
* @template T of object
* @param class-string<T> $className
* @return T
*/
function make(string $className): object
{
return new $className;
}
$e = make(Exception::class);
echo $e->getMessage();
function make(string $className): object
{
return new $className;
}
$e = make(Exception::class);
echo $e->getMessage();
/**
* @template T
*/
interface Collection
{
/** @param T $item */
public function add($item): void;
/** @return T */
public function get(): mixed;
}
/** @implements Collection<Bar> */
class BarCollection implements Collection
{
public function add($item): void {}
public function get(): mixed
{
return new Bar();
}
}
/**
* @template T
*/
interface Collection
{
/** @param T $item */
public function add($item): void;
/** @return T */
public function get(): mixed;
}
/** @implements Collection<Bar> */
class BarCollection implements Collection
{
public function add($item): void {}
public function get(): mixed
{
return 'Heyyy';
}
}
/**
* @template T
*/
class Collection
{
/** T $item */
protected mixed $item;
/** @param T $item */
public function add($item): void {$this->item = $item;}
/** @return T */
public function get(): mixed { return $this->item;}
}
/** @extends Collection<Bar> */
class BarCollection extends Collection
{
public function add($item): void {$this->item = $item;}
public function get(): mixed
{
return new Bar();
}
}
/**
* @template T
*/
class Collection
{
/** T $item */
protected mixed $item;
/** @return T */
public function get(): mixed { return $this->item;}
}
/** @extends Collection<Bar> */
class BarCollection extends Collection
{
public function get(): mixed
{
return 'Heyyy';
}
}
PaymentMean
CreditCard
covariance
contravariance
Depuis PHP 7.4
J'approuve !
class PaymentResult {}
class CreditCardPaymentResult extends PaymentResult {}
interface PaymentMean
{
public function processPayment(int $amount): PaymentResult;
}
class CreditCard implements PaymentMean
{
public function processPayment(int $amount): CreditCardPaymentResult
{
}
}
Covariance
class Result {}
class PaymentResult extends Result {}
interface PaymentMean
{
public function processPayment(int $amount): PaymentResult;
}
class CreditCard implements PaymentMean
{
public function processPayment(int $amount): Result
{
}
}
Covariance
abstract class AsyncPaymentProcessor
{
public function process(PaymentMean $paymentMean): void;
}
class BankTransfertPaymentProcessor extends AsyncPaymentProcessor
{
public function process(PaymentMean $paymentMean): void
{
}
}
Contravariance
interface PaymentMean {}
class BankTransfer implements PaymentMean {}
abstract class AsyncPaymentProcessor
{
public function process(PaymentMean $paymentMean): void;
}
class BankTransferPaymentProcessor extends AsyncPaymentProcessor
{
public function process(BankTransfer $paymentMean): void
{
}
}
Contravariance
/**
* @template T of PaymentMean
*/
abstract class AsyncPaymentProcessor
{
/**
* @param T $paymentMean
*/
public function process(PaymentMean $paymentMean): void;
}
/**
* @implements AsyncPaymentProcessor<BankTransfert>
*/
class BankTransfertPaymentProcessor extends AsyncPaymentProcessor
{
public function process(PaymentMean $paymentMean): void
{
}
}
Contravariance
/**
* @phpstan-template T of \Exception
*
* @phpstan-param T $param
* @phpstan-return T
*/
function foo($param) { ... }
/**
* @psalm-template T of \Exception
*
* @psalm-param T $param
* @psalm-return T
*/
function foo($param) { ... }
Pas de surcoût au runtime
Couverture fonctionnelle
Source de vérité
PHP + annotations + attributs
Pas d'obligation
/**
* Return the given value, optionally passed through the given callback.
*
* @template TValue
* @template TReturn
*
* @param TValue $value
* @param (callable(TValue): (TReturn))|null $callback
* @return ($callback is null ? TValue : TReturn)
*/
function with($value, callable $callback = null)
{
return is_null($callback) ? $value : $callback($value);
}
Form
Finder
Messenger
Workflow
Serializer
Security
VarExporter
Http
DI
Console
...
namespace Symfony\Component\Security\Http\Authenticator\Passport;
class Passport
{
/**
* @template TBadge of BadgeInterface
*
* @param class-string<TBadge> $badgeFqcn
*
* @return TBadge|null
*/
public function getBadge(string $badgeFqcn): ?BadgeInterface
{
return $this->badges[$badgeFqcn] ?? null;
}
...
}
namespace Symfony\Component\Serializer;
interface SerializerInterface
{
/**
* Deserializes data into the given type.
*
* @template TObject of object
* @template TType of string|class-string<TObject>
*
* @param TType $type
* @param array<string, mixed> $context
*
* @psalm-return (TType is class-string<TObject> ? TObject : mixed)
*
* @phpstan-return ($type is class-string<TObject> ? TObject : mixed)
*/
public function deserialize(mixed $data, string $type, ...): mixed;
}
<?php
namespace Symfony\Component\Messenger;
final class Envelope
{
/**
* @template TStamp of StampInterface
*
* @param class-string<TStamp> $stamp
*
* @return TStamp|null
*/
public function last(string $stamp): ?StampInterface
{
return isset($this->stamps[$stamp]) ? end($this->stamps[$stamp]) : null;
}
}
<?php
namespace Symfony\Component\DependencyInjection\ParameterBag;
interface ContainerBagInterface extends ContainerInterface
{
/**
* Replaces parameter placeholders (%name%) by their values.
*
* @template TValue of array<array|scalar>|scalar
*
* @param TValue $value
*
* @return mixed
* @psalm-return (TValue is scalar ? array|scalar : array<array|scalar>)
*/
public function resolveValue(mixed $value);
}
<?php
namespace Symfony\Component\Console\Helper;
/**
* @implements \IteratorAggregate<string, HelperInterface>
*/
class HelperSet implements \IteratorAggregate
{
}
<?php
namespace Symfony\Component\DependencyInjection;
/**
* A ServiceProviderInterface exposes the identifiers
* and the types of services provided by a container.
*
* @template T of mixed
*/
interface ServiceProviderInterface extends ContainerInterface
{
/**
* @return T
*/
public function get(string $id): mixed;
}
/**
* @template-covariant T of mixed
*
* @implements ServiceProviderInterface<T>
*/
class ServiceLocator implements ServiceProviderInterface, \Countable {}
Concept important (collection, ...)
Pas natif en PHP
Très bien supporté par des outils
https://slides.com/kpn13
@kpn13
https://blog.karimpinchon.com