On Mon, May 11, 2026, at 11:25 AM, Seifeddine Gmati wrote:
> Hi Rowan,
>
> I think the core premise is empirically testable, and the test has
> already run: PHP has had generics in docblocks for a decade, used by
> every major framework. If "someone publishes a library with broken
> type information and downstream users get surprised by runtime
> behavior" were a real failure mode, we would see it today, as Laravel,
> Doctrine, Symfony, PSL, and PHPUnit all run on `@template`-annotated
> code and are consumed by millions of downstream applications. The
> failure mode you describe is the one that would occur most under the
> current system, yet it doesn't.
>
> The reason it doesn't is that the people who care about generic type
> information are the same people who run static analysis. The
> intersection of "uses generics" and "doesn't run a static analyzer" is
> approximately empty in practice. Native syntax doesn't change that. It
> just gives current users a better way to express what they mean.
>
> Regarding the second point, the proposed implementation is enforced
> more at compile time than docblocks are. The engine validates:
>
> - Generic parameter declarations: arity cap, bound conformance,
> default-vs-bound, no top-level self-reference, no shadowing.
> - Variance soundness: Covariant parameters cannot appear in input
> positions, contravariant parameters cannot appear in output positions,
> at the declaration site.
> - Inheritance: arity at extends/implements/use, bound conformance with
> bound-on-bound for forwarded parameters, diamond detection, parametric
> LSP into properties/hooks/trait methods/inherited methods.
> - Turbofish at runtime: arity and bounds at every call site that
> supplies type arguments.
>
> Docblocks validate none of this; the engine accepts a docblock that
> says anything. Native syntax is a stricter contract than what the
> ecosystem has today.
>
> Finally, on the "broken code published to Packagist" concern: someone
> can already publish:
>
> ```
> /** Map the given array using the provided callable. */
> function map(array $array, callable $fn): array {
> return [];
> }
> ```
>
> The signature claims to take an array and a callable, returns an
> array. The body returns an empty array regardless. Today, this passes
> every PHP type check, parses without error, runs in production, and is
> technically conformant. Nobody is calling for PHP to ship a [precious
> type system](https://www.youtube.com/watch?v=Ay-gnCqDw9o) with support
> for
> [lifetimes](https://doc.rust-lang.org/rust-by-example/scope/lifetime/explicit.html),
> and [const generic
> parameters](https://doc.rust-lang.org/reference/items/generics.html#r-items.generics.const.usage)
> to prevent it (e.g. `function map<'a, I, O, const SIZE: int>(vec<&'a
> I, SIZE> & ![] $array, fn(&'a I): &'a O $fn): vec<&'a O, SIZE> &
> ![]`). The same logic applies to generics: the contract a user writes
> is the contract a user is responsible for honoring, and PHP's role is
> to provide tools for expressing and checking that contract, not to
> make every conceivable mistake impossible.
>
> The options you listed both close off paths the PHP ecosystem has
> already shown it doesn't want.
>
> Cheers,
> Seifeddine.
Hi Seifeddine.
First off, let me congratulate you on an absolutely superb "stage setting" in
this RFC. :-) The first half (the history and arguments for it) is solid, and
I very much appreciate the level of detail it includes.
That said, I tend to agree with Levi. Erased types are a foot-gun.
> The options you listed both close off paths the PHP ecosystem has
> already shown it doesn't want.
I'm not sure I follow what you're saying here, but it's worth noting that the
two points Levi mentioned have never actually been viable, so saying "the
ecosystem doesn't want it" isn't justified. I strongly suspect that the
associated types work that Gina was (and still is, AFAIK) working on would be
well-received and widely used.
As you note, this RFC is a little more than erased generics; it does provide a
little validation, and reflection support (in addition to a standardized syntax
that's much more understandable than the current de facto standard docblock
syntax). That's not nothing, and is a marginal improvement over the status
quo. The question is whether that is enough of a benefit, and what future
improvements it makes easier/harder.
I'm also not sure we can conclude that
> The intersection of "uses generics" and "doesn't run a static analyzer" is
> approximately empty in practice.
Technically, anyone using Symfony or Laravel is "using generics" in that
Symfony and Laravel have generic doc-types on their code. That doesn't imply
that everyone building a site with Symfony or Laravel is regularly running
PHPStan to verify those, or adding their own doc-types to match it. They
*should* be, but I've worked on teams that were using Laravel and I had to
teach them about PHPStan.
Where this becomes a land-mine is less the production deploys today, but that
future improvements become BC breaks. Technically only BC breaks for sloppy
code, but we've gotten ample flack in the past for "BC breaks only for sloppy
code" (eg, promoting undefined vars/keys to warnings, adding return types to
magic methods/interfaces, etc.). My fear (and I don't know how to quantify how
justified this fear is) is that people who don't use SA tools will write code
on top of someone else's generic code, not care that their types are buggy, not
notice, and then we start enforcing it in the future and their code breaks.
I wouldn't want to have a generics-version of #[ReturnTypeWillChange].
A firm and explicitly documented agreement that "if your types are wrong,
that's not covered by BC, so we don't care if it breaks on you later" would be
an option; not a popular option, perhaps, but an option. :-)
Another option, if we want to take the stance that SA is The Solution(tm) to
generics, is to ship an actual first-party SA tool with PHP. Probably not as
robust or pedantic as PHPStan/Psalm/Mago, but at the very least an enhanced
alternative to the existing lint support, to catch things like type mismatches,
generics errors, etc. Yes, I realize this opens up a whole other can of worms,
but generics always opens a can of worms.
I think the associated types enforcement could absolutely be done at the same
time as the erased-for-callsite part. But if we do erased, does that make
adding associated types (basically "declaration enforcement") in the future
harder? Either technically or politically? If so, then that's a problem. If
we set up the processes (both technical and political) to ensure that it is not
harder to add, then we're in a much better position.
So for me to vote in favor of this RFC, we would need to have *some* degree of
first-party enforcement, if for no other reason so that we can improve that
enforcement (whether runtime or otherwise) in the future, bit by bit. "PHP
ships with what you need to write valid PHP" is a good rule to have. We can
(and assuredly will) debate what that means in practice.
As to the RFC details itself:
Syntax notes:
- I agree with Bob that the +/- syntax is very confusing. I would much rather
use in/out, as seen in Kotlin and C#, which are both vastly more
self-documenting and match what some of our peer languages are doing. As Bob
notes, this can be done without completely claiming the "in" and "out" keywords.
- An interesting quirk I found in my previous research[1] is that languages
that use : for inheritance use : for bounding generics. Languages that use
"extends" for inheritance also use "extends" for bounding generics. PHP uses
"extends", so that pattern would suggest we use "class Box<T extends FooBar>"
rather than :. That's longer, though. I'm not sure how I feel here; we
certainly don't have to use the same keyword in both places, but it could be
expected. I mention it mainly so that we can have that discussion and make a
deliberate, informed decision.
Other notes:
- I am completely OK with skipping typed arrays at this point. In practice I'd
rather build objects with nice operator overrides directly into the stdlib.
(See my previous investigation[2] on the subject.)
- I fully expect "turbofish" to result in all kinds of slide shenanigans at
conferences. At least on my slides. :-)
- What would a turbofish on a static call look like? self::<Bar>foo(1) or
self::::<Bar>foo(1)? The RFC should specify.
- I'd be completely OK with lowering the argument cap from 127, too. Like, if
you have more than 4 then you're probably doing something very wrong already.
:-) Reserving more space there seems fine.
- Why is ReflectionGenericVariance backed? I don't see what the ints add.
- The "What is/isn't enforced" section could really use examples. A lot of it
is hard for me to follow as it's so abstract. (And thus determine if it's
"enough" enforcement for me to be able to support it.) Same for the
Limitations section. Examples please.
- OK, having read the full RFC, it seems to be sort of "mostly-erased"
generics. There is notably more enforcement than the term "erased" would
imply. (IE, I'd normally expect that term to mean Python-style "they may as
well be comments" types.) I would recommend raising the "what is enforced"
section, or some junior version of it, way up to the top to help set
expectations, because this is an interesting semi-enforced hybrid. I am still
unsure if it's enforced-enough, but I think calling it "erased" (and most
people won't automatically grok what "bound erased means") is doing it a
disservice. It looks like, in practice, basically everything on the
declaration side is enforced; it's just the call side that is unenforced. Is
that an approximately accurate summary?
- I recommend making a bigger deal of the fact that the turbofish being
optional is a BC layer. Having the absence of it default to mixed is a *huge*
deal for making generics adoption smoother. This is an attribute that should
get a lot more attention than the RFC currently gives it.
- If I understand correctly, this RFC would allow for foo(Collection<int> $c)
{}, but wouldn't actually enforce it at runtime. Within the function,
`instanceof Collection` would still work, but there's no `instanceof
Collection<int>` alternative. Am I reading that correctly?
- I don't see an example of how you'd write "the return value is of the class
specified by this string parameter." That's one of the status quo examples you
show, and one that I use rather frequently. Please include an example of that,
or note that it is not supported and what we should do instead.
I think at this point I am still skeptical, but warming to it, and could be
convinced. But more convincing is needed. And lunch, which I think I need
after reading all of this. :-)
Thanks, Seifeddine!
--Larry Garfield
[1] https://github.com/Crell/php-rfcs/blob/master/generics/use-cases.md
[2] https://github.com/Crell/php-rfcs/blob/master/collections/research-notes.md