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