3min.

Do you want more PHPStan violations?

Edit 2019–07–08: Good news! PHPStan 0.11.10 includes support for inferring private property type from constructor! https://github.com/phpstan/phpstan/releases/tag/0.11.10:

Turn on with inferPrivatePropertyTypeFromConstructor: true


We use PHPStan a lot and we love it. Some of us even donate each month some money via Patreon.

I faced an issue few months ago, and I hit the very same one today while I started a big refactoring of an application. And I totally forgot about it 😂 Memory sucks :)

So let’s talk about it, and see how to solve it. And now I hope I will remember to fix it on every project I’m working on.

Consider this piece of code:

class Model
{
}

class ServiceA
{
    private $serviceB;

    public function __construct(ServiceB $serviceB)
    {
        $this->serviceB = $serviceB;
    }

    public function init()
    {
        // This line will generate a FatalError (DateTime != Model)
        $this->serviceB->process(new DateTime());
    }
}

class ServiceB
{
    public function process(Model $model)
    {
    }
}

I’m expecting to get this error:

Parameter #1 $model of method ServiceB::process() expects Model, DateTime given.

But why is it valid? Because PHPStan does not infer type from constructor argument. And this is totally valid to not do so.

A solution would be to add some PHPDoc:

class ServiceA
{
    /** @var ServiceB */
    private $serviceB;
}

In the Symfony community, we have some standards. Additionally, I hate useless PHPDoc. I don’t want to pollute my code by adding them everywhere.

Symfony’s coding standards state to not add the PHPDoc to a property if it can be inferred from the constructor, it is also the default configuration of PHP CS Fixer and it is considered a best practice by some.

Since PHPStan has a nice plugin system, I wrote a plugin to automatically add theses type-hint in PHPStan engine few months ago. I used it in my current application and I was able to find 45 new violations \o/

You can read the code and add it to your project if you want to.


<?php

use PHPStan\Broker\Broker;
use PHPStan\Broker\BrokerFactory as PhpstanBrokerFactory;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\Php\PhpPropertyReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\Type\ObjectType;

class BrokerFactory extends PhpstanBrokerFactory
{
    public function create(): Broker
    {
        $broker = parent::create();

        $r = new \ReflectionProperty($broker, 'propertiesClassReflectionExtensions');
        $r->setAccessible(true);

        $propertiesClassReflectionExtensions = $r->getValue($broker);
        array_unshift($propertiesClassReflectionExtensions, new PropertiesClassReflectionBasedOnConstructor());
        $r->setValue($broker, $propertiesClassReflectionExtensions);

        return $broker;
    }
}

class PropertiesClassReflectionBasedOnConstructor implements PropertiesClassReflectionExtension
{
    public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
    {
        return $this->extractFromConstructor($classReflection->getNativeReflection(), $propertyName);
    }

    public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
    {
        $nativeClassReflection = $classReflection->getNativeReflection();

        foreach ($nativeClassReflection->getConstructor()->getParameters() as $parameter) {
            if ($propertyName !== $parameter->name) {
                continue;
            }

            $type = new ObjectType((string) $parameter->getType());

            return new PhpPropertyReflection($classReflection, $type, $nativeClassReflection->getProperty($propertyName), false, false);
        }
    }

    private function extractFromConstructor(\ReflectionClass $nativeClassReflection, string $propertyName)
    {
        if (!$nativeClassReflection->hasProperty($propertyName)) {
            return false;
        }

        $constructor = $nativeClassReflection->getConstructor();
        if (!$constructor) {
            return false;
        }

        foreach ($constructor->getParameters() as $parameter) {
            if ($propertyName !== $parameter->name) {
                continue;
            }

            if (!$parameter->getType()) {
                return false;
            }

            if ($parameter->isOptional()) {
                return false;
            }

            return class_exists((string) $parameter->getType());
        }

        if ($parentClass = $nativeClassReflection->getParentClass()) {
            try {
                $reflectionClass = new \ReflectionClass($parentClass);
            } catch (\ReflectionException $e) {
                return false;
            }

            return $this->extractFromConstructor($reflectionClass, $propertyName);
        }

        return false;
    }
}

Commentaires et discussions

Nos articles sur le même sujet

Ces clients ont profité de notre expertise