Generics via Attributes in PHP — Can We Have Them?

Published on Oct 31, 2022 by Roman Pronskiy

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.

Meme: Why can't we have generics in PHP?
https://twitter.com/brendt_gd/status/1583360505766285314

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:

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: