checkMandatoriesImplementations()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.1406

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
ccs 3
cts 4
cp 0.75
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 0
crap 3.1406
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 EntityManagerInterface
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
     * List of arguments to pass to the entity constructor
73
     * @var array
74
     */
75
    protected $constructorParams = [];
76
77
    /**
78
     * AbstractConverter constructor.
79
     * @param ValidatorInterface $validator
80
     * @param SerializerInterface $serializer
81
     * @param EntityManagerInterface $entityManager
82
     */
83 4
    public function __construct(ValidatorInterface $validator, SerializerInterface $serializer, EntityManagerInterface $entityManager)
84
    {
85 4
        $this->accessor = PropertyAccess::createPropertyAccessor();
86 4
        $this->validator = $validator;
87 4
        $this->serializer = $serializer;
88 4
        $this->entityManager = $entityManager;
89 4
    }
90
91
    /**
92
     * @throws RuntimeException
93
     */
94 4
    final protected function checkMandatoriesImplementations(): void
95
    {
96 4
        if (is_null(static::RELATED_ENTITY)
0 ignored issues
show
introduced by
The condition is_null(static::RELATED_ENTITY) is always true.
Loading history...
97 4
            || is_null(static::NAME)) {
98
            throw new RuntimeException('ParamConverter must overload following constants: RELATED_ENTITY & NAME');
99
        }
100 4
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function supports(ParamConverter $configuration)
106
    {
107
        return $configuration->getName() === static::NAME;
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113
    public function apply(Request $request, ParamConverter $configuration)
114
    {
115
        $content = $request->getContent();
116
117
        if (!$content) {
118
            return false;
119
        }
120
121
        $raw = json_decode($content, true);
122
123
        if (json_last_error()) {
124
            $violationList = new ConstraintViolationList();
125
            $violation = new ConstraintViolation(sprintf('JSON Error %s', json_last_error_msg()), null, [], null, $this->getPropertyPath(), null);
126
            $violationList->add($violation);
127
            throw new ValidationException(
128
                $violationList,
129
                sprintf('Wrong parameter to create new %s (generic)', static::RELATED_ENTITY),
130
                420
131
            );
132
        }
133
134
        if ($raw
135
            && array_key_exists(static::NAME, $raw)) {
136
            $request->attributes->set($configuration->getName(), $this->initFromRequest($raw[static::NAME], static::NAME));
137
        }
138
139
        return true;
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145 4
    public function getIdProperty(): string
146
    {
147 4
        return $this->idProperty;
148
    }
149
150
    /**
151
     * @param $json
152
     * @return mixed
153
     * @throws RuntimeException
154
     * @throws \TypeError
155
     */
156 4
    protected function buildEntity($json)
157
    {
158 4
        $className = static::RELATED_ENTITY;
159 4
        $entity = new $className(...$this->constructorParams);
160
161 4
        $this->buildWithEzProps($json, $entity);
162 4
        $this->buildWithManyRelProps($json, $entity);
163 4
        $this->buildWithOneRelProps($json, $entity);
164
165 3
        $errors = $this->validator->validate($entity);
166 3
        if (count($errors)) {
167
            throw new ValidationException($errors);
168
        }
169
170 3
        return $entity;
171
    }
172
173
    /**
174
     * Used for simple property that is not linked to other entities with relation like ManyTo OneTo...
175
     *
176
     * @param array $json
177
     * @param EntityInterface $entity
178
     * @return EntityInterface
179
     *
180
     * @throws \TypeError
181
     */
182 4
    private function buildWithEzProps(array $json, EntityInterface $entity): EntityInterface
183
    {
184 4
        $ezProps = $this->getEzPropsName();
185 4
        foreach ($ezProps as $prop) {
186 4
            if (!array_key_exists($prop, $json)
187 4
                || $json[$prop] === null) {
188
                continue;
189
            }
190
191 4
            $this->accessor->setValue($entity, $prop, $json[$prop]);
192
        }
193
194 4
        return $entity;
195
    }
196
197
    /**
198
     * @param array $json
199
     * @param EntityInterface $entity
200
     * @return EntityInterface
201
     * @throws RuntimeException
202
     */
203 4
    private function buildWithManyRelProps(array $json, EntityInterface $entity): EntityInterface
204
    {
205 4
        $relManyProps = $this->getManyRelPropsName();
206 4
        foreach ($relManyProps as $prop => $operationsInfo) {
207 4
            if (!array_key_exists($prop, $json)
208 4
                || $json[$prop] === null) {
209 2
                continue;
210
            }
211
212 2
            $this->checkOperationsInfo($operationsInfo, 'getManyRelPropsName');
213
214 2
            $entity = $this->manageRelationsBetweenChildsAndParent($entity, $operationsInfo, $json, $prop);
215
        }
216
217 4
        return $entity;
218
    }
219
220
    /**
221
     * @todo: if json is an object : creation, if it's a string : retreive the entity with doctrine and add it to entity
222
     *
223
     * @param array $json
224
     * @param EntityInterface $entity
225
     * @return EntityInterface
226
     *
227
     * @throws RuntimeException
228
     */
229 4
    private function buildWithOneRelProps(array $json, EntityInterface $entity): EntityInterface
230
    {
231 4
        $ezProps = $this->getOneRelPropsName();
232 4
        foreach ($ezProps as $prop => $operationsInfo) {
233 4
            if (!array_key_exists($prop, $json)
234 4
                || $json[$prop] === null) {
235
                continue;
236
            }
237
238 4
            $this->checkOperationsInfo($operationsInfo, 'getOneRelPropsName');
239
240 4
            $relation = $operationsInfo['converter']->initFromRequest($json[$prop], $prop);
241 3
            $relationRegistered = $this->useRegistry($relation, $operationsInfo);
242
243 3
            $this->doAlterEntityWithSetter($entity, $operationsInfo, $prop, $relationRegistered);
244
        }
245
246 3
        return $entity;
247
    }
248
249
    /**
250
     * @param EntityInterface $entity
251
     * @param $operationsInfo
252
     * @param $prop
253
     * @param $relation
254
     * @return EntityInterface
255
     */
256 3
    private function doAlterEntityWithSetter(EntityInterface $entity, $operationsInfo, $prop, $relation): EntityInterface
257
    {
258 3
        if (array_key_exists('setter', $operationsInfo)) {
259
            // I don't fond a quick way to use the propertyAccessor so i keep this for instance
260 3
            $methodName = $operationsInfo['setter'];
261 3
            $entity->$methodName($relation);
262
        } else {
263
            try {
264 1
                $this->accessor->setValue($entity, $prop, $relation);
265
            } catch (\TypeError $e) {
266
                // @todo manage this with a log + a report to user with explanation on what have not been processed
267
            }
268
        }
269
270 3
        return $entity;
271
    }
272
273
    /**
274
     * @param EntityInterface $entity
275
     * @param $operationsInfo
276
     * @param $json
277
     * @param $prop
278
     * @return EntityInterface
279
     */
280 2
    private function manageRelationsBetweenChildsAndParent(EntityInterface $entity, $operationsInfo, $json, $prop): EntityInterface
281
    {
282 2
        $relations = $operationsInfo['converter']->initFromRequest($json[$prop], $prop);
283
284 1
        foreach ($relations as $relation) {
285 1
            $this->doCallback($entity, $operationsInfo, $relation);
286
287 1
            $this->doAlterEntityWithSetter($entity, $operationsInfo, $prop, $relation);
288
        }
289
290 1
        return $entity;
291
    }
292
293
    /**
294
     * @param EntityInterface $entity
295
     * @param $operationsInfo
296
     * @param $relation
297
     */
298 1
    private function doCallback(EntityInterface $entity, $operationsInfo, $relation): void
299
    {
300 1
        if (!array_key_exists('cb', $operationsInfo)) {
301
            return;
302
        }
303
304 1
        if (!is_callable($operationsInfo['cb'])) {
305
            throw new RuntimeException('cb in operations info must be callable');
306
        }
307
308 1
        $operationsInfo['cb']($relation, $entity);
309 1
    }
310
311
    /**
312
     * @param $jsonOrArray
313
     * @return mixed
314
     * @throws ValidationException
315
     */
316 4
    protected function checkJsonOrArray($jsonOrArray)
317
    {
318 4
        $json = is_string($jsonOrArray) ? json_decode($jsonOrArray, true) : $jsonOrArray;
319
320
        // test if invalid json, should i use json_last_error ?
321 4
        if (is_string($jsonOrArray)
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (is_string($jsonOrArray)...ast_error()) || ! $json, Probably Intended Meaning: is_string($jsonOrArray) ...ast_error() || ! $json)
Loading history...
322 4
            && json_last_error()
323 4
            || !$json) {
324 1
            $violationList = new ConstraintViolationList();
325 1
            $properties = array_values(self::$propertyPath);
326 1
            $violation = new ConstraintViolation(
327 1
                sprintf('jsonOrArray for %s must be string or array', end($properties)),
328 1
                null,
329 1
                [],
330
                $jsonOrArray,
331 1
                $this->getPropertyPath(),
332
                $jsonOrArray
333
            );
334 1
            $violationList->add($violation);
335 1
            throw new ValidationException($violationList);
336
        }
337
338 4
        return $json;
339
    }
340
341
    /**
342
     * @param $operationsInfo
343
     * @param $methodName
344
     * @throws RuntimeException
345
     */
346 4
    protected function checkOperationsInfo($operationsInfo, $methodName): void
347
    {
348 4
        if (!array_key_exists('converter', $operationsInfo)) {
349
            throw new RuntimeException(sprintf(
350
                'Library ParamConverter::%s must return an associative array '
351
                . 'with the key as the Entity props name also used in HTTP Request Json node, and the value must contain '
352
                . 'an array with converter key, and a setter if you don\'t want to use default propertyAccess',
353
                $methodName
354
            ));
355
        }
356
357 4
        if (!is_object($operationsInfo['converter'])
358 4
            || !$operationsInfo['converter'] instanceof ConverterInterface) {
359
            throw new RuntimeException('converter should be an object that implements ConverterInterface');
360
        }
361 4
    }
362
363
    /**
364
     * @param $relation
365
     * @param $operationsInfo
366
     * @return mixed
367
     */
368 3
    protected function useRegistry($relation, $operationsInfo)
369
    {
370 3
        if (array_key_exists('registryKey', $operationsInfo)) {
371 3
            if (!array_key_exists($operationsInfo['registryKey'], self::$registry)) {
372 1
                self::$registry[$operationsInfo['registryKey']] = [];
373
            }
374
375 3
            $serialized = $this->serializer->serialize($relation, 'json');
376 3
            if (array_key_exists($serialized, self::$registry[$operationsInfo['registryKey']])) {
377 2
                $relation = self::$registry[$operationsInfo['registryKey']][$serialized];
378
            } else {
379 2
                self::$registry[$operationsInfo['registryKey']][$serialized] = $relation;
380
            }
381
        }
382
383 3
        return $relation;
384
    }
385
386
    /**
387
     * @param $id
388
     * @param $class
389
     * @return null|object
390
     * @throws InvalidArgumentException
391
     */
392 2
    protected function getFromDatabase($id, $class = null)
393
    {
394 2
        if (!$class && static::RELATED_ENTITY) {
395 2
            $class = static::RELATED_ENTITY;
396
        } else {
397
            throw new \InvalidArgumentException(sprintf('You must define constant RELATED_ENTITY form you ParamConverter %s', static::NAME));
398
        }
399
400 2
        $entityExists = $this->entityManager
401 2
            ->getRepository($class)
402 2
            ->find($id);
403
404 2
        if ($entityExists) {
405 2
            return $entityExists;
406
        }
407
408
        throw new \InvalidArgumentException(sprintf(static::RELATED_ENTITY . ' %d doesn\'t exists', $id));
409
    }
410
411
    /**
412
     * @return string
413
     */
414 1
    final protected function getPropertyPath(): string
415
    {
416 1
        $raw = implode('.', self::$propertyPath);
417
418 1
        return strtr($raw, ['.[' => '[', ]);
419
    }
420
}
421