Generics and Value Objects

Generics by Docblock and Immutable Value Objects

PHP has no generic types: array cannot say "array of Money". A bound-erased generics RFC of May 2026 is marked Declined, so docblock generics remain the convention: PHPStan 376,908 and Psalm (Debugging and Analysis) read @template T and list<T> and check every call, while the engine ignores them. Pair them with value objects, small readonly classes compared by value and changed only by returning a copy.

An immutable Money and a TypedList<T> checked by static analysisPHP
<?php
final readonly class Money {
  public function __construct(public int $cents, public string $currency = 'USD') {}
  public function plus(Money $o): static {
    if ($o->currency !== $this->currency) throw new LogicException('Currency mismatch');
    return clone($this, ['cents' => $this->cents + $o->cents]);   // a new object
  }
}
/** @template T */
final class TypedList {
  /** @param list<T> $items */
  public function __construct(public private(set) array $items = []) {}
  /**
   * @param T $item
   * @return self<T>
   */
  public function with(mixed $item): self { return new self([...$this->items, $item]); }
}
/** @var TypedList<Money> $cart */
$cart = new TypedList()->with(new Money(1999))->with(new Money(500));
$total = array_reduce($cart->items, fn(Money $a, Money $b) => $a->plus($b), new Money(0));
var_dump($total->cents, $total == new Money(2499));    // == compares values
$broken = $cart->with('ten dollars');              // runs, but violates TypedList<Money>
Output
int(2499)
bool(true)

The last line runs, because T is erased at run time. PHPStan (composer require --dev phpstan/phpstan) catches it:

PHPStan reports the type-argument violationPHP
vendor/bin/phpstan analyse --level=9 --no-progress --error-format=raw money.php
Output
/tmp/ch04-11/money.php:23:Parameter #1 $item of method TypedList<Money>::with() expects Money,
  string given.