PHPStan Generics for Dummies - With 2 Parents

Found a typo? Edit me

In previous post we talked about how to promote generics to parent class. There we learned how to tell parent class what generic type we use (typically from ProductRepository and AbstractRepository).

Today we take the same use case and add one more parent class.

That must be easy, right?

Let's start where we ended last time:

/**
 * @extends AbstractRepository<Product>
 */
final class ProductRepository extends AbstractRepository
{
}

With an abstract parent:

/**
 * @template TEntity as object
 */
abstract class AbstractRepository
{
    /**
     * @return TEntity
     */
    public function get($id)
    {
        // ...
    }
}

PHPStan now knows exact returned type of all method annotated with TEntity:

/** @var ProductRepository $productRepository */
$product = $productRepository->get(1);

Here we always have a Product type.


Constraints Have Changed

Everything works well, our project grows, and more and more users are visiting our website. Life is good.

Just the response time is lagging more and more. How could we improve it? One way is to cache the most visited entities.

Alright! We'll add a new repository class with cache:

abstract class AbstractCachedRepository extends AbstractRepository
{
    // cache everywhere
}

And one parent switch:

 /**
  * @extends AbstractRepository<Product>
  */
-final class ProductRepository extends AbstractRepository
+final class ProductRepository extends AbstractCachedRepository
 {
     // ...
 }

Our class structure changes like this:

- ProductRepository -> AbstractRepository
+ ProductRepository -> AbstractCachedRepository -> AbstractRepository

The cache is working, and lagging is gone... we run PHPStan, and... it crashes. Why? Because the class in @extends class is no longer there:

/**
 * @extends AbstractRepository<Product>
 */
final class ProductRepository extends AbstractCachedRepository
{
}

The ProductRepository class does not see its grand-parent class AbstractRepository, only its direct parent. We'll fix that:

 /**
- * @extends AbstractRepository<Product>
+ * @extends AbstractCachedRepository<Product>
  */
 final class ProductRepository extends AbstractCachedRepository
 {
 }

Well done! Now we rerun PHPStan to see the result... and it crashes. Why?

2 Steps to Man in the Middle Class

AbstractCachedRepository is a middle man. Its job is to take type from the child class and send it to the parent class. But it's just a bare class with no annotations.


It has no idea what to do. We have to tell it to...

1. pick the type from child class

+ /**
+  * @template TEntity of object
+  */
  abstract class AbstractCachedRepository extends AbstractRepository
  {
  }

2. send it to parent class via @template-extends

  /**
   * @template TEntity of object
+  * @template-extends AbstractRepository<TEntity>
   */
  abstract class AbstractCachedRepository extends AbstractRepository
  {
  }

We run PHPStan, and... it works!


Let PHPStan Watch Your Back

Is your PHPStan passing even without generic definition in child class? You need to enable it first.

There are 2 ways. One of them is level 6+:

parameters:
    level: 6

Are you stuck on a lower level? There is single parameter you can enable instead:

parameters:
    level: 4
    checkGenericClassInNonGenericObjectType: true

Then run PHPStan and complete generic types with confidence.


Happy coding!


Have you find this post useful? Do you want more?

Follow me on Twitter, RSS or support me on GitHub Sponsors.