Search code examples
phplaravelilluminate-containerphpstan

How to get phpstan to infer the type for my Laravel Collection pipeline?


Given my class

<?php
declare(strict_types=1);

use Illuminate\Support\Collection;
use stdClass;

class PhpstanIssue
{
    /**
     * @param Collection<Collection<stdClass>> $collection
     *
     * @return Collection<Foo>
     */
    public function whyDoesThisFail(Collection $collection): Collection
    {
        return $collection
            ->flatten() // Collection<stdClass>
            ->map(static function (\stdClass $std): ?Foo {
                return Foo::get($std);
            }) // should now be Collection<?Foo>
            ->filter(); // should now be Collection<Foo>
    }
}

I am highely confused why phpstan (0.12.64) would fail with:

18: [ERROR] Method PhpstanIssue::whyDoesThisFail() should return
Illuminate\Support\Collection&iterable<Foo> but returns 
Illuminate\Support\Collection&iterable<Illuminate\Support\Collection&iterable<stdClass>>. (phpstan)

Why can't phpstan infer the proper result type of this pipe? How can I make phpstan understand the pipe?


I can verify that my code works within a phpunit testcase:

class MyCodeWorks extends TestCase
{
    public function testPipeline()
    {
        $result = (new PhpstanIssue())->whyDoesThisFail(
            new Collection(
                [
                    new Collection([new \stdClass(), new \stdClass()]),
                    new Collection([new \stdClass()]),
                ]
            )
        );

        self::assertCount(3, $result);
        foreach ($result as $item) {
            self::assertInstanceOf(Foo::class, $item);
        }
    }
}

will pass.


My Foo is just a dummy class for the sake of this question. It's only relevant that it takes a stdClass instance and transforms it into a ?Foo one.

class Foo
{
    public static function get(\stdClass $std): ?Foo
    {
        // @phpstan-ignore-next-line
        return (bool) $std ? new static() : null;
    }
}


Solution

  • Illuminate\Support\Collection class is not generic on its own. So writing Collection<Foo> is wrong. That causes the error messages like Illuminate\Support\Collection&iterable<Illuminate\Support\Collection&iterable<stdClass>>

    You have two options:

    1. Installing Larastan. It's a PHPStan extension for Laravel. And it has stub files that makes Illuminate\Support\Collection class generic.

    2. Or if you are just using the illuminate/collections standalone package without full Laravel app you can write your own stub files. From PHPStan docs:

    ... you can write a stub file with the right PHPDoc. It’s like source code, but PHPStan only reads PHPDocs from it. So the namespace and class/interface/trait/method/function names must match with the original source you’re describing. But method bodies can stay empty, PHPStan is only interested in the PHPDocs.

    For your example the following stub file should be enough:

    <?php
    
    namespace Illuminate\Support;
    
    /**
     * @template TKey
     * @template TValue
     * @implements \ArrayAccess<TKey, TValue>
     * @implements Enumerable<TKey, TValue>
     */
    class Collection implements \ArrayAccess, Enumerable
    {
        /**
         * @template TReturn
         * @param callable(TValue, TKey): TReturn $callable
         * @return static<TKey, TReturn>
         */
        public function map($callable) {}
    }