Language

Erased generics

Reuse classes and functions while keeping the types of the values you pass in and get back.

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 string $id { get; }
}

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 bound T : Entity lets the class read $entity->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 bound when the generic code needs a member every value shares:

interface HasSlug
{
    public string $slug { get; }
}

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

Use a bound when your function needs to access a shared property or method. Here, T : HasSlug lets indexBySlug read each item's slug, while the returned map keeps the caller's specific item type.

Passing generic values to other APIs

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 mixed $value)
    {
    }

    /**
     * @return T
     */
    public function get(): mixed
    {
        return $this->value;
    }
}

PHPStan and IDEs can still read the type relationship from generated @template, @param, and @return PHPDoc. Generic inheritance and trait use are described by @extends, @implements, and @use. You write the types in .ppphp; the build maintains these comments for your PHP tools.

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.