Generics in PHP? Inside the Bound-Erased Generic Types RFC
PHP's Bound-Erased Generic Types RFC brings native generic syntax to PHP 8.6. Here is how bound erasure works, the syntax, and why the vote is contested.
Generics are perennially at the top of the “what does PHP still lack” list. Every “What’s New in PHP” talk fields the question, and every year the answer has been some version of “it is hard, and we are working on it.” Right now there is a concrete proposal on the table. The Bound-Erased Generic Types RFC is in voting through June 28, 2026, targeting PHP 8.6, and it is one of the more divisive proposals the internals list has seen in a while. Let me walk through what it actually does, because the headline of “PHP gets generics” hides a lot of nuance.
What the RFC Proposes
The RFC adds generic type syntax across the language. You can declare type parameters on classes, interfaces, traits, functions, methods, closures, and arrow functions. Those parameters can carry bounds, defaults, and variance markers, and you can supply type arguments at use sites and at call sites. Here is the canonical example from the proposal:
final readonly class Pair<+L, +R> {
public function __construct(
public L $left,
public R $right,
) {}
public function swap(): Pair<R, L> {
return new Pair($this->right, $this->left);
}
}
final readonly class Box<+T> {
public function __construct(
public T $value,
) {}
public function map<U>(callable $fn): Box<U> {
return new Box(($fn)($this->value));
}
}
function identity<T>(T $value): T {
return $value;
}
The +L and +R markers declare covariance. A leading - would mark contravariance, and an unmarked parameter is invariant. This matters because a generic class is usually invariant when a type placeholder appears in both read and write positions, and variance markers let library authors loosen that where it is safe.
At call sites you get what other languages call turbofish syntax, using ::<...> to pin the type arguments explicitly:
$greeting = new Box::<string>("hello, world");
$result = identity::<Pair<int, string>>($swapped);
There is also a new Reflection API that exposes the pre-erasure metadata, so tooling can read the declared type parameters even though the engine itself does not act on them. That last point is the whole story, so let us talk about erasure.
The “Bound-Erased” Part
This is where the RFC makes its central design bet, and it is the reason for the awkward name. At compile time, every type parameter is replaced by its declared bound, or by mixed if it has no bound. The runtime virtual machine is left completely unchanged. In other words, Box<string> and Box<int> are the same class at runtime, and the engine performs no checking of the generic arguments at all. Verifying that you actually put a string in your Box<string> is left to static analysis tools like PHPStan and Psalm.
The model is borrowed directly from Java, which has shipped erased generics for two decades, and it aligns with the existing PHPDoc @template ecosystem that already runs across hundreds of thousands of files on GitHub. The pitch is straightforward: you get clean native syntax instead of docblocks, the runtime cost is essentially zero, and the door stays open for opt-in reified generics or monomorphization to be layered on later without a breaking change.
If you already lean on PHPStan or Psalm generics, the appeal is obvious. Type relationships that currently live only in comments would move into real syntax that the parser, Reflection, and your IDE can all see. The author, Saif Eddin Gmati, built this on top of the earlier Foundation-sponsored work by Arnaud Le Blanc, which itself continued Nikita Popov’s experimental reified-generics implementation. This is not a weekend proposal.
Why It Is Contentious
Here is the catch that has split the internals list. PHP has had runtime type checking since version 5.0, a full decade before strict_types existed. Pass the wrong type to a typed parameter and the engine reacts, either by coercing or by throwing. Erased generics break that expectation. Native-looking syntax that the engine silently ignores is, to many internals developers, a genuine inconsistency rather than a convenience.
Gina Peter Banyard laid out the case against in a detailed opinion piece. The core argument is that most developers will reasonably assume Box<string> is checked at runtime, and will be surprised when it is not. Worse, the feature is effectively designed for static analysis users, but the data does not support the claim that nearly everyone uses such tools. The JetBrains State of PHP 2025 survey put PHPStan adoption around 36 percent and Psalm around 8 percent, with roughly 40 percent of respondents using no code quality tooling at all. If erased generics ship, libraries will start using them, and consumers who never run an analyzer will get syntax that does nothing for them and silently lets bad values through.
Consider the inconsistency directly:
class StringList {
public function add(string $value) { /* coerces 123 to "123" */ }
}
class GenericList<T> {
public function add(T $value) { /* no coercion, no check */ }
}
$a = new StringList();
$a->add(123); // becomes "123" at runtime
$b = new GenericList::<string>();
$b->add(123); // stays 123, engine does not care
Two calls that look almost identical behave differently, and nothing at runtime tells you why.
The counter-proposal from that camp is gradual, runtime-checked generics. Banyard has a working prototype limited to invariant generics on interfaces, with real runtime enforcement. The argument is that PHP’s type system has always grown incrementally, from class types in 5.0 through scalar types in 7.0 to DNF types in 8.2, so generics should arrive the same way rather than as a large erased feature that may never become reified.
Where It Stands
As of mid-June, the official internals vote is trending toward failing, since RFCs require a two-thirds supermajority and the runtime-consistency objection has real weight on the list. The community sentiment is warmer, which captures the tension perfectly: application developers who already write @template annotations want the syntax now, while the people maintaining the language want semantics that stay coherent for everyone.
Whichever way the vote lands, the conversation is healthy. Even a “no” advances the design, because it forces the next proposal to grapple with runtime checking head on. If you maintain a library that leans heavily on generic docblocks, this RFC is worth reading in full and forming your own opinion, because the syntax it establishes, erased or not, is likely to shape whatever eventually does pass.