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.