phpphpstormphpdoc

PhpStorm PHPDoc type inference: is there a way to manipulate an array and let PhpStorm know its type is same as the original array?


An example is better than any explaining: Let's say I have the following code

function arrayToIdMap(array $models) : array
{
    $result = [];
    foreach ($models as $model) {
        /** @var BaseClassWithGetIdMethod $model **/
        $result[$model->getId()] = $model;
    }
    return $result;
}

/** @var Car[] **/
$carList = getCarList();

/** @var Truck[] **/
$truckList = getTruckList();

/** @var Car[] **/
$carMap = arrayToIdMap($carList);

/** @var Truck[] **/
$truckMap = arrayToIdMap($truckList);

What I want to do is, to inform PHP automatically the types of $carMap and $truckMap so that I have some code that acts like the following pseudocode:

/**
 * @param array $$models
 * @return The_Original_Type_Inferred_From_$models
 */
function arrayToIdMap(array $models) : array
{
    $result = [];
    foreach ($models as $model) {
        /** @var BaseClassWithGetIdMethod $model **/
        $result[$model->getId()] = $model;
    }
    return $result;
}

/** @var Car[] **/
$carList = getCarList();

/** @var Truck[] **/
$truckList = getTruckList();

$carMap = arrayToIdMap($carList);
$truckMap = arrayToIdMap($truckList);

// $carMap[$id]->someMethod() will not show error if Car class has the someMethod method and PhpStorm knows $carMap is of type Car[] at this point

Is this possible or not?


Solution

  • This could be done with PhpStorm's Advanced Meta functionality (PhpStorm uses it for standard array_xxx PHP functions -- have a look here: https://github.com/JetBrains/phpstorm-stubs/blob/master/meta/.phpstorm.meta.php#L64

    This does not use PHPDoc though. The extra info (advanced metadata) should be stored in a separate file for PhpStorm eyes only.

    Make .phpstorm.meta.php in the project root wit the following content:

    <?php
    
    namespace PHPSTORM_META {
        override(\arrayToIdMap(0), type(0));
    }
    

    Now the file with your code:

    <?php
    // Defining used classes and functions
    
    class BaseClassWithGetIdMethod {
        private string $id;
    
        public function getId(): string
        {
            return $this->id;
        }
    }
    
    class Car extends BaseClassWithGetIdMethod {
        public function makeCar(){}
    }
    
    class Truck extends BaseClassWithGetIdMethod {
        public function makeTruck(){}
    }
    
    function arrayToIdMap(array $models) : array
    {
        $result = [];
        foreach ($models as $model) {
            /** @var BaseClassWithGetIdMethod $model **/
            $result[$model->getId()] = $model;
        }
        return $result;
    }
    
    // Test code
    
    /** @var Car[] $carList **/
    $carList = getCarList();
    
    /** @var Truck[] $truckList **/
    $truckList = getTruckList();
    
    $carMap = arrayToIdMap($carList);
    //$carMap[0]->
    
    $truckMap = arrayToIdMap($truckList);
    //$truckMap[0]->
    

    With the code above PhpStorm provides correct methods:

    enter image description here


    If it has to be via PHPDoc ... look at generics.

    You would need Psalm (or is it PHPStan?) plugin enabled for that (even if you do not use the actual Psalm) as it is how such a support is supplied here. Anyway, both plugins are bundled with PhpStorm and enabled by default. https://www.jetbrains.com/help/phpstorm/using-psalm.html#generics-support

    The code:

    <?php
    // Defining used classes and functions
    
    class BaseClassWithGetIdMethod {
        private string $id;
    
        public function getId(): string
        {
            return $this->id;
        }
    }
    
    class Car extends BaseClassWithGetIdMethod {
        public function makeCar(){}
    }
    
    class Truck extends BaseClassWithGetIdMethod {
        public function makeTruck(){}
    }
    
    /**
     * @template T
     * @param T $models
     * @return T
     */
    function arrayToIdMapZ(array $models) : array
    {
        $result = [];
        foreach ($models as $model) {
            /** @var BaseClassWithGetIdMethod $model **/
            $result[$model->getId()] = $model;
        }
        return $result;
    }
    
    // Test code
    
    /** @var Car[] $carList **/
    $carList = getCarList();
    
    /** @var Truck[] $truckList **/
    $truckList = getTruckList();
    
    $carMapZ = arrayToIdMapZ($carList);
    $truckMapZ = arrayToIdMapZ($truckList);
    
    $carMapZ[0]->
    //$truckMapZ[0]->
    

    That's what I see in my PhpStorm:

    enter image description here