Les génériques

(en          )

A propos de moi

Karim Pinchon

  •    Développeur backend
  •    @kpn13
  •    https://blog.karimpinchon.com
  •    https://slides.com/kpn13

De quoi va-t-on parler ?

Les génériques ?

Des cas d'utilisations

Regardons ailleurs

En PHP

L'écosystème

L'exemple de Symfony

Sommaire

C'est quoi les génériques ?

Définition

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.

Définition

Ajouter une couche d'abstraction sur les types.

Définition

Avantages

Pas de cast (ou documentation)

Optimise et simplifie (?)

Facilite la maintenance du code

Augmente la sécurité du code

 

Des inconvénients

Notation pas (toujours) simple

Performances au runtime (?)

Aujourd'hui

Concept important

Souvent natif (Java, Go, Rust, ...)

Très attendu

Les cas d'utilisations

Des collections

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 {}
}

Des utilitaires

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[]

Des factories

function make(string $className): ???
{
    return new $className();
}

$result = make(Foo::class); // is a Foo
$result = make(Exception::class); // is an Exception

Regardons ailleurs

Quels langages ?

Des conventions ?

  • T : 1er type
  • S : 2ème type
  • U : 3ème type
  • V : 4ème type
  • K : clé d'une map
  • V : valeur d'une map
  • ...
  • TKey
  • TValue
  • TFooBar

Exemple en Go

Exemple en Go

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)

Exemple en Go

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)

Exemple en Java

Exemple en Java

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>();

Exemple en Java

class Util {
    public static <T> T findGreatest(T p1, T p2) {
        ...
    }
}

Util.findGreatest(1, 42); //42
Util.findGreatest("symfony", "laravel"); // symfony

Exemple en Typescript

Exemple en Typescript

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);

Exemple en Typescript

Exemple en Rust

Exemple en Rust

fn foo<T>(p: T) { 
    ...
}

Comment ça se passe en PHP ?

Pourquoi PHP ne supportera probablement jamais les génériques ?

(parce que Nikita l'a dit)

  • https://github.com/PHPGenerics/php-generics-rfc/issues/45
  • https://www.redditmedia.com/r/PHP/comments/...

On ne typehint plus ?

// 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];

Et si on utilisait mixed ?

  • Depuis PHP 8.0
  • object|resource|array|string|float|int|bool|null

Si on utilisait ça ?

Et si on utilisait mixed ?

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];

Quelles solutions avons-nous alors ?

Les annotations !

  • @template
  • @extends
  • @implements
  • class-string
  • ...

Les limites des annotations

la syntaxe sous forme de commentaire

coloration syntaxique quasi inexistante

validation du contenu difficile

...

/*

L'écosystème

Des outils pour nous aider

Les annotations

@template

/**
* @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

/**
* @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

/**
 * @template T
 */
interface Collection
{
  /**
   * @param T $item
   */
  public function push(mixed $item): void;

  /**
   * @return T
   */
  public function pop(): mixed;
}

@template

/**
 * @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 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('Hello Symfony Live Paris!');

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();

@implements

/**
 * @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();
    }
}

@implements

/**
 * @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';
    }
}

@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<Bar> */
class BarCollection extends Collection
{
    public function get(): mixed
    {
    	return 'Heyyy';
    }
}

Covariance et contravariance

Covariance et contravariance

PaymentMean

CreditCard

covariance

contravariance

Covariance et contravariance

Depuis PHP 7.4

  • covariance sur le type de retour
  • contravariance sur le type des arguments

J'approuve !

Covariance et 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 et 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 et contravariance

abstract class AsyncPaymentProcessor
{
    public function process(PaymentMean $paymentMean): void;
}

class BankTransfertPaymentProcessor extends AsyncPaymentProcessor
{
    public function process(PaymentMean $paymentMean): void
    {
    	
    }
}

Contravariance

Covariance et 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

Covariance et 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

Spécificités

PHPStan / PSalm

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) { ... }

Support PHPStorm

PhpStorm 2021.2

PhpStorm 2021.2

Est-ce parfait ?

Est-ce parfait ?

Pas de surcoût au runtime

Couverture fonctionnelle

Source de vérité

PHP + annotations + attributs

Pas d'obligation

Est-ce parfait ?

/**
* 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);
}

L'exemple de Symfony

Pleins de pull requests

Dans quels composants ?

Form

Finder

Messenger

Workflow

Serializer

Security

VarExporter

Http

DI

Console

...

Quelques exemples

Quelques exemples

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;
    }
    
    ...
}

Quelques exemples

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;
}

Quelques exemples

<?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;
    }
}

Quelques exemples

<?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);
}

Quelques exemples

<?php
namespace Symfony\Component\Console\Helper;

/**
 * @implements \IteratorAggregate<string, HelperInterface>
 */
class HelperSet implements \IteratorAggregate
{
}

Quelques exemples

<?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

Concept important (collection, ...)

Pas natif en PHP

Très bien supporté par des outils

https://slides.com/kpn13

Remerciements

   @kpn13

   https://blog.karimpinchon.com

  • 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

Les génériques (en PHP)

By Karim PINCHON

Les génériques (en 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.

  • 1,339