By NielsenIQ
Generics?
Usecases
Look around
PHP
Ecosystem
Symfony example
Generics allow you to customize the types inside classes, interfaces and methods...
Write typed code without being specifically bound to a type.
Add a layer of abstraction over types.
No cast (or documentation)
Optimize and simplify (?)
Easy code maintenance
Increases code security
Not so easy notation
Runtime perf ?
Important concept
Could be native (Java, Go, Rust, ...)
highly awaited
class Collection
{
public function push($item): void {}
public function pop() {}
}
$dogs = new Collection();
$dogs->push(new Dog()); // seems ok
$dogs->push(new Cat()); // seems not ok at all
$dog = $dogs->pop(); // is it really a dog?
class TypedCollection
{
public __construct(private readonly string $type) {}
public function push($item): void
{
if ($item::class !== $this->type) {
return;
}
// add in the list
}
public function pop() {}
}
$dogs = new TypedCollection(Dog::class);
$dogs->push(new Dog()); // seems ok
$dogs->push(new Cat()); // will be handled properly
$dog = $dogs->pop(); // I will get a dog for sure
class DogCollection
{
public function push(Dog $item): void {}
public function pop(): Dog {}
}
$dogs = new DogCollection();
$dogs->push(new Dog()); // seems obvious
$dogs->push(new Cat()); // error
$dog = $dogs->pop();
But what if I need a collection of cats?
function make(string $className)
{
return new $className();
}
$result = make(Foo::class); // is a Foo
$result = make(Exception::class); // is an Exception
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[]
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);
Without generic
function identity<T>(arg: T): T {
return arg;
}
let outputString = identity<string>("myString");
let outputNumber = identity<number>(42);
With generic
fn foo<T>(p: T) {
...
}
(because Nikita said so)
// i can take every param and return what i want
function identity($value)
{
return $value;
}
$r1 = identity(2023);
$r2 = identity('Dutch PHP Conference');
$r3 = identity(true);
$r4 = identity(['Dutch PHP Conference']);
echo $r4[0];
Why don't we use that?
function identity(mixed $value): mixed
{
return $value;
}
$r1 = identity(2023);
$r2 = identity('Dutch PHP Conference');
$r3 = identity(true);
$r4 = identity(['Dutch PHP Conference']);
echo $r4[0];
Annotations!
syntax as comments
almost non-existent syntax highlighting
difficult content validation
...
/*
/**
* @template T
* @param T $value
* @return T
*/
function identity(mixed $value): mixed
{
return $value;
}
$r1 = identity(2023);
$r2 = identity('Dutch PHP Conference');
$r3 = identity(true);
$r4 = identity(['Dutch PHP Conference']);
echo $r4[0];
/**
* @template T
* @param T $value
* @return T
*/
function identity(mixed $value): mixed
{
return [$value];
}
$r1 = identity(2023);
$r2 = identity('Dutch PHP Conference');
$r3 = identity(true);
$r4 = identity(['Dutch PHP Conference']);
echo $r1[0]; // 2023
echo $r2[0]; // Dutch PHP Conference
echo $r3[0]; // 1
echo $r4[0]; // Warning: Array to string conversion
/**
* @template T
*/
class Collection
{
/**
* @param T $item
*/
public function push(mixed $item): void {...}
/**
* @return T
*/
public function pop(): mixed {...}
}
/**
* @template K
* @template V
*/
class 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('Dutch PHP Conference');
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();
PaymentMean
AsyncPaymentMean
SyncPaymentMean
BankTransfer
CreditCard
/** @template T */
interface PaymentMeanCollection
{
/** @param T $item */
public function add($item): void;
/** @return T */
public function get(): mixed;
}
/** @implements PaymentMeanCollection<AsyncPaymentMean> */
class AsyncPaymentMeanCollection implements PaymentMeanCollection
{
public function add($item): void {}
public function get(): mixed
{
// BankTransfer implements AsyncPaymentMean so OK
return new BankTransfer();
}
}
/** @template T */
interface PaymentMeanCollection
{
/** @param T $item */
public function add($item): void;
/** @return T */
public function get(): mixed;
}
/** @implements PaymentMeanCollection<AsyncPaymentMean> */
class AsyncPaymentMeanCollection implements PaymentMeanCollection
{
public function add($item): void {}
public function get(): mixed
{
// CreditCard is a sync payment mean so not OK at all
return new CreditCard();
}
}
/**
* @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<Foo> */
class FooCollection extends Collection
{
public function get(): mixed
{
return 'Dutch PHP Conference';
}
}
PaymentMean
CreditCard
covariance
contravariance
Since PHP 7.4
I approve!
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(AsyncPaymentMean $paymentMean): void;
}
class BankTransfertPaymentProcessor extends AsyncPaymentProcessor
{
public function process(AsyncPaymentMean $paymentMean): void {...}
// or
public function process(PaymentMean $paymentMean): void {...}
}
Contravariance
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;
}
/** @extends 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) { ... }
No runtime overhead
Functional coverage
Source of truth
PHP + annotations + attributes
No 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 {}
Useful for DX
Not native in PHP
Very good tooling support
https://slides.com/kpn13
@kpn13
https://blog.karimpinchon.com
Joind.in