• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tochka-developers / jsonrpc / 4135501466

pending completion
4135501466

push

github

darkdarin
Merge remote-tracking branch 'origin/v5.0'

209 of 813 new or added lines in 51 files covered. (25.71%)

233 of 1307 relevant lines covered (17.83%)

1.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Support/DefaultHandleResolver.php
1
<?php
2

3
namespace Tochka\JsonRpc\Support;
4

5
use Illuminate\Container\Container;
6
use Illuminate\Contracts\Container\BindingResolutionException;
7
use Tochka\JsonRpc\Annotations\ValidationRule;
8
use Tochka\JsonRpc\Contracts\CasterRegistryInterface;
9
use Tochka\JsonRpc\Contracts\HandleResolverInterface;
10
use Tochka\JsonRpc\Contracts\ValidatorInterface;
11
use Tochka\JsonRpc\Contracts\ParamsResolverInterface;
12
use Tochka\JsonRpc\DTO\JsonRpcRoute;
13
use Tochka\JsonRpc\DTO\JsonRpcServerRequest;
14
use Tochka\JsonRpc\Exceptions\InvalidTypeException;
15
use Tochka\JsonRpc\Exceptions\NotNullableValueException;
16
use Tochka\JsonRpc\Exceptions\ParameterRequiredException;
17
use Tochka\JsonRpc\Route\Parameters\Parameter;
18
use Tochka\JsonRpc\Route\Parameters\ParameterObject;
19
use Tochka\JsonRpc\Route\Parameters\ParameterTypeEnum;
20
use Tochka\JsonRpc\Standard\DTO\JsonRpcRequest;
21
use Tochka\JsonRpc\Standard\Exceptions\Additional\InvalidParameterException;
22
use Tochka\JsonRpc\Standard\Exceptions\Additional\InvalidParametersException;
23
use Tochka\JsonRpc\Standard\Exceptions\Errors\InvalidParameterError;
24
use Tochka\JsonRpc\Standard\Exceptions\InternalErrorException;
25
use Tochka\JsonRpc\Standard\Exceptions\JsonRpcException;
26
use Tochka\JsonRpc\Standard\Exceptions\MethodNotFoundException;
27

28
class DefaultHandleResolver implements HandleResolverInterface
29
{
30
    private Container $container;
31
    private ParamsResolverInterface $paramsResolver;
32
    private CasterRegistryInterface $casterRegistry;
33
    private ValidatorInterface $validator;
34

35
    /**
36
     * @psalm-suppress PossiblyUnusedMethod
37
     */
38
    public function __construct(
39
        ParamsResolverInterface $paramsResolver,
40
        CasterRegistryInterface $casterRegistry,
41
        ValidatorInterface $validator,
42
        Container $container
43
    ) {
NEW
44
        $this->container = $container;
×
NEW
45
        $this->paramsResolver = $paramsResolver;
×
NEW
46
        $this->casterRegistry = $casterRegistry;
×
NEW
47
        $this->validator = $validator;
×
48
    }
49

50
    /**
51
     * @throws BindingResolutionException
52
     */
53
    public function handle(JsonRpcServerRequest $request): mixed
54
    {
NEW
55
        $controllerInstance = $this->initializeController($request);
×
NEW
56
        $route = $request->getRoute();
×
NEW
57
        if ($route === null || $route->controllerMethod === null || !method_exists($controllerInstance, $route->controllerMethod)) {
×
NEW
58
            throw new MethodNotFoundException();
×
59
        }
60

NEW
61
        $parameters = $this->mapParameters($request->getJsonRpcRequest()->params, $route->parameters);
×
62

NEW
63
        return $controllerInstance->{$route->controllerMethod}(...$parameters);
×
64
    }
65

66
    /**
67
     * @param array|object|null $rawInputParameters
68
     * @param array<string, Parameter> $methodParameters
69
     * @return array
70
     */
71
    private function mapParameters(array|object|null $rawInputParameters, array $methodParameters): array
72
    {
NEW
73
        if ($rawInputParameters === null) {
×
NEW
74
            return [];
×
75
        }
76

NEW
77
        $parameters = [];
×
78

79
        /** @var array<int, array<int, InvalidParameterError>> $errors */
NEW
80
        $errors = [];
×
81

82
        // если входные параметры переданы массивом - преобразуем в объект с именованными параметрами
NEW
83
        if (is_array($rawInputParameters)) {
×
NEW
84
            $namedParameters = (object)[];
×
NEW
85
            $i = 0;
×
NEW
86
            foreach ($methodParameters as $parameter) {
×
NEW
87
                if (isset($rawInputParameters[$i])) {
×
NEW
88
                    $parameterName = $parameter->name;
×
NEW
89
                    $namedParameters->$parameterName = $rawInputParameters[$i];
×
90
                }
NEW
91
                $i++;
×
92
            }
93

NEW
94
            $rawInputParameters = $namedParameters;
×
95
        }
96

NEW
97
        $rules = [];
×
NEW
98
        foreach ($methodParameters as $parameterName => $parameter) {
×
99
            try {
NEW
100
                if ($parameter->castFullRequest && $parameter->className !== null) {  // если необходимо весь запрос скастовать в объект
×
NEW
101
                    $parameterObject = $this->paramsResolver->getParameterObject($parameter->className);
×
NEW
102
                    $parameters[] = $this->castObject($rawInputParameters, $parameter, $parameterObject, $parameterName);
×
NEW
103
                } elseif ($parameter->castFromDI && $parameter->className !== null) { // если необходимо создать объект из DI
×
104
                    /** @psalm-suppress MixedAssignment */
NEW
105
                    $parameters[] = $this->container->make($parameter->className);
×
106
                } else { // стандартный каст в параметры
NEW
107
                    $rules[$parameterName] = $this->getValidationRules($parameter);
×
108
                    /** @psalm-suppress MixedAssignment */
NEW
109
                    $parameters[] = $this->castParameter((array)$rawInputParameters, $parameter, $parameterName);
×
110
                }
NEW
111
            } catch (InvalidParameterException $e) {
×
NEW
112
                $errors[] = [$e->getParameterError()];
×
NEW
113
            } catch (InvalidParametersException $e) {
×
NEW
114
                $errors[] = $e->getParametersError()->getParameterErrors();
×
NEW
115
            } catch (JsonRpcException $e) {
×
NEW
116
                throw $e;
×
NEW
117
            } catch (\Throwable $e) {
×
NEW
118
                throw InternalErrorException::from($e);
×
119
            }
120
        }
121

NEW
122
        $validatorErrors = $this->validator->validateAndGetErrors((array)$rawInputParameters, $rules);
×
NEW
123
        if (!empty($validatorErrors)) {
×
NEW
124
            $errors[] = $validatorErrors;
×
125
        }
126

NEW
127
        if (!empty($errors)) {
×
128
            /** @var array<int, InvalidParameterError> $combinerErrors */
NEW
129
            $combinerErrors = array_merge(...$errors);
×
NEW
130
            throw InvalidParametersException::from($combinerErrors);
×
131
        }
132

NEW
133
        return $parameters;
×
134
    }
135

136
    /**
137
     * @throws \ReflectionException
138
     */
139
    private function castParameter(array $rawInputParameters, Parameter $parameter, string $fullFieldName): mixed
140
    {
NEW
141
        if (!array_key_exists($parameter->name, $rawInputParameters)) {
×
NEW
142
            if ($parameter->required) {
×
NEW
143
                throw new ParameterRequiredException($fullFieldName);
×
144
            }
145

NEW
146
            return $parameter->defaultValue;
×
147
        }
148

NEW
149
        return $this->castValue($rawInputParameters[$parameter->name], $parameter, $fullFieldName);
×
150
    }
151

152
    /**
153
     * @throws \ReflectionException
154
     */
155
    private function castValue(mixed $value, Parameter $parameter, string $fullFieldName): mixed
156
    {
NEW
157
        if ($value === null) {
×
NEW
158
            if (!$parameter->nullable) {
×
NEW
159
                throw new NotNullableValueException($fullFieldName);
×
160
            }
161

NEW
162
            return null;
×
163
        }
164

NEW
165
        $varType = gettype($value);
×
NEW
166
        $type = ParameterTypeEnum::fromVarType($varType);
×
167

NEW
168
        if ($parameter->className !== null && $parameter->type->is(ParameterTypeEnum::TYPE_OBJECT())) {
×
NEW
169
            $parameterObject = $this->paramsResolver->getParameterObject($parameter->className);
×
NEW
170
            $object = $this->castObject($value, $parameter, $parameterObject, $fullFieldName);
×
NEW
171
            if (!$parameter->nullable && $object === null) {
×
NEW
172
                throw new NotNullableValueException($fullFieldName);
×
173
            }
NEW
174
            return $object;
×
175
        }
176

NEW
177
        if ($type !== $parameter->type && $parameter->type->isNot(ParameterTypeEnum::TYPE_MIXED())) {
×
NEW
178
            throw new InvalidTypeException($fullFieldName, $type->getValue(), $parameter->type->getValue());
×
179
        }
180

NEW
181
        if ($parameter->parametersInArray !== null && $parameter->type->is(ParameterTypeEnum::TYPE_ARRAY()) && is_array($value)) {
×
NEW
182
            $resultArray = [];
×
NEW
183
            $i = 0;
×
184

185
            /** @var array<int, array<int, InvalidParameterError>> $errors */
NEW
186
            $errors = [];
×
187

188
            /** @psalm-suppress MixedAssignment */
NEW
189
            foreach ($value as $inputArrayItem) {
×
190
                try {
191
                    /** @psalm-suppress MixedAssignment */
NEW
192
                    $resultArray[] = $this->castValue(
×
NEW
193
                        $inputArrayItem,
×
NEW
194
                        $parameter->parametersInArray,
×
NEW
195
                        $fullFieldName . '[' . $i . ']'
×
NEW
196
                    );
×
NEW
197
                } catch (InvalidParameterException $e) {
×
NEW
198
                    $errors[] = [$e->getParameterError()];
×
NEW
199
                } catch (InvalidParametersException $e) {
×
NEW
200
                    $errors[] = $e->getParametersError()->getParameterErrors();
×
NEW
201
                } catch (JsonRpcException $e) {
×
NEW
202
                    throw $e;
×
NEW
203
                } catch (\Throwable $e) {
×
NEW
204
                    throw InternalErrorException::from($e);
×
205
                }
NEW
206
                $i++;
×
207
            }
208

NEW
209
            if (!empty($errors)) {
×
210
                /** @var array<int, InvalidParameterError> $combinerErrors */
NEW
211
                $combinerErrors = array_merge(...$errors);
×
NEW
212
                throw InvalidParametersException::from($combinerErrors);
×
213
            }
214

NEW
215
            return $resultArray;
×
216
        }
217

NEW
218
        return $value;
×
219
    }
220

221
    /**
222
     * @throws \ReflectionException
223
     */
224
    private function castObject(
225
        mixed $value,
226
        Parameter $parameter,
227
        ?ParameterObject $parameterObject,
228
        string $fullFieldName
229
    ): ?object {
NEW
230
        if ($parameterObject === null) {
×
NEW
231
            throw new JsonRpcException(JsonRpcException::CODE_INTERNAL_ERROR);
×
232
        }
233

NEW
234
        if ($parameterObject->customCastByCaster !== null) {
×
NEW
235
            return $this->casterRegistry->cast(
×
NEW
236
                $parameterObject->customCastByCaster,
×
NEW
237
                $parameter,
×
NEW
238
                $value,
×
NEW
239
                $fullFieldName
×
NEW
240
            );
×
241
        }
242

243
        // Создаем экземпляр класса без участия конструктора, так как все равно не можем правильно просадить параметры
244
        // в конструктор. А так будет возможность в DTO юзать кастомные конструкторы, при этом JsonRpc сможет все равно
245
        // в него кастить
NEW
246
        $reflectionClass = new \ReflectionClass($parameterObject->className);
×
NEW
247
        $instance = $reflectionClass->newInstanceWithoutConstructor();
×
248

NEW
249
        if ($parameterObject->properties === null) {
×
NEW
250
            return $instance;
×
251
        }
252

NEW
253
        $propertyValues = (array)$value;
×
254

255
        /** @var array<int, array<int, InvalidParameterError>> $errors */
NEW
256
        $errors = [];
×
257

NEW
258
        foreach ($parameterObject->properties as $property) {
×
259
            try {
NEW
260
                $propertyName = $property->name;
×
NEW
261
                if (!$property->required && !array_key_exists($propertyName, $propertyValues)) {
×
NEW
262
                    continue;
×
263
                }
264

NEW
265
                $instance->$propertyName = $this->castParameter(
×
NEW
266
                    $propertyValues,
×
NEW
267
                    $property,
×
NEW
268
                    $fullFieldName . '.' . $propertyName
×
NEW
269
                );
×
NEW
270
            } catch (InvalidParameterException $e) {
×
NEW
271
                $errors[] = [$e->getParameterError()];
×
NEW
272
            } catch (InvalidParametersException $e) {
×
NEW
273
                $errors[] = $e->getParametersError()->getParameterErrors();
×
NEW
274
            } catch (JsonRpcException $e) {
×
NEW
275
                throw $e;
×
NEW
276
            } catch (\Throwable $e) {
×
NEW
277
                throw InternalErrorException::from($e);
×
278
            }
279
        }
280

NEW
281
        if (!empty($errors)) {
×
282
            /** @var array<int, InvalidParameterError> $combinerErrors */
NEW
283
            $combinerErrors = array_merge(...$errors);
×
NEW
284
            throw InvalidParametersException::from($combinerErrors);
×
285
        }
286

NEW
287
        return $instance;
×
288
    }
289

290
    /**
291
     * @throws BindingResolutionException
292
     */
293
    private function initializeController(JsonRpcServerRequest $request): object
294
    {
NEW
295
        $route = $request->getRoute();
×
296

297
        // если нет такого контроллера или метода
298
        /** @psalm-suppress DocblockTypeContradiction */
NEW
299
        if ($route === null || $route->controllerClass === null || !class_exists($route->controllerClass)) {
×
NEW
300
            throw new MethodNotFoundException();
×
301
        }
302

NEW
303
        $this->container->when([$route->controllerClass])
×
NEW
304
            ->needs(JsonRpcServerRequest::class)
×
NEW
305
            ->give(fn () => $request);
×
306

NEW
307
        $this->container->when([$route->controllerClass])
×
NEW
308
            ->needs(JsonRpcRequest::class)
×
NEW
309
            ->give(fn () => $request->getJsonRpcRequest());
×
310

NEW
311
        $this->container->when([$route->controllerClass])
×
NEW
312
            ->needs(JsonRpcRoute::class)
×
NEW
313
            ->give(fn () => $request->getRoute());
×
314

315
        /** @var object $controller */
NEW
316
        $controller = $this->container->make($route->controllerClass);
×
317

NEW
318
        if (!is_callable([$controller, $route->controllerMethod])) {
×
NEW
319
            throw new MethodNotFoundException();
×
320
        }
321

NEW
322
        return $controller;
×
323
    }
324

325
    private function getValidationRules(Parameter $parameter): array
326
    {
NEW
327
        $rules = [];
×
NEW
328
        foreach ($parameter->annotations as $annotation) {
×
NEW
329
            if ($annotation instanceof ValidationRule) {
×
NEW
330
                $rules[] = explode('|', $annotation->rule);
×
331
            }
332
        }
333

NEW
334
        return array_merge(...$rules);
×
335
    }
336
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc