Completed
Push — master ( 8bbcd1...4bee86 )
by Denis
04:24
created

Processor   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 271
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 13
dl 0
loc 271
c 0
b 0
f 0
rs 8.3157

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A addHandler() 0 9 2
A addMapper() 0 4 1
B process() 0 21 5
A handleCallUnit() 0 11 2
A handleNotificationUnit() 0 11 2
A handleErrorUnit() 0 4 1
B getClassAndMethod() 0 13 5
A invoke() 0 20 3
C prepareActualParameters() 0 50 10
A isNamedParameters() 0 4 1
A matchType() 0 13 3
C isType() 0 22 7

How to fix   Complexity   

Complex Class

Complex classes like Processor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Processor, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpJsonRpc\Server;
4
5
use PhpJsonRpc\Core\Call\CallUnit;
6
use PhpJsonRpc\Core\Call\CallError;
7
use PhpJsonRpc\Core\Call\CallNotification;
8
use PhpJsonRpc\Core\CallSpecifier;
9
use PhpJsonRpc\Core\Result\AbstractResult;
10
use PhpJsonRpc\Core\Result\ResultError;
11
use PhpJsonRpc\Core\Result\ResultNotification;
12
use PhpJsonRpc\Core\Result\ResultUnit;
13
use PhpJsonRpc\Core\ResultSpecifier;
14
use PhpJsonRpc\Error\InvalidParamsException;
15
use PhpJsonRpc\Error\JsonRpcException;
16
use PhpJsonRpc\Error\MethodNotFoundException;
17
use PhpJsonRpc\Error\ServerErrorException;
18
19
class Processor
20
{
21
    /**
22
     * @var array
23
     */
24
    private $handlers = [];
25
26
    /**
27
     * @var MapperInterface[]
28
     */
29
    private $mappers = [];
30
31
    /**
32
     * Processor constructor.
33
     */
34
    public function __construct()
35
    {
36
        $this->mappers[] = new GeneralMapper();
37
    }
38
39
    /**
40
     * @param mixed $object
41
     */
42
    public function addHandler($object)
43
    {
44
        if (!is_object($object)) {
45
            throw new \DomainException('Expected object');
46
        }
47
48
        $key = get_class($object);
49
        $this->handlers[$key] = $object;
50
    }
51
52
    /**
53
     * @param MapperInterface $mapper
54
     */
55
    public function addMapper(MapperInterface $mapper)
56
    {
57
        $this->mappers[] = $mapper;
58
    }
59
60
    /**
61
     * @param CallSpecifier $specifier
62
     * @return ResultSpecifier
63
     */
64
    public function process(CallSpecifier $specifier): ResultSpecifier
65
    {
66
        if (count($this->mappers) === 0) {
67
            throw new \LogicException('Mappers not found');
68
        }
69
70
        $resultUnits = [];
71
        $callUnits   = $specifier->getUnits();
72
73
        foreach ($callUnits as $unit) {
74
            if ($unit instanceof CallUnit) {
75
                $resultUnits[] = $this->handleCallUnit($unit);
76
            } elseif ($unit instanceof CallNotification) {
77
                $resultUnits[] = $this->handleNotificationUnit($unit);
78
            } else {
79
                $resultUnits[] = $this->handleErrorUnit($unit);
80
            }
81
        }
82
83
        return new ResultSpecifier($resultUnits, $specifier->isSingleCall());
84
    }
85
86
    /**
87
     * @param CallUnit $unit
88
     * @return AbstractResult
89
     */
90
    private function handleCallUnit(CallUnit $unit): AbstractResult
91
    {
92
        try {
93
            list($class, $method) = $this->getClassAndMethod($unit->getRawMethod());
94
            $result = $this->invoke($class, $method, $unit->getRawParams());
95
        } catch (JsonRpcException $exception) {
96
            return new ResultError($exception);
97
        }
98
99
        return new ResultUnit($unit->getRawId(), $result);
100
    }
101
102
    /**
103
     * @param CallNotification $unit
104
     * @return AbstractResult
105
     */
106
    private function handleNotificationUnit(CallNotification $unit): AbstractResult
107
    {
108
        try {
109
            list($class, $method) = $this->getClassAndMethod($unit->getRawMethod());
110
            $this->invoke($class, $method, $unit->getRawParams());
111
        } catch (JsonRpcException $exception) {
112
            return new ResultError($exception);
113
        }
114
115
        return new ResultNotification();
116
    }
117
118
    /**
119
     * @param CallError $unit
120
     * @return AbstractResult
121
     */
122
    private function handleErrorUnit(CallError $unit): AbstractResult
123
    {
124
        return new ResultError($unit->getBaseException());
125
    }
126
127
    /**
128
     * @param string $requestedMethod
129
     * @return array
130
     * @throws MethodNotFoundException
131
     */
132
    private function getClassAndMethod(string $requestedMethod)
133
    {
134
        foreach ($this->mappers as $mapper) {
135
            /** @var MapperInterface $mapper */
136
            list($class, $method)  = $mapper->getClassAndMethod($requestedMethod);
137
138
            if ($class && array_key_exists($class, $this->handlers) && method_exists($this->handlers[$class], $method)) {
139
                return [$class, $method];
140
            }
141
        }
142
143
        throw new MethodNotFoundException();
144
    }
145
146
    /**
147
     * @param string $class
148
     * @param string $method
149
     * @param array $parameters
150
     * @return mixed
151
     * @throws InvalidParamsException
152
     * @throws ServerErrorException
153
     */
154
    private function invoke(string $class, string $method, array $parameters)
155
    {
156
        $handler    = $this->handlers[$class];
157
        $reflection = new \ReflectionMethod($handler, $method);
158
159
        if ($reflection->getNumberOfRequiredParameters() > count($parameters)) {
160
            throw new InvalidParamsException('Expected ' . $reflection->getNumberOfRequiredParameters() . ' parameters');
161
        }
162
163
        $formalParameters = $reflection->getParameters();
164
        $actualParameters = $this->prepareActualParameters($formalParameters, $parameters);
165
166
        try {
167
            $result = $reflection->invokeArgs($handler, $actualParameters);
168
        } catch (\Exception $exception) {
169
            throw new ServerErrorException($exception->getMessage(), $exception->getCode());
170
        }
171
172
        return $result;
173
    }
174
175
    /**
176
     * @param \ReflectionParameter[] $formalParameters Formal parameters
177
     * @param array                  $parameters       Parameters from request (raw)
178
     * @return array
179
     * @throws InvalidParamsException
180
     */
181
    private function prepareActualParameters(array $formalParameters, array $parameters): array
182
    {
183
        $result = [];
184
185
        // Handle named parameters
186
        if ($this->isNamedParameters($parameters)) {
187
188
            foreach ($formalParameters as $formalParameter) {
189
                /** @var \ReflectionParameter $formalParameter */
190
191
                $formalType = (string) $formalParameter->getType();
192
                $name       = $formalParameter->getName();
0 ignored issues
show
Bug introduced by
Consider using $formalParameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
193
194
                if ($formalParameter->isOptional()) {
195
                    if (array_key_exists($name, $parameters)) {
196
                        $result[$name] = $this->matchType($formalType, $parameters[$name]);
197
                    } else {
198
                        continue;
199
                    }
200
                }
201
202
                if (!array_key_exists($name, $parameters)) {
203
                    throw new InvalidParamsException('Named parameter error');
204
                }
205
206
                $result[$name] = $this->matchType($formalType, $parameters[$name]);
207
            }
208
209
            return $result;
210
        }
211
212
        // Handle positional parameters
213
        for ($position = 0; $position < count($formalParameters); $position++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
214
            /** @var \ReflectionParameter $formalParameter */
215
            $formalParameter = $formalParameters[$position];
216
217
            if ($formalParameter->isOptional() && !isset($parameters[$position])) {
218
                break;
219
            }
220
221
            if (!isset($parameters[$position])) {
222
                throw new InvalidParamsException('Positional parameter error');
223
            }
224
225
            $formalType = (string) $formalParameter->getType();
226
            $result[] = $this->matchType($formalType, $parameters[$position]);
227
        }
228
229
        return $result;
230
    }
231
232
    /**
233
     * @param array $rawParameters
234
     * @return bool
235
     */
236
    private function isNamedParameters(array $rawParameters): bool
237
    {
238
        return array_keys($rawParameters) !== range(0, count($rawParameters) - 1);
239
    }
240
241
    /**
242
     * @param string $formalType
243
     * @param mixed $value
244
     * @return mixed
245
     * @throws InvalidParamsException
246
     */
247
    private function matchType($formalType, $value)
248
    {
249
        // Parameter without type-hinting returns as is
250
        if ($formalType === '') {
251
            return $value;
252
        }
253
254
        if ($this->isType($formalType, $value)) {
255
            return $value;
256
        }
257
258
        throw new InvalidParamsException('Match type failed');
259
    }
260
261
    /**
262
     * @param string $type
263
     * @param $value
264
     * @return bool
265
     * @throws InvalidParamsException
266
     */
267
    private function isType(string $type, $value)
268
    {
269
        switch ($type) {
270
            case 'bool':
271
                return is_bool($value);
272
273
            case 'int':
274
                return is_int($value);
275
276
            case 'float':
277
                return is_float($value) || is_int($value);
278
279
            case 'string':
280
                return is_string($value);
281
282
            case 'array':
283
                return is_array($value);
284
285
            default:
286
                throw new InvalidParamsException('Type match error');
287
        }
288
    }
289
}
290