Search code examples
phppsalm-php

Use template in var docblock php


I have a php class containing a collection of class. It uses an array with classname as key and instance as value. So I have a getter that takes a classname and returns the corresponding instance (or null if not find). I'm trying through docblock to specify that the returned object is the same as passed classname

public function getService(string $service): ?object
{
    if ($this->hasService($service)) {
        return $this->services[$service];
    }

    return null;
}

So I've tried to do this :

/**
 * @template T
 * @var array<class-string<T>, T>
 */
private array $services = [];

/**
 * @template T
 * @psalm-param class-string<T> $service
 * @return T|null
 */
public function getService(string $service): ?object
{
    if ($this->hasService($service)) {
        return $this->services[$service];
    }

    return null;
}

With that, the function do what I expects. But psalm returns me 2 errors :

ERROR: InvalidReturnType - src/Services/ServiceManager.php:189:16 - The declared return type '(T:fn-marmot\brick\services\servicemanager::getservice as object)|null' for Marmot\Brick\Services\ServiceManager::getService is incorrect, got 'null|object' (see https://psalm.dev/011)
     * @return T|null


ERROR: InvalidReturnStatement - src/Services/ServiceManager.php:194:20 - The inferred type 'object' does not match the declared return type '(T:fn-marmot\brick\services\servicemanager::getservice as object)|null' for Marmot\Brick\Services\ServiceManager::getService (see https://psalm.dev/128)
            return $this->services[$service];

Solution

  • You need a class-string-map for that: https://psalm.dev/r/134a1df401

    <?php
    
    interface ServiceInterface {}
    
    class C {
        /**
         * @var class-string-map<T as ServiceInterface, T>
         */
        private array $services = [];
        
        /**
         * @template T of ServiceInterface
         * @param class-string<T> $name
         * @return T
         */
        public function getService(string $name): object {
            if (!isset($this->services[$name])) { throw new RuntimeException; }
            return $this->services[$name];
        }
    }