Passed
Push — master ( 3cea6c...f5964a )
by Benjamin
02:44
created

AbstractConverter::getPropertyPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 2
nc 1
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 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
            $relations = $operationsInfo['converter']->initFromRequest($json[$prop], $prop);
207
208
            // I don't fond a quick way to use the propertyAccessor so i keep this for instance
209
            $methodName = array_key_exists('setter', $operationsInfo) ? $operationsInfo['setter'] : null;
210
            foreach ($relations as $relation) {
211
                if (array_key_exists('cb', $operationsInfo)) {
212
                    if (!is_callable($operationsInfo['cb'])) {
213
                        throw new RuntimeException('cb in operations info must be callable');
214
                    }
215
216
                    $operationsInfo['cb']($relation, $entity);
217
                }
218
219
                if ($methodName) {
220
                    $entity->$methodName($relation);
221
                } else {
222
                    try {
223
                        $this->accessor->setValue($entity, $prop, $relation);
224
                    } catch (\TypeError $e) {
225
                        // @todo manage this with a log + a report to user with explanation on what have not been processed
226
                    }
227
                }
228
            }
229
        }
230
231
        return $entity;
232
    }
233
234
    /**
235
     * @todo: if json is an object : creation, if it's a string : retreive the entity with doctrine and add it to entity
236
     *
237
     * @param array $json
238
     * @param EntityInterface $entity
239
     * @return EntityInterface
240
     *
241
     * @throws RuntimeException
242
     */
243
    private function buildWithOneRelProps(array $json, EntityInterface $entity): EntityInterface
244
    {
245
        $ezProps = $this->getOneRelPropsName();
246
        foreach ($ezProps as $prop => $operationsInfo) {
247
            if (!array_key_exists($prop, $json)
248
                || $json[$prop] === null) {
249
                continue;
250
            }
251
252
            $this->checkOperationsInfo($operationsInfo, 'getOneRelPropsName');
253
254
            $relation = $operationsInfo['converter']->initFromRequest($json[$prop], $prop);
255
            $relationRegistered = $this->useRegistry($relation, $operationsInfo);
256
257
            if (array_key_exists('setter', $operationsInfo)) {
258
                $methodName = $operationsInfo['setter'];
259
                $entity->$methodName($relationRegistered);
260
            } else {
261
                try {
262
                    $this->accessor->setValue($entity, $prop, $relationRegistered);
263
                } catch (\TypeError $e) {
264
                    // @todo manage this with a log + a report to user with explanation on what have not been processed
265
                }
266
            }
267
        }
268
269
        return $entity;
270
    }
271
272
    /**
273
     * @param $jsonOrArray
274
     * @return mixed
275
     * @throws ValidationException
276
     */
277
    protected function checkJsonOrArray($jsonOrArray)
278
    {
279
        $json = is_string($jsonOrArray) ? json_decode($jsonOrArray, true) : $jsonOrArray;
280
281
        // test if invalid json, should i use json_last_error ?
282
        if (!$json) {
283
            $violationList = new ConstraintViolationList();
284
            $violation = new ConstraintViolation(
285
                sprintf('jsonOrArray for %s must be string or array', end(array_values(self::$propertyPath))),
286
                null,
287
                [],
288
                $jsonOrArray,
289
                $this->getPropertyPath(),
290
                $jsonOrArray
291
            );
292
            $violationList->add($violation);
293
            throw new ValidationException($violationList);
294
        }
295
296
        return $json;
297
    }
298
299
    /**
300
     * @param $operationsInfo
301
     * @param $methodName
302
     * @throws RuntimeException
303
     */
304
    protected function checkOperationsInfo($operationsInfo, $methodName): void
305
    {
306
        if (!array_key_exists('converter', $operationsInfo)) {
307
            throw new RuntimeException(sprintf(
308
                'Library ParamConverter::%s must return an associative array '
309
                . 'with the key as the Entity props name also used in HTTP Request Json node, and the value must contain '
310
                . 'an array with converter key, and a setter if you don\'t want to use default propertyAccess',
311
                $methodName
312
            ));
313
        }
314
315
        if (!is_object($operationsInfo['converter'])
316
            || !$operationsInfo['converter'] instanceof ConverterInterface) {
317
            throw new RuntimeException('converter should be an object that implements ConverterInterface');
318
        }
319
    }
320
321
    /**
322
     * @param $relation
323
     * @param $operationsInfo
324
     * @return mixed
325
     */
326
    protected function useRegistry($relation, $operationsInfo)
327
    {
328
        if (array_key_exists('registryKey', $operationsInfo)) {
329
            if (!array_key_exists($operationsInfo['registryKey'], self::$registry)) {
330
                self::$registry[$operationsInfo['registryKey']] = [];
331
            }
332
333
            $serialized = $this->serializer->serialize($relation, 'json');
334
            if (array_key_exists($serialized, self::$registry[$operationsInfo['registryKey']])) {
335
                $relation = self::$registry[$operationsInfo['registryKey']][$serialized];
336
            } else {
337
                self::$registry[$operationsInfo['registryKey']][$serialized] = $relation;
338
            }
339
        }
340
341
        return $relation;
342
    }
343
344
    /**
345
     * @param $id
346
     * @param $class
347
     * @return null|object
348
     * @throws InvalidArgumentException
349
     */
350
    protected function getFromDatabase($id, $class = null)
351
    {
352
        if (!$class && static::RELATED_ENTITY) {
353
            $class = static::RELATED_ENTITY;
354
        } else {
355
            throw new \InvalidArgumentException(sprintf('You must define constant RELATED_ENTITY form you ParamConverter %s', static::NAME));
356
        }
357
358
        $entityExists = $this->entityManager
359
            ->getRepository($class)
360
            ->find($id);
361
362
        if ($entityExists) {
363
            return $entityExists;
364
        }
365
366
        throw new \InvalidArgumentException(sprintf(static::RELATED_ENTITY . ' %d doesn\'t exists', $id));
367
    }
368
369
    /**
370
     * @return string
371
     */
372
    final protected function getPropertyPath(): string
373
    {
374
        $raw = implode('.', self::$propertyPath);
375
376
        return strtr($raw, ['.[' => '[', ]);
377
    }
378
}
379