Passed
Push — master ( f5964a...d290cd )
by Benjamin
02:50
created

AbstractConverter::doAlterEntityWithSetter()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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