Passed
Push — master ( 7b3c3d...558002 )
by Ross
03:47
created

TacticianRuleSet::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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