Generics via Attributes in PHP — Can We Have Them?
tl;dr: How about using generics in PHP attributes?
#[<T>]
class Stack
{
public function push(#[<T>] mixed $item): void
{
}
public function pop(): #[<T>] mixed
{
}
}
Native generics. Will they be in PHP or not? Does PHP need them at all? We'll leave this speculation for the next time, but today let's discuss what generics might look like in PHP attributes.
Status-PHPDoc-quo
Nikita Popov did a comprehensive research on generics in PHP and shared detailed results here. Nikita also wrote a summary on Reddit during an AMA with the PhpStorm team:
The conclusion that Nikita came to is that there are only three ways to implement generics, and none of them will work in PHP. Or rather, it is possible to implement them, but each of them has significant drawbacks.
Nevertheless, the implementation of erased quasi-generics already exists today. I'm talking about PHPDoc annotations.
Although there is no official standard, the popular static analyzers PHPStan and Psalm, as well as PhpStorm, support a syntax that can generally be called well-established.
What prevents people from using generics via annotations? I just posed this question on Twitter. They are, after all, essentially erased generics, pretty much like in Python, for instance.
There were some constructive and substantive concerns raised in the replies:
Nothing stops me from using phpdoc generics, & I use them, but without native language support, I can?t enforce them on downstream users of my libraries, so I still have to write a lot of validation code to check types.
— Ben Ramsey @[email protected] (@ramsey) October 18, 2022
I think this would also be a problem with erased generics.
There were also comments about the usability of such generics. But what annoys me, and what the Twitter mob didn't note, is that in modern PHP code you have to use both attributes and PHPDoc annotations simultaneously.
Generics, why no attributes?
The PHPDoc annotations are unstructured strings. They were meant to be replaced by attributes, which are part of the language, and set a strict format for metadata in PHP.
However, in the case of generics, the attributes look terrible:
/** @template T of object */
class Queue
{
/** @var array<int,T> */
private array $queue = [];
/** @param T $item */
public function add($item): void {}
/** @return T */
public function next() {}
}
// The same with current attributes =>
#[Template("T", "object")]
class Queue
{
#[Type("array<int,T>")]
private array $queue = [];
public function add(#[Type("T")] $item): void {}
#[Type("T")]
public function next() {}
}
In addition, the attributes only work on declarations but not on call-site. Consequently, you cannot do this:
/** @var Queue<Person> $personQueue */
$personQueue = new Queue();
// The same with current attributes =>
#[Type("Queue<Person>")]
$personQueue = new Queue();
Generics in attributes syntax RFC
What if generics looked prettier but remained in attributes?
#[<T>]
class Stack
{
public function push(#[<T>] mixed $item): void
{
}
public function pop(): #[<T>] mixed
{
}
}
Pros:
- PHP code remains untouched and BC breaks are not added
- The code becomes cleaner and prettier (subjectively)
- Static analyzers work as with PHPDoc
- Information about generics is available in the language itself (!)
Cons:
- Type information still in two places
- Hacky syntax (?)
- What else?
Why not all the way native generics with <T>
?
Adding native erased generics we get to the inconsistency that some types are not checked at runtime. The attributes never gave a promise to change runtime behavior.
But what about runtime checks?
Since generics information is contained in attributes, it is available at runtime! This means that type checks can be implemented in userland in PHP. Such checks will probably be slower than the native ones, but the main advantage is that they can be entirely optional!
That means you can have early runtime checks in your local and test environments. For production you can disable such runtime checks and get performance boost there.
🚧 Static Analysis PoC
Here is a fork of Nikita's PHP parser that demonstrates this concept. And here is the PHPStan fork with the ability to use this syntax.
What do you think?
- How do you like this syntax?
- What problems do you see with this?
- What are other benefits and drawbacks?
Many thanks to Dave Liddament whose talk at the International PHP Conference in Munich inspired this idea. It literally came up during our discussion after Dave's talk:
It seems PHP generics is a hot topic at the moment. @pronskiy following on from our conversation at IPC, has the syntax #<> been suggested?
— Dave Liddament (@DaveLiddament) October 30, 2022
Would this work for adding type information for static analysis?
See more in gist: https://t.co/IOzSGgt1Xo
1/n https://t.co/BHkqP3cr07 pic.twitter.com/g2eIzm1ndT