Completed
Push — master ( b8726d...b3e88f )
by Benjamin
08:16
created

checkMandatoriesImplementations()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 3
eloc 3
nc 2
nop 0
1
<?php
2
3
namespace Rebolon\Request\ParamConverter;
4
5
use Rebolon\Entity\EntityInterface;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
8
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
9
use Symfony\Component\HttpFoundation\Request;
10
use Symfony\Component\PropertyAccess\PropertyAccess;
11
use Symfony\Component\Validator\ConstraintViolation;
12
use Symfony\Component\Validator\ConstraintViolationList;
13
use Symfony\Component\Validator\Validator\ValidatorInterface;
14
use Symfony\Component\Serializer\SerializerInterface;
15
use \RuntimeException;
16
17
/**
18
 * @todo there is maybe a way to mutualize the 3 methods buildWith*
19
 *
20
 * Class AbstractConverter
21
 * @package Rebolon\Request\ParamConverter\Library
22
 */
23
abstract class AbstractConverter implements ConverterInterface
24
{
25
    /**
26
     * must be overload by child class
27
     */
28
    const NAME = null;
29
30
    /**
31
     * must be overload by child class
32
     */
33
    const RELATED_ENTITY = null;
34
35
    /**
36
     * @var array
37
     */
38
    protected static $registry = [];
39
40
    /**
41
     * @var \Symfony\Component\PropertyAccess\PropertyAccessor
42
     */
43
    protected $accessor;
44
45
    /**
46
     * @var ValidatorInterface
47
     */
48
    protected $validator;
49
50
    /**
51
     * @var SerializerInterface
52
     */
53
    protected $serializer;
54
55
    /**
56
     * @var EntityManager
0 ignored issues
show
Bug introduced by
The type Rebolon\Request\ParamConverter\EntityManager was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
57
     */
58
    protected $entityManager;
59
60
    /**
61
     * @var array
62
     */
63
    protected static $propertyPath = [];
64
65
    /**
66
     * Default name of the id property
67
     * @var string
68
     */
69
    protected $idProperty = 'id';
70
71
    /**
72
     * AbstractConverter constructor.
73
     * @param ValidatorInterface $validator
74
     * @param SerializerInterface $serializer
75
     * @param EntityManagerInterface $entityManager
76
     */
77
    public function __construct(ValidatorInterface $validator, SerializerInterface $serializer, EntityManagerInterface $entityManager)
78
    {
79
        $this->accessor = PropertyAccess::createPropertyAccessor();
80
        $this->validator = $validator;
81
        $this->serializer = $serializer;
82
        $this->entityManager = $entityManager;
0 ignored issues
show
Documentation Bug introduced by
It seems like $entityManager of type Doctrine\ORM\EntityManagerInterface is incompatible with the declared type Rebolon\Request\ParamConverter\EntityManager of property $entityManager.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
83
    }
84
85
    /**
86
     * @throws RuntimeException
87
     */
88
    final protected function checkMandatoriesImplementations(): void {
89
        if (is_null(static::RELATED_ENTITY)
0 ignored issues
show
introduced by
The condition is_null(static::RELATED_ENTITY) is always true.
Loading history...
90
            || !is_null(static::NAME)) {
91
            throw new RuntimeException('ParamConverter must overload following constants: RELATED_ENTITY & NAME');
92
        }
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98
    public function supports(ParamConverter $configuration)
99
    {
100
        return $configuration->getName() === static::NAME;
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function apply(Request $request, ParamConverter $configuration)
107
    {
108
        $content = $request->getContent();
109
110
        if (!$content) {
111
            return false;
112
        }
113
114
        $raw = json_decode($content, true);
115
116
        if (json_last_error()) {
117
            $violationList = new ConstraintViolationList();
118
            $violation = new ConstraintViolation(sprintf('JSON Error %s', json_last_error_msg()), null, [], null, $this->getPropertyPath(), null);
119
            $violationList->add($violation);
120
            throw new ValidationException(
121
                $violationList,
122
                sprintf('Wrong parameter to create new %s (generic)', static::RELATED_ENTITY),
123
                420
124
            );
125
        }
126
127
        if ($raw
128
            && array_key_exists(static::NAME, $raw)) {
129
            $request->attributes->set($configuration->getName(), $this->initFromRequest($raw[static::NAME], static::NAME));
0 ignored issues
show
Deprecated Code introduced by
The function Rebolon\Request\ParamCon...rter::initFromRequest() has been deprecated: Use ItemAbstractConverter::initFromRequest or ListAbstractConverter::initFromRequest ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

129
            $request->attributes->set($configuration->getName(), /** @scrutinizer ignore-deprecated */ $this->initFromRequest($raw[static::NAME], static::NAME));

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
130
        }
131
132
        return true;
133
    }
134
135
    /**
136
     * @inheritdoc
137
     */
138
    public function getIdProperty(): string {
139
        return $this->idProperty;
140
    }
141
142
    /**
143
     * @deprecated Use ItemAbstractConverter::initFromRequest or ListAbstractConverter::initFromRequest
144
     */
145
    public function initFromRequest($jsonOrArray, $propertyPath)
146
    {
147
        $this->checkMandatoriesImplementations();
148
149
        try {
150
            self::$propertyPath[] = $propertyPath;
151
152
            $json = $this->checkJsonOrArray($jsonOrArray);
153
154
            $idPropertyIsInJson = false;
155
            if (!is_array($json)
156
                || ($idPropertyIsInJson = array_key_exists($this->getIdProperty(), $json))
157
            ) {
158
                /**
159
                 * We don't care of other properties. We don't accept update on sub-entity, we can create or re-use
160
                 * So here we just clean json and replace it with the id content
161
                 */
162
                if ($idPropertyIsInJson) {
163
                    $json = $json[$this->getIdProperty()];
164
                }
165
166
                array_pop(self::$propertyPath);
167
168
                return $this->getFromDatabase($json);
169
            }
170
171
            $entity = $this->buildEntity($json);
172
173
            array_pop(self::$propertyPath);
174
175
            return $entity;
176
        } catch (ValidationException $e) {
177
            throw $e;
178
        } catch (\Exception $e) {
179
            $violationList = new ConstraintViolationList();
180
            $violation = new ConstraintViolation($e->getMessage(), null, [], null, $this->getPropertyPath(), null);
181
            $violationList->add($violation);
182
            throw new ValidationException(
183
                $violationList,
184
                sprintf('Wrong parameter to create new %s (generic)', static::RELATED_ENTITY),
185
                420,
186
                $e
187
            );
188
        }
189
    }
190
191
192
    /**
193
     * @param $json
194
     * @return mixed
195
     * @throws RuntimeException
196
     * @throws \TypeError
197
     */
198
    protected function buildEntity($json)
199
    {
200
        $className = static::RELATED_ENTITY;
201
        $entity = new $className();
202
203
        $this->buildWithEzProps($json, $entity);
204
        $this->buildWithManyRelProps($json, $entity);
205
        $this->buildWithOneRelProps($json, $entity);
206
207
        $errors = $this->validator->validate($entity);
208
        if (count($errors)) {
209
            throw new ValidationException($errors);
210
        }
211
212
        return $entity;
213
    }
214
215
    /**
216
     * Used for simple property that is not linked to other entities with relation like ManyTo OneTo...
217
     *
218
     * @param array $json
219
     * @param EntityInterface $entity
220
     * @return EntityInterface
221
     *
222
     * @throws \TypeError
223
     */
224
    private function buildWithEzProps(array $json, EntityInterface $entity): EntityInterface
225
    {
226
        $ezProps = $this->getEzPropsName();
227
        foreach ($ezProps as $prop) {
228
            if (!array_key_exists($prop, $json)
229
                || $json[$prop] === null) {
230
                continue;
231
            }
232
233
            $this->accessor->setValue($entity, $prop, $json[$prop]);
234
        }
235
236
        return $entity;
237
    }
238
239
    /**
240
     * @param array $json
241
     * @param EntityInterface $entity
242
     * @return EntityInterface
243
     * @throws RuntimeException
244
     */
245
    private function buildWithManyRelProps(array $json, EntityInterface $entity): EntityInterface
246
    {
247
        $relManyProps = $this->getManyRelPropsName();
248
        foreach ($relManyProps as $prop => $operationsInfo) {
249
            if (!array_key_exists($prop, $json)
250
                || $json[$prop] === null) {
251
                continue;
252
            }
253
254
            $this->checkOperationsInfo($operationsInfo, 'getManyRelPropsName');
255
256
            $relations = $operationsInfo['converter']->initFromRequest($json[$prop], $prop);
257
258
            // I don't fond a quick way to use the propertyAccessor so i keep this for instance
259
            $methodName = array_key_exists('setter', $operationsInfo) ? $operationsInfo['setter'] : null;
260
            foreach ($relations as $relation) {
261
                if (array_key_exists('cb', $operationsInfo)) {
262
                    if (!is_callable($operationsInfo['cb'])) {
263
                        throw new RuntimeException('cb in operations info must be callable');
264
                    }
265
266
                    $operationsInfo['cb']($relation, $entity);
267
                }
268
269
                if ($methodName) {
270
                    $entity->$methodName($relation);
271
                } else {
272
                    try {
273
                        $this->accessor->setValue($entity, $prop, $relation);
274
                    } catch (\TypeError $e) {
275
                        // @todo manage this with a log + a report to user with explanation on what have not been processed
276
                    }
277
                }
278
            }
279
        }
280
281
        return $entity;
282
    }
283
284
    /**
285
     * @todo: if json is an object : creation, if it's a string : retreive the entity with doctrine and add it to entity
286
     *
287
     * @param array $json
288
     * @param EntityInterface $entity
289
     * @return EntityInterface
290
     *
291
     * @throws RuntimeException
292
     */
293
    private function buildWithOneRelProps(array $json, EntityInterface $entity): EntityInterface
294
    {
295
        $ezProps = $this->getOneRelPropsName();
296
        foreach ($ezProps as $prop => $operationsInfo) {
297
            if (!array_key_exists($prop, $json)
298
                || $json[$prop] === null) {
299
                continue;
300
            }
301
302
            $this->checkOperationsInfo($operationsInfo, 'getOneRelPropsName');
303
304
            $relation = $operationsInfo['converter']->initFromRequest($json[$prop], $prop);
305
            $relationRegistered = $this->useRegistry($relation, $operationsInfo);
306
307
            if (array_key_exists('setter', $operationsInfo)) {
308
                $methodName = $operationsInfo['setter'];
309
                $entity->$methodName($relationRegistered);
310
            } else {
311
                try {
312
                    $this->accessor->setValue($entity, $prop, $relationRegistered);
313
                } catch (\TypeError $e) {
314
                    // @todo manage this with a log + a report to user with explanation on what have not been processed
315
                }
316
            }
317
        }
318
319
        return $entity;
320
    }
321
322
    /**
323
     * @param $jsonOrArray
324
     * @return mixed
325
     * @throws ValidationException
326
     */
327
    protected function checkJsonOrArray($jsonOrArray)
328
    {
329
        $json = is_string($jsonOrArray) ? json_decode($jsonOrArray, true) : $jsonOrArray;
330
331
        // test if invalid json, should i use json_last_error ?
332
        if (!$json) {
333
            $violationList = new ConstraintViolationList();
334
            $violation = new ConstraintViolation(
335
                sprintf('jsonOrArray for %s must be string or array', end(array_values(self::$propertyPath))),
336
                null,
337
                [],
338
                $jsonOrArray,
339
                $this->getPropertyPath(),
340
                $jsonOrArray
341
            );
342
            $violationList->add($violation);
343
            throw new ValidationException($violationList);
344
        }
345
346
        return $json;
347
    }
348
349
    /**
350
     * @param $operationsInfo
351
     * @param $methodName
352
     * @throws RuntimeException
353
     */
354
    protected function checkOperationsInfo($operationsInfo, $methodName): void
355
    {
356
        if (!array_key_exists('converter', $operationsInfo)) {
357
            throw new RuntimeException(sprintf(
358
                'Library ParamConverter::%s must return an associative array '
359
                . 'with the key as the Entity props name also used in HTTP Request Json node, and the value must contain '
360
                . 'an array with converter key, and a setter if you don\'t want to use default propertyAccess',
361
                $methodName
362
            ));
363
        }
364
365
        if (!is_object($operationsInfo['converter'])
366
            || !$operationsInfo['converter'] instanceof ConverterInterface) {
367
            throw new RuntimeException('converter should be an object that implements ConverterInterface');
368
        }
369
    }
370
371
    /**
372
     * @param $relation
373
     * @param $operationsInfo
374
     * @return mixed
375
     */
376
    protected function useRegistry($relation, $operationsInfo)
377
    {
378
        if (array_key_exists('registryKey', $operationsInfo)) {
379
            if (!array_key_exists($operationsInfo['registryKey'], self::$registry)) {
380
                self::$registry[$operationsInfo['registryKey']] = [];
381
            }
382
383
            $serialized = $this->serializer->serialize($relation, 'json');
384
            if (array_key_exists($serialized, self::$registry[$operationsInfo['registryKey']])) {
385
                $relation = self::$registry[$operationsInfo['registryKey']][$serialized];
386
            } else {
387
                self::$registry[$operationsInfo['registryKey']][$serialized] = $relation;
388
            }
389
        }
390
391
        return $relation;
392
    }
393
394
    /**
395
     * @param $id
396
     * @param $class
397
     * @return null|object
398
     * @throws InvalidArgumentException
399
     */
400
    protected function getFromDatabase($id, $class = null)
401
    {
402
        if (!$class && static::RELATED_ENTITY) {
403
            $class = static::RELATED_ENTITY;
404
        } else {
405
            throw new \InvalidArgumentException(sprintf('You must define constant RELATED_ENTITY form you ParamConverter %s', static::name));
0 ignored issues
show
Bug introduced by
The constant Rebolon\Request\ParamCon...AbstractConverter::name was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
406
        }
407
408
        $entityExists = $this->entityManager
409
            ->getRepository($class)
410
            ->find($id);
411
412
        if ($entityExists) {
413
            return $entityExists;
414
        }
415
416
        throw new \InvalidArgumentException(sprintf(static::RELATED_ENTITY . ' %d doesn\'t exists', $id));
417
    }
418
419
    /**
420
     * @return string
421
     */
422
    final protected function getPropertyPath(): string
423
    {
424
        $raw = implode('.', self::$propertyPath);
425
426
        return strtr($raw, ['.[' => '[', ]);
427
    }
428
}
429