Completed
Push — master ( 0d4fd4...557d64 )
by Denis
02:02
created

Invoker::toObject()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 2
1
<?php
2
3
namespace PhpJsonRpc\Server;
4
5
use PhpJsonRpc\Common\TypeAdapter\TypeAdapter;
6
use PhpJsonRpc\Common\TypeAdapter\TypeCastException;
7
use PhpJsonRpc\Error\InvalidParamsException;
8
use PhpJsonRpc\Error\ServerErrorException;
9
10
/**
11
 * Invoker of methods
12
 */
13
class Invoker
14
{
15
    /**
16
     * @var TypeAdapter
17
     */
18
    private $typeAdapter;
19
20
    /**
21
     * Invoker constructor.
22
     */
23
    public function __construct()
24
    {
25
        $this->typeAdapter = new TypeAdapter();
26
    }
27
28
    /**
29
     * @param mixed  $object
30
     * @param string $method
31
     * @param array  $parameters
32
     *
33
     * @return mixed
34
     */
35
    public function invoke($object, string $method, array $parameters)
36
    {
37
        $handler    = $object;
38
        $reflection = new \ReflectionMethod($handler, $method);
39
40
        if ($reflection->getNumberOfRequiredParameters() > count($parameters)) {
41
            throw new InvalidParamsException('Expected ' . $reflection->getNumberOfRequiredParameters() . ' parameters');
42
        }
43
44
        $formalParameters = $reflection->getParameters();
45
        $actualParameters = $this->prepareActualParameters($formalParameters, $parameters);
46
47
        try {
48
            $result = $reflection->invokeArgs($handler, $actualParameters);
49
        } catch (\Exception $exception) {
50
            throw new ServerErrorException($exception->getMessage(), $exception->getCode());
51
        }
52
53
        return $this->castResult($result);
54
    }
55
56
    /**
57
     * @return TypeAdapter
58
     */
59
    public function getTypeAdapter(): TypeAdapter
60
    {
61
        return $this->typeAdapter;
62
    }
63
64
    /**
65
     * @param \ReflectionParameter[] $formalParameters Formal parameters
66
     * @param array                  $parameters       Parameters from request (raw)
67
     *
68
     * @return array
69
     */
70
    private function prepareActualParameters(array $formalParameters, array $parameters): array
71
    {
72
        $result = [];
73
74
        // Handle named parameters
75
        if ($this->isNamedArray($parameters)) {
76
77
            foreach ($formalParameters as $formalParameter) {
78
                /** @var \ReflectionParameter $formalParameter */
79
80
                $formalType = (string) $formalParameter->getType();
81
                $name       = $formalParameter->name;
82
83
                if ($formalParameter->isOptional()) {
84
                    if (!array_key_exists($name, $parameters)) {
85
                        continue;
86
                    }
87
88
                    $result[$name] = $formalParameter->getClass() !== null
89
                        ? $this->toObject($formalParameter->getClass()->name, $parameters[$name])
90
                        : $this->matchType($formalType, $parameters[$name]);
91
                }
92
93
                if (!array_key_exists($name, $parameters)) {
94
                    throw new InvalidParamsException('Named parameter error');
95
                }
96
97
                $result[$name] = $this->matchType($formalType, $parameters[$name]);
98
            }
99
100
            return $result;
101
        }
102
103
        // Handle positional parameters
104
        foreach ($formalParameters as $position => $formalParameter) {
105
            /** @var \ReflectionParameter $formalParameter */
106
107
            if ($formalParameter->isOptional() && !isset($parameters[$position])) {
108
                break;
109
            }
110
111
            if (!isset($parameters[$position])) {
112
                throw new InvalidParamsException('Positional parameter error');
113
            }
114
115
            $formalType = (string) $formalParameter->getType();
116
            $result[] = $formalParameter->getClass() !== null
117
                ? $this->toObject($formalParameter->getClass()->name, $parameters[$position])
118
                : $this->matchType($formalType, $parameters[$position]);
119
        }
120
121
        return $result;
122
    }
123
124
    /**
125
     * @param array $rawParameters
126
     *
127
     * @return bool
128
     */
129
    private function isNamedArray(array $rawParameters): bool
130
    {
131
        return array_keys($rawParameters) !== range(0, count($rawParameters) - 1);
132
    }
133
134
    /**
135
     * @param string $formalType
136
     * @param mixed  $value
137
     *
138
     * @return mixed
139
     */
140
    private function matchType(string $formalType, $value)
141
    {
142
        // Parameter without type-hinting returns as is
143
        if ($formalType === '') {
144
            return $value;
145
        }
146
147
        if ($this->isType($formalType, $value)) {
148
            return $value;
149
        }
150
151
        throw new InvalidParamsException('Type hint failed');
152
    }
153
154
    /**
155
     * @param string $formalClass
156
     * @param mixed  $value
157
     *
158
     * @return mixed
159
     */
160
    private function toObject(string $formalClass, $value)
161
    {
162
        if ($this->typeAdapter->isClassRegistered($formalClass)) {
163
            try {
164
                return $this->typeAdapter->toObject($value);
165
            } catch (TypeCastException $exception) {
166
                throw new InvalidParamsException('Class cast failed');
167
            }
168
        }
169
170
        throw new InvalidParamsException('Class hint failed');
171
    }
172
173
    /**
174
     * @param string $type
175
     * @param $value
176
     *
177
     * @return bool
178
     */
179
    private function isType(string $type, $value)
180
    {
181
        switch ($type) {
182
            case 'bool':
183
                return is_bool($value);
184
185
            case 'int':
186
                return is_int($value);
187
188
            case 'float':
189
                return is_float($value) || is_int($value);
190
191
            case 'string':
192
                return is_string($value);
193
194
            case 'array':
195
                return is_array($value);
196
197
            default:
198
                throw new InvalidParamsException('Type match error');
199
        }
200
    }
201
202
    /**
203
     * @param $result
204
     *
205
     * @return mixed
206
     */
207
    private function castResult($result)
208
    {
209
        try {
210
            $result = $this->typeAdapter->toArray($result);
211
        } catch (TypeCastException $exception) {
212
            return $result;
213
        }
214
215
        return $result;
216
    }
217
}
218