Passed
Push — master ( 558002...98c96a )
by Ross
02:03
created

TacticianRuleSet::getNodeType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace League\Tactician\PHPStan;
5
6
use League\Tactician\Handler\Mapping\CommandToHandlerMapping;
7
use PhpParser\Node;
8
use PhpParser\Node\Expr\MethodCall;
9
use PHPStan\Analyser\Scope;
10
use PHPStan\Broker\Broker;
11
use PHPStan\Broker\ClassNotFoundException;
12
use PHPStan\Reflection\MissingMethodFromReflectionException;
13
use PHPStan\Reflection\ParametersAcceptorSelector;
14
use PHPStan\Rules\Rule;
15
use PHPStan\Type\ObjectType;
16
use PHPStan\Type\Type;
17
use PHPStan\Type\TypeWithClassName;
18
use PHPStan\Type\UnionType;
19
use function array_filter;
20
use function array_merge;
21
22
final class TacticianRuleSet implements Rule
23
{
24
    /**
25
     * @var CommandToHandlerMapping
26
     */
27
    private $mapping;
28
    /**
29
     * @var Broker
30
     */
31
    private $broker;
32
    /**
33
     * @var string
34
     */
35
    private $commandBusClass;
36
    /**
37
     * @var string
38
     */
39
    private $commandBusMethod;
40
41 13
    public function __construct(CommandToHandlerMapping $mapping, Broker $broker, string $commandBusClass, string $commandBusMethod)
42
    {
43 13
        $this->mapping = $mapping;
44 13
        $this->broker = $broker;
45 13
        $this->commandBusClass = $commandBusClass;
46 13
        $this->commandBusMethod = $commandBusMethod;
47 13
    }
48
49 13
    public function getNodeType(): string
50
    {
51 13
        return MethodCall::class;
52
    }
53
54 13
    public function processNode(Node $methodCall, Scope $scope): array
55
    {
56 13
        if (! $methodCall instanceof MethodCall
57 13
            || ! $methodCall->name instanceof Node\Identifier
58 13
            || $methodCall->name->name !== $this->commandBusMethod) {
59 1
            return [];
60
        }
61
62 12
        $type = $scope->getType($methodCall->var);
63
64 12
        if (! (new ObjectType($this->commandBusClass))->isSuperTypeOf($type)->yes()) {
65
            return [];
66
        }
67
68
        // Wrong number of arguments passed to handle? Delegate to other PHPStan rules
69 12
        if (count($methodCall->args) !== 1) {
70 1
            return []; //
71
        }
72
73 11
        $commandType = $scope->getType($methodCall->args[0]->value);
74
75 11
        $errors = [];
76 11
        foreach ($this->getInspectableCommandTypes($commandType) as $commandType) {
77 11
            $errors = array_merge(
78 11
                $errors,
79 11
                $this->inspectCommandType($methodCall, $scope, $commandType)
80
            );
81
        }
82
83 11
        return $errors;
84
    }
85
86
    /**
87
     * @return array<string>
88
     */
89 11
    private function inspectCommandType(
90
        MethodCall $methodCallOnBus,
91
        Scope $scope,
92
        TypeWithClassName $commandType
93
    ): array {
94 11
        $handlerClassName = $this->mapping->getClassName($commandType->getClassName());
95
96
        try {
97 11
            $handlerClass = $this->broker->getClass($handlerClassName);
98 2
        } catch (ClassNotFoundException $e) {
99
            return [
100 2
                "Tactician tried to route the command {$commandType->getClassName()} but could not find the matching " .
101 2
                "handler {$handlerClassName}.",
102
            ];
103
        }
104
105 10
        $handlerMethodName = $this->mapping->getMethodName($commandType->getClassName());
106
107
        try {
108 10
            $handlerMethod = $handlerClass->getMethod($handlerMethodName, $scope);
109 1
        } catch (MissingMethodFromReflectionException $e) {
110
            return [
111 1
                "Tactician tried to route the command {$commandType->getClassName()} to " .
112 1
                "{$handlerClass->getName()}::{$handlerMethodName} but while the class could be loaded, the method " .
113 1
                "'{$handlerMethodName}' could not be found on the class.",
114
            ];
115
        }
116
117
        /** @var \PHPStan\Reflection\ParameterReflection[] $parameters */
118 9
        $parameters = ParametersAcceptorSelector::selectFromArgs(
119 9
            $scope,
120 9
            $methodCallOnBus->args,
121 9
            $handlerMethod->getVariants()
122 9
        )->getParameters();
123
124 9
        if (count($parameters) === 0) {
125
            return [
126 2
                "Tactician tried to route the command {$commandType->getClassName()} to " .
127 2
                "{$handlerClass->getName()}::{$handlerMethodName} but the method '{$handlerMethodName}' does not " .
128 2
                "accept any parameters.",
129
            ];
130
        }
131
132 7
        if (count($parameters) > 1) {
133
            return [
134 1
                "Tactician tried to route the command {$commandType->getClassName()} to " .
135 1
                "{$handlerClass->getName()}::{$handlerMethodName} but the method '{$handlerMethodName}' accepts " .
136 1
                "too many parameters.",
137
            ];
138
        }
139
140 6
        if ($parameters[0]->getType()->accepts($commandType, true)->no()) {
141
            return [
142 2
                "Tactician tried to route the command {$commandType->getClassName()} to " .
143 2
                "{$handlerClass->getName()}::{$handlerMethodName} but the method '{$handlerMethodName}' has a " .
144 2
                "typehint that does not allow this command.",
145
            ];
146
        }
147
148 4
        return [];
149
    }
150
151
    /** @return TypeWithClassName[] */
152 11
    private function getInspectableCommandTypes(Type $type): array
153
    {
154 11
        if ($type instanceof TypeWithClassName) {
155 10
            return [$type];
156
        }
157
158 1
        if ($type instanceof UnionType) {
159 1
            return array_filter(
160 1
                $type->getTypes(),
161
                function (Type $type) {
162 1
                    return $type instanceof TypeWithClassName;
163 1
                }
164
            );
165
        }
166
167
        return [];
168
    }
169
}
170