Issues (24)

src/TacticianRuleSet.php (11 issues)

Labels
Severity
1
<?php
2
declare(strict_types=1);
3
4
namespace League\Tactician\PHPStan;
5
6
use League\Tactician\Handler\Mapping\CommandToHandlerMapping;
7
use League\Tactician\Handler\Mapping\MethodDoesNotExist;
8
use PhpParser\Node;
9
use PhpParser\Node\Expr\MethodCall;
10
use PHPStan\Analyser\Scope;
0 ignored issues
show
The type PHPStan\Analyser\Scope was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use PHPStan\Broker\Broker;
0 ignored issues
show
The type PHPStan\Broker\Broker was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use PHPStan\Broker\ClassNotFoundException;
0 ignored issues
show
The type PHPStan\Broker\ClassNotFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use PHPStan\Reflection\MissingMethodFromReflectionException;
0 ignored issues
show
The type PHPStan\Reflection\Missi...FromReflectionException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use PHPStan\Reflection\ParametersAcceptorSelector;
0 ignored issues
show
The type PHPStan\Reflection\ParametersAcceptorSelector was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use PHPStan\Rules\Rule;
0 ignored issues
show
The type PHPStan\Rules\Rule was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use PHPStan\Type\ObjectType;
0 ignored issues
show
The type PHPStan\Type\ObjectType was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use PHPStan\Type\Type;
0 ignored issues
show
The type PHPStan\Type\Type was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use PHPStan\Type\TypeWithClassName;
0 ignored issues
show
The type PHPStan\Type\TypeWithClassName was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use PHPStan\Type\UnionType;
0 ignored issues
show
The type PHPStan\Type\UnionType was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use function array_filter;
21
use function array_merge;
22
23
/** @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\MethodCall> */
24
final class TacticianRuleSet implements Rule
25
{
26
    /**
27
     * @var CommandToHandlerMapping
28
     */
29
    private $mapping;
30
    /**
31
     * @var Broker
32
     */
33
    private $broker;
34
    /**
35
     * @var string
36
     */
37
    private $commandBusClass;
38
    /**
39
     * @var string
40
     */
41
    private $commandBusMethod;
42
43 14
    public function __construct(
44
        CommandToHandlerMapping $mapping,
45
        Broker $broker,
46
        string $commandBusClass,
47
        string $commandBusMethod
48
    ) {
49 14
        $this->mapping = $mapping;
50 14
        $this->broker = $broker;
51 14
        $this->commandBusClass = $commandBusClass;
52 14
        $this->commandBusMethod = $commandBusMethod;
53 14
    }
54
55
    public function getNodeType(): string
56
    {
57
        return MethodCall::class;
58
    }
59
60 14
    public function processNode(Node $methodCall, Scope $scope): array
61
    {
62 14
        if (! $methodCall instanceof MethodCall
63 14
            || ! $methodCall->name instanceof Node\Identifier
64 14
            || $methodCall->name->name !== $this->commandBusMethod) {
65 1
            return [];
66
        }
67
68 13
        $type = $scope->getType($methodCall->var);
69
70 13
        if (! (new ObjectType($this->commandBusClass))->isSuperTypeOf($type)->yes()) {
71
            return [];
72
        }
73
74
        // Wrong number of arguments passed to handle? Delegate to other PHPStan rules
75 13
        if (count($methodCall->args) !== 1) {
76 1
            return []; //
77
        }
78
79 12
        $commandType = $scope->getType($methodCall->args[0]->value);
0 ignored issues
show
The property value does not seem to exist on PhpParser\Node\VariadicPlaceholder.
Loading history...
80
81 12
        $errors = [];
82 12
        foreach ($this->getInspectableCommandTypes($commandType) as $commandType) {
83 11
            $errors = array_merge(
84 11
                $errors,
85 11
                $this->inspectCommandType($methodCall, $scope, $commandType)
86
            );
87
        }
88
89 12
        return $errors;
90
    }
91
92
    /**
93
     * @return array<string>
94
     */
95 11
    private function inspectCommandType(
96
        MethodCall $methodCallOnBus,
97
        Scope $scope,
98
        TypeWithClassName $commandType
99
    ): array {
100 11
        [$handlerClassName, $handlerMethodName] = $this->getHandlerClassAndMethodNames($commandType);
101
102
        try {
103 11
            $handlerClass = $this->broker->getClass($handlerClassName);
104 2
        } catch (ClassNotFoundException $e) {
105
            return [
106 2
                "Tactician tried to route the command {$commandType->getClassName()} but could not find the matching " .
107 2
                "handler {$handlerClassName}.",
108
            ];
109
        }
110
111
        try {
112 10
            $handlerMethod = $handlerClass->getMethod($handlerMethodName, $scope);
113 1
        } catch (MissingMethodFromReflectionException $e) {
114
            return [
115 1
                "Tactician tried to route the command {$commandType->getClassName()} to " .
116 1
                "{$handlerClass->getName()}::{$handlerMethodName} but while the class could be loaded, the method " .
117 1
                "'{$handlerMethodName}' could not be found on the class.",
118
            ];
119
        }
120
121
        /** @var \PHPStan\Reflection\ParameterReflection[] $parameters */
122 9
        $parameters = ParametersAcceptorSelector::selectFromArgs(
123 9
            $scope,
124 9
            $methodCallOnBus->args,
125 9
            $handlerMethod->getVariants()
126 9
        )->getParameters();
127
128 9
        if (count($parameters) === 0) {
129
            return [
130 2
                "Tactician tried to route the command {$commandType->getClassName()} to " .
131 2
                "{$handlerClass->getName()}::{$handlerMethodName} but the method '{$handlerMethodName}' does not " .
132 2
                "accept any parameters.",
133
            ];
134
        }
135
136 7
        if (count($parameters) > 1) {
137
            return [
138 1
                "Tactician tried to route the command {$commandType->getClassName()} to " .
139 1
                "{$handlerClass->getName()}::{$handlerMethodName} but the method '{$handlerMethodName}' accepts " .
140 1
                "too many parameters.",
141
            ];
142
        }
143
144 6
        if ($parameters[0]->getType()->accepts($commandType, true)->no()) {
145
            return [
146 2
                "Tactician tried to route the command {$commandType->getClassName()} to " .
147 2
                "{$handlerClass->getName()}::{$handlerMethodName} but the method '{$handlerMethodName}' has a " .
148 2
                "typehint that does not allow this command.",
149
            ];
150
        }
151
152 4
        return [];
153
    }
154
155
    /** @return TypeWithClassName[] */
156 12
    private function getInspectableCommandTypes(Type $type): array
157
    {
158 12
        $types = [];
159 12
        if ($type instanceof TypeWithClassName) {
160 11
            $types = [$type];
161
        }
162
163 12
        if ($type instanceof UnionType) {
164 1
            $types = array_filter(
165 1
                $type->getTypes(),
166 1
                function (Type $type) {
167 1
                    return $type instanceof TypeWithClassName;
168 1
                }
169
            );
170
        }
171
172 12
        return array_filter(
173 12
            $types,
174 12
            function (TypeWithClassName $type) {
175 12
                if (! $this->broker->hasClass($type->getClassName())) {
176
                    return false;
177
                }
178
179 12
                $class = $this->broker->getClass($type->getClassName());
180 12
                return ! $class->isInterface() && ! $class->isAbstract();
181 12
            }
182
        );
183
    }
184
185
    /**
186
     * @return string[]
187
     */
188 11
    private function getHandlerClassAndMethodNames(TypeWithClassName $commandType): array
189
    {
190
        try {
191 11
            $handler = $this->mapping->findHandlerForCommand($commandType->getClassName());
192
193 9
            return [$handler->getClassName(), $handler->getMethodName()];
194 3
        } catch (MethodDoesNotExist $e) {
195
            // Suppress this exception because PHPStan will find this on its own in a much nicer way
196 3
            return [$e->getClassName(), $e->getMethodName()];
197
        }
198
    }
199
}
200