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.

  • 203