Documentation
Erased generics
Preserve relationships between values across reusable APIs without adding runtime types or wrappers.A repository that returns object can store many kinds of record, but every caller must cast or hope. A repository hard-coded to Customer is safe, but cannot be reused for Order. Generics let one declaration express the relationship: “the type you configure this repository with is the type it saves and returns.”
Keep a type relationship intact
<?php
interface Entity
{
public function id(): string;
}
final class Repository<T : Entity>
{
public function __construct(private Storage $storage)
{
}
public function save(T $entity): void
{
$this->storage->put($entity->id(), $entity);
}
public function find(string $id): ?T
{
return $this->storage->get($id);
}
}
T is a type parameter. The constraint T : Entity lets the class call id() and ensures every repository value implements Entity.
A use such as Repository<Customer> now carries a precise contract:
Repository<Customer> $customers = createCustomerRepository();
?Customer $customer = $customers->find($customerId);
The result is ?Customer because the selected type argument flows through the method return. No cast and no duplicated customer-specific repository interface are required.
Where type parameters can appear
++PHP supports generic:
- classes;
- interfaces;
- traits;
- functions;
- methods;
- references to those declarations.
A standalone function can preserve its caller's type:
function first<T>(array<T> $values): ?T
{
if ($values === []) {
return null;
}
return $values[0];
}
?Order $firstOrder = first($orders);
The compiler infers type arguments from ordinary function arguments and expected return types. In this example, first($orders) produces ?Order.
Generic references can be nested:
Repository<User>
Box<array<Order>>
iterable<int, User>
array<string, Repository<Customer>>
Use constraints when the implementation needs behavior
An unconstrained T can be stored, returned, or passed through. Add one constraint when the generic code needs to call a shared method:
interface HasSlug
{
public function slug(): string;
}
function indexBySlug<T : HasSlug>(array<T> $items): array<string, T>
{
array<string, T> $index = [];
foreach ($items as T $item) {
$index[$item->slug()] = $item;
}
return $index;
}
Choose the narrowest capability that makes the algorithm possible. A bound is not a label; it is the behavior available inside the generic body.
Keep each type argument exact
Box<Dog> is not automatically a Box<Animal>. If both reading and writing are possible, treating those types as interchangeable could allow a Cat to be written into a box created for Dog.
If an API only needs every value to share one capability, use a constraint for that capability. If it genuinely accepts unrelated values, declare mixed instead of weakening a more precise type.
Understand erasure
Generics exist for checking and documentation, not as new runtime classes. This ++PHP declaration:
final class Box<T>
{
public function __construct(private T $value)
{
}
public function get(): T
{
return $this->value;
}
}
emits one ordinary PHP class:
/**
* @template T
*/
final class Box
{
/** @param T $value */
public function __construct(private $value)
{
}
/** @return T */
public function get()
{
return $this->value;
}
}
The executable syntax contains no <T>, but deterministic @template, @param, @return, @extends, @implements, and @use metadata preserves the relationship for PHPStan, IDEs, and PHP consumers.
The compiler emits one PHP Box class. Box<Customer> and Box<Order> describe different checked uses of that class while sharing the same runtime definition.
Pass runtime choices as values
T describes a relationship between types, so it cannot be used as a PHP value at runtime. The compiler rejects:
new T();
T::class;
$value instanceof T;
Call generic functions normally and let the compiler infer the type from their arguments and expected return type. When code must choose a class or behavior at runtime, pass a typed class string, factory, callable, or strategy object.
Cross the PHP boundary
Existing PHP can expose generic relationships through accurate PHPDoc:
/**
* @template T
* @param list<T> $values
* @return T|null
*/
function legacy_first(array $values): mixed
{
return $values[0] ?? null;
}
++PHP reads that metadata when analyzing a call. In the other direction, emitted PHPDoc lets ordinary PHP tools retain relationships introduced in .ppphp. Native ++PHP syntax remains authoritative when source syntax and PHPDoc disagree in a .ppphp file.
Use typed arrays for list and map relationships, then see Mixed projects for an incremental adoption workflow.