Generics
(in )
About me
Karim Pinchon
- Backend developer
- @kpn13
- https://blog.karimpinchon.com
- https://slides.com/kpn13
By NielsenIQ
What are we going to talk about?
Generics?
Usecases
Look around
PHP
Ecosystem
Symfony example
Summary
Generics? What is it?
Definition
Generics allow you to customize the types inside classes, interfaces and methods...
Write typed code without being specifically bound to a type.
Definition
Add a layer of abstraction over types.
Definition
Benefits
No cast (or documentation)
Optimize and simplify (?)
Easy code maintenance
Increases code security
Drawbacks
Not so easy notation
Runtime perf ?
Today
Important concept
Could be native (Java, Go, Rust, ...)
highly awaited
Usecases
Collections
Collections
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?
Collections
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
Collections
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?
Factories
function make(string $className)
{
return new $className();
}
$result = make(Foo::class); // is a Foo
$result = make(Exception::class); // is an Exception
Utils
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[]
Let's look around
Which languages ?
Any conventions ?
- T : 1st type
- S : 2nd type
- U : 3rd type
- V : 4th type
- K : map key
- V : map value
- ...
- TKey
- TValue
- TFooBar
- TMyInterface
Golang example
Golang example
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)
Go example
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)
Java example
Java example
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>();
Java example
class Util {
public static <T> T findGreatest(T p1, T p2) {
...
}
}
Util.findGreatest(1, 42); //42
Util.findGreatest("symfony", "laravel"); // symfony
Typescript example
Typescript example
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);
Typescript example
With generic
Rust example
Rust example
fn foo<T>(p: T) {
...
}
How does it work in PHP?
Why PHP will probably never support generics?
(because Nikita said so)
- https://github.com/PHPGenerics/php-generics-rfc/issues/45
- https://www.redditmedia.com/r/PHP/comments/...
No more typehint?
// 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];
How about mixed?
- Since PHP 8.0
- object|resource|array|string|float|int|bool|null
Why don't we use that?
How about mixed?
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];
What solutions do we have, then?
Annotations!
- @template
- @extends
- @implements
- class-string
- ...
The limits of annotations
syntax as comments
almost non-existent syntax highlighting
difficult content validation
...
/*
Ecosystem
Tools to help us
Annotations
@template
/**
* @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
/**
* @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
/**
* @template T
*/
class Collection
{
/**
* @param T $item
*/
public function push(mixed $item): void {...}
/**
* @return T
*/
public function pop(): mixed {...}
}
@template
/**
* @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 Foo
/**
* @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 Foo
/**
* @template T of \Exception
* @param T $value
* @return T
*/
function identity($value): mixed
{
return $value;
}
$r1 = identity('Dutch PHP Conference');
assert(is_string($r1));
class-string
/**
* @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();
class-string
function make(string $className): object
{
return new $className;
}
$e = make(Exception::class);
echo $e->getMessage();
PaymentMean
AsyncPaymentMean
SyncPaymentMean
BankTransfer
CreditCard
@implements
/** @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();
}
}
@implements
/** @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();
}
}
@extends
/**
* @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();
}
}
@extends
/**
* @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';
}
}
Covariance and contravariance
Covariance and contravariance
PaymentMean
CreditCard
covariance
contravariance
Covariance and contravariance
Since PHP 7.4
- covariance on return type
- contravariance on arguments type
I approve!
Covariance and contravariance
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
Covariance and contravariance
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
Covariance and contravariance
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
Covariance and contravariance
abstract class AsyncPaymentProcessor
{
public function process(PaymentMean $paymentMean): void;
}
class BankTransferPaymentProcessor extends AsyncPaymentProcessor
{
public function process(BankTransfer $paymentMean): void
{
}
}
Contravariance
Covariance and 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 / PSalm
specificities
PSalm / PHPStan
/**
* @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) { ... }
PhpStorm 2021.2
PHPStorm support
PhpStorm 2021.2
Is it perfect?
Is it perfect?
No runtime overhead
Functional coverage
Source of truth
PHP + annotations + attributes
No obligation
Is it perfect?
/**
* 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);
}
Symfony's example
Lots of pull requests
Which components?
Form
Finder
Messenger
Workflow
Serializer
Security
VarExporter
Http
DI
Console
...
Some examples
Some examples
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;
}
...
}
Some examples
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;
}
Some examples
<?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;
}
}
Some examples
<?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);
}
Some examples
<?php
namespace Symfony\Component\Console\Helper;
/**
* @implements \IteratorAggregate<string, HelperInterface>
*/
class HelperSet implements \IteratorAggregate
{
}
Some examples
<?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 {}
Conclusion
Conclusion
Useful for DX
Not native in PHP
Very good tooling support
https://slides.com/kpn13
Thank you
@kpn13
https://blog.karimpinchon.com
Joind.in
- https://stitcher.io/blog/php-generics-and-why-we-need-them
- https://psalm.dev/docs/annotating_code/templated_annotations/
- https://phpstan.org/blog/generics-in-php-using-phpdocs
- https://phpstan.org/blog/generics-by-examples
- https://phpstan.org/blog/whats-up-with-template-covariant
- https://go.dev/doc/tutorial/generics
- https://youtrack.jetbrains.com/issue/WI-60894
- https://youtrack.jetbrains.com/issue/WI-61497
- https://github.com/mrsuh/php-generics
- https://arnaud.le-blanc.net/post/phpstan-generics.html
- https://github.com/PHPGenerics/php-generics-rfc/issues/45
- https://www.redditmedia.com/r/PHP/comments/j65968/ama_with_the_phpstorm_team_from_jetbrains_on/g83skiz/
Ressources
Generics (in PHP)
By Karim PINCHON
Generics (in PHP)
Les génériques sont un concept des langages de programmation. En bref, ils permettent d'écrire du code typé mais sans être spécifiquement lié à un type particulier. Beaucoup de langages supportent déjà les génériques plus ou moins nativement : Java, C#, Go, Rust Typescript... Ce n'est pas le cas de PHP, mais grâce à sa communauté et son ecosystème, il est tout de même possible de les utiliser. Je vous propose de voir comment et dans quel but, en s'appuyant sur l'exemple de composants Symfony.
- 344