GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 69b831...bd71a5 )
by joseph
20:49 queued 13s
created

EntityFactory::updateDto()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1.0787

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 7
ccs 4
cts 7
cp 0.5714
crap 1.0787
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Factory;
4
5
use Doctrine\Common\Collections\Collection;
6
use Doctrine\Common\NotifyPropertyChanged;
7
use Doctrine\ORM\EntityManagerInterface;
8
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
9
use EdmondsCommerce\DoctrineStaticMeta\DoctrineStaticMeta;
10
use EdmondsCommerce\DoctrineStaticMeta\Entity\DataTransferObjects\DtoFactory;
11
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Interfaces\PrimaryKey\IdFieldInterface;
12
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\AlwaysValidInterface;
13
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\DataTransferObjectInterface;
14
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
15
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\UsesPHPMetaDataInterface;
16
use EdmondsCommerce\DoctrineStaticMeta\Exception\MultipleValidationException;
17
use EdmondsCommerce\DoctrineStaticMeta\Exception\ValidationException;
18
use InvalidArgumentException;
19
use LogicException;
20
use RuntimeException;
21
use ts\Reflection\ReflectionClass;
22
use TypeError;
23
use function get_class;
24
use function is_object;
25
use function print_r;
26
use function spl_object_hash;
27
28
/**
29
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
30
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
31
 */
32
class EntityFactory implements EntityFactoryInterface
33
{
34
    /**
35
     * This array is used to track Entities that in the process of being created as part of a transaction
36
     *
37
     * @var EntityInterface[][]
38
     */
39
    private static $created = [];
40
    /**
41
     * @var NamespaceHelper
42
     */
43
    protected $namespaceHelper;
44
    /**
45
     * @var EntityDependencyInjector
46
     */
47
    protected $entityDependencyInjector;
48
    /**
49
     * @var EntityManagerInterface
50
     */
51
    private $entityManager;
52
    /**
53
     * @var DtoFactory
54
     */
55
    private $dtoFactory;
56
    /**
57
     * @var array|bool[]
58
     */
59
    private $dtosProcessed;
60
61 5
    public function __construct(
62
        NamespaceHelper $namespaceHelper,
63
        EntityDependencyInjector $entityDependencyInjector,
64
        DtoFactory $dtoFactory
65
    ) {
66 5
        $this->namespaceHelper          = $namespaceHelper;
67 5
        $this->entityDependencyInjector = $entityDependencyInjector;
68 5
        $this->dtoFactory               = $dtoFactory;
69 5
    }
70
71 5
    public function setEntityManager(EntityManagerInterface $entityManager): EntityFactoryInterface
72
    {
73 5
        $this->entityManager = $entityManager;
74
75 5
        return $this;
76
    }
77
78
    /**
79
     * Get an instance of the specific Entity Factory for a specified Entity
80
     *
81
     * Not type hinting the return because the whole point of this is to have an entity specific method, which we
82
     * can't hint for
83
     *
84
     * @param string $entityFqn
85
     *
86
     * @return mixed
87
     */
88 1
    public function createFactoryForEntity(string $entityFqn)
89
    {
90 1
        $this->assertEntityManagerSet();
91 1
        $factoryFqn = $this->namespaceHelper->getFactoryFqnFromEntityFqn($entityFqn);
92
93 1
        return new $factoryFqn($this, $this->entityManager);
94
    }
95
96 5
    private function assertEntityManagerSet(): void
97
    {
98 5
        if (!$this->entityManager instanceof EntityManagerInterface) {
0 ignored issues
show
introduced by
$this->entityManager is always a sub-type of Doctrine\ORM\EntityManagerInterface.
Loading history...
99
            throw new RuntimeException(
100
                'No EntityManager set, this must be set first using setEntityManager()'
101
            );
102
        }
103 5
    }
104
105
    public function getEntity(string $className)
106
    {
107
        return $this->create($className);
108
    }
109
110
    /**
111
     * Build a new entity, optionally pass in a DTO to provide the data that should be used
112
     *
113
     * Optionally pass in an array of property=>value
114
     *
115
     * @param string                           $entityFqn
116
     *
117
     * @param DataTransferObjectInterface|null $dto
118
     *
119
     * @return mixed
120
     * @throws MultipleValidationException
121
     * @throws ValidationException
122
     */
123 5
    public function create(string $entityFqn, DataTransferObjectInterface $dto = null)
124
    {
125 5
        $this->assertEntityManagerSet();
126
127 5
        return $this->createEntity($entityFqn, $dto);
128
    }
129
130
    /**
131
     * Create the Entity
132
     *
133
     * If the update step throw an exception, then we detach the entity to prevent us having an empty entity in the
134
     * unit of work which would otherwise be saved to the DB
135
     *
136
     * @param string                           $entityFqn
137
     *
138
     * @param DataTransferObjectInterface|null $dto
139
     *
140
     * @param bool                             $isRootEntity
141
     *
142
     * @return EntityInterface
143
     * @throws MultipleValidationException
144
     * @throws ValidationException
145
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
146
     */
147 5
    private function createEntity(
148
        string $entityFqn,
149
        DataTransferObjectInterface $dto = null,
150
        $isRootEntity = true
151
    ): EntityInterface {
152 5
        if ($isRootEntity) {
153 5
            $this->dtosProcessed = [];
154
        }
155 5
        if (null === $dto) {
156
            $dto = $this->dtoFactory->createEmptyDtoFromEntityFqn($entityFqn);
157
        }
158 5
        $idString = (string)$dto->getId();
159 5
        if (isset(self::$created[$entityFqn][$idString])) {
160 1
            return self::$created[$entityFqn][$idString];
161
        }
162
        try {
163
            #At this point a new entity is added to the unit of work
164 5
            $entity = $this->getNewInstance($entityFqn, $dto->getId());
165
166 5
            self::$created[$entityFqn][$idString] = $entity;
167
168
            #At this point, nested entities are added to the unit of work
169 5
            $this->updateDto($entity, $dto);
170
            #At this point, the entity values are set
171 5
            $entity->update($dto);
172
173 5
            if ($isRootEntity) {
174
                #Now we have persisted all the entities, we need to validate them all
175 5
                $this->stopTransaction();
176
            }
177 1
        } catch (ValidationException | MultipleValidationException | TypeError $e) {
178
            # Something has gone wrong, now we need to remove all created entities from the unit of work
179 1
            foreach (self::$created as $entities) {
180 1
                foreach ($entities as $createdEntity) {
181 1
                    if ($createdEntity instanceof EntityInterface) {
182 1
                        $this->entityManager->getUnitOfWork()->detach($createdEntity);
183
                    }
184
                }
185
            }
186
            # And then we need to ensure that they are cleared out from the created and processed arrays
187 1
            self::$created       = [];
188 1
            $this->dtosProcessed = [];
189 1
            throw $e;
190
        }
191
192 4
        return $entity;
193
    }
194
195
    /**
196
     * Build a new instance, bypassing PPP protections so that we can call private methods and set the private
197
     * transaction property
198
     *
199
     * @param string $entityFqn
200
     * @param mixed  $id
201
     *
202
     * @return EntityInterface
203
     */
204 5
    private function getNewInstance(string $entityFqn, $id): EntityInterface
205
    {
206 5
        if (isset(self::$created[$entityFqn][(string)$id])) {
207
            throw new RuntimeException('Trying to get a new instance when one has already been created for this ID');
208
        }
209 5
        $reflection = $this->getDoctrineStaticMetaForEntityFqn($entityFqn)
210 5
                           ->getReflectionClass();
211 5
        $entity     = $reflection->newInstanceWithoutConstructor();
212
213 5
        $runInit = $reflection->getMethod(UsesPHPMetaDataInterface::METHOD_RUN_INIT);
214 5
        $runInit->setAccessible(true);
215 5
        $runInit->invoke($entity);
216
217 5
        $transactionProperty = $reflection->getProperty(AlwaysValidInterface::CREATION_TRANSACTION_RUNNING_PROPERTY);
218 5
        $transactionProperty->setAccessible(true);
219 5
        $transactionProperty->setValue($entity, true);
220
221 5
        $idSetter = $reflection->getMethod('set' . IdFieldInterface::PROP_ID);
222 5
        $idSetter->setAccessible(true);
223 5
        $idSetter->invoke($entity, $id);
224
225 5
        if ($entity instanceof EntityInterface) {
226 5
            $this->initialiseEntity($entity);
227
228 5
            $this->entityManager->persist($entity);
229
230 5
            return $entity;
231
        }
232
        throw new LogicException('Failed to create an instance of EntityInterface');
233
    }
234
235 5
    private function getDoctrineStaticMetaForEntityFqn(string $entityFqn): DoctrineStaticMeta
236
    {
237 5
        return $entityFqn::getDoctrineStaticMeta();
238
    }
239
240
    /**
241
     * Take an already instantiated Entity and perform the final initialisation steps
242
     *
243
     * @param EntityInterface $entity
244
     *
245
     * @throws \ReflectionException
246
     */
247 5
    public function initialiseEntity(EntityInterface $entity): void
248
    {
249 5
        $entity->ensureMetaDataIsSet($this->entityManager);
250 5
        $this->addListenerToEntityIfRequired($entity);
251 5
        $this->entityDependencyInjector->injectEntityDependencies($entity);
252 5
        $debugInitMethod = $entity::getDoctrineStaticMeta()
253 5
                                  ->getReflectionClass()
254 5
                                  ->getMethod(UsesPHPMetaDataInterface::METHOD_DEBUG_INIT);
255 5
        $debugInitMethod->setAccessible(true);
256 5
        $debugInitMethod->invoke($entity);
257 5
    }
258
259
    /**
260
     * Generally DSM Entities are using the Notify change tracking policy.
261
     * This ensures that they are fully set up for that
262
     *
263
     * @param EntityInterface $entity
264
     */
265 5
    private function addListenerToEntityIfRequired(EntityInterface $entity): void
266
    {
267 5
        if (!$entity instanceof NotifyPropertyChanged) {
0 ignored issues
show
introduced by
$entity is always a sub-type of Doctrine\Common\NotifyPropertyChanged.
Loading history...
268
            return;
269
        }
270 5
        $listener = $this->entityManager->getUnitOfWork();
271 5
        $entity->addPropertyChangedListener($listener);
272 5
    }
273
274 5
    private function updateDto(
275
        EntityInterface $entity,
276
        DataTransferObjectInterface $dto
277
    ): void {
278 5
        $this->replaceNestedDtoWithEntityInstanceIfIdsMatch($dto, $entity);
279 5
        $this->replaceNestedDtosWithNewEntities($dto);
280 5
        $this->dtosProcessed[spl_object_hash($dto)] = true;
281 5
    }
282
283
    /**
284
     * @param DataTransferObjectInterface $dto
285
     * @param EntityInterface             $entity
286
     * @SuppressWarnings(PHPMD.NPathComplexity)
287
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
288
     */
289 5
    private function replaceNestedDtoWithEntityInstanceIfIdsMatch(
290
        DataTransferObjectInterface $dto,
291
        EntityInterface $entity
292
    ): void {
293 5
        $dtoHash = spl_object_hash($dto);
294 5
        if (isset($this->dtosProcessed[$dtoHash])) {
295 1
            return;
296
        }
297 5
        $this->dtosProcessed[$dtoHash] = true;
298 5
        $getters                       = $this->getGettersForDtosOrCollections($dto);
299 5
        if ([[], []] === $getters) {
300 4
            return;
301
        }
302 1
        [$dtoGetters, $collectionGetters] = array_values($getters);
303 1
        $entityFqn = get_class($entity);
304 1
        foreach ($dtoGetters as $getter) {
305 1
            $propertyName        = substr($getter, 3, -3);
306 1
            $issetAsEntityMethod = 'isset' . $propertyName . 'AsEntity';
307 1
            if (true === $dto->$issetAsEntityMethod()) {
308
                continue;
309
            }
310
311 1
            $got = $dto->$getter();
312 1
            if (null === $got) {
313 1
                continue;
314
            }
315 1
            $gotHash = spl_object_hash($got);
316 1
            if (isset($this->dtosProcessed[$gotHash])) {
317
                continue;
318
            }
319
320 1
            if ($got instanceof DataTransferObjectInterface) {
321 1
                if ($got::getEntityFqn() === $entityFqn && $got->getId() === $entity->getId()) {
322
                    $setter = 'set' . $propertyName;
323
                    $dto->$setter($entity);
324
                    continue;
325
                }
326 1
                $this->replaceNestedDtoWithEntityInstanceIfIdsMatch($got, $entity);
327 1
                continue;
328
            }
329
330
            throw new LogicException('Unexpected got item ' . get_class($got));
331
        }
332 1
        foreach ($collectionGetters as $getter) {
333 1
            $got = $dto->$getter();
334 1
            if (false === ($got instanceof Collection)) {
335
                continue;
336
            }
337 1
            foreach ($got as $key => $gotItem) {
338 1
                if (false === ($gotItem instanceof DataTransferObjectInterface)) {
339
                    continue;
340
                }
341 1
                if ($gotItem::getEntityFqn() === $entityFqn && $gotItem->getId() === $entity->getId()) {
342 1
                    $got->set($key, $entity);
343 1
                    continue;
344
                }
345 1
                $this->replaceNestedDtoWithEntityInstanceIfIdsMatch($gotItem, $entity);
346
            }
347
        }
348 1
    }
349
350 5
    private function getGettersForDtosOrCollections(DataTransferObjectInterface $dto): array
351
    {
352 5
        $dtoReflection     = new ReflectionClass(get_class($dto));
353 5
        $dtoGetters        = [];
354 5
        $collectionGetters = [];
355 5
        foreach ($dtoReflection->getMethods() as $method) {
356 5
            $methodName = $method->getName();
357 5
            if (0 !== strpos($methodName, 'get')) {
358 5
                continue;
359
            }
360 5
            $returnType = $method->getReturnType();
361 5
            if (null === $returnType) {
362 5
                continue;
363
            }
364 5
            $returnTypeName = $returnType->getName();
365 5
            if (false === \ts\stringContains($returnTypeName, '\\')) {
366 5
                continue;
367
            }
368 5
            $returnTypeReflection = new ReflectionClass($returnTypeName);
369
370 5
            if ($returnTypeReflection->implementsInterface(DataTransferObjectInterface::class)) {
371 1
                $dtoGetters[] = $methodName;
372 1
                continue;
373
            }
374 5
            if ($returnTypeReflection->implementsInterface(Collection::class)) {
375 1
                $collectionGetters[] = $methodName;
376 1
                continue;
377
            }
378
        }
379
380 5
        return [$dtoGetters, $collectionGetters];
381
    }
382
383
    /**
384
     * @param DataTransferObjectInterface $dto
385
     *
386
     * @throws MultipleValidationException
387
     * @throws ValidationException
388
     * @SuppressWarnings(PHPMD.NPathComplexity)
389
     */
390 5
    private function replaceNestedDtosWithNewEntities(DataTransferObjectInterface $dto): void
391
    {
392 5
        $getters = $this->getGettersForDtosOrCollections($dto);
393 5
        if ([[], []] === $getters) {
394 4
            return;
395
        }
396 1
        [$dtoGetters, $collectionGetters] = array_values($getters);
397 1
        foreach ($dtoGetters as $getter) {
398 1
            $propertyName        = substr($getter, 3, -3);
399 1
            $issetAsEntityMethod = 'isset' . $propertyName . 'AsEntity';
400 1
            if (true === $dto->$issetAsEntityMethod()) {
401
                continue;
402
            }
403
404 1
            $nestedDto = $dto->$getter();
405 1
            if (null === $nestedDto) {
406 1
                continue;
407
            }
408 1
            $setter = 'set' . substr($getter, 3, -3);
409 1
            $dto->$setter($this->createEntity($nestedDto::getEntityFqn(), $nestedDto, false));
410
        }
411 1
        foreach ($collectionGetters as $getter) {
412 1
            $nestedDto = $dto->$getter();
413 1
            if (false === ($nestedDto instanceof Collection)) {
414
                continue;
415
            }
416 1
            $this->convertCollectionOfDtosToEntities($nestedDto);
417
        }
418 1
    }
419
420
    /**
421
     * This will take an ArrayCollection of DTO objects and replace them with the Entities
422
     *
423
     * @param Collection $collection
424
     *
425
     * @throws MultipleValidationException
426
     * @throws ValidationException
427
     */
428 1
    private function convertCollectionOfDtosToEntities(Collection $collection): void
429
    {
430 1
        if (0 === $collection->count()) {
431 1
            return;
432
        }
433 1
        [$dtoFqn, $collectionEntityFqn] = $this->deriveDtoAndEntityFqnFromCollection($collection);
434
435 1
        foreach ($collection as $key => $dto) {
436 1
            if ($dto instanceof $collectionEntityFqn) {
437 1
                continue;
438
            }
439 1
            if (false === is_object($dto)) {
440
                throw new InvalidArgumentException('Unexpected DTO value ' .
441
                                                    print_r($dto, true) .
442
                                                    ', expected an instance of' .
443
                                                    $dtoFqn);
444
            }
445 1
            if (false === ($dto instanceof DataTransferObjectInterface)) {
446
                throw new InvalidArgumentException('Found none DTO item in collection, was instance of ' .
447
                                                    get_class($dto));
448
            }
449 1
            if (false === ($dto instanceof $dtoFqn)) {
450
                throw new InvalidArgumentException('Unexpected DTO ' . get_class($dto) . ', expected ' . $dtoFqn);
451
            }
452 1
            $collection->set($key, $this->createEntity($collectionEntityFqn, $dto, false));
453
        }
454 1
    }
455
456
    /**
457
     * Loop through a collection and determine the DTO and Entity Fqn it contains
458
     *
459
     * @param Collection $collection
460
     *
461
     * @return array
462
     * @SuppressWarnings(PHPMD.NPathComplexity)
463
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
464
     */
465 1
    private function deriveDtoAndEntityFqnFromCollection(Collection $collection): array
466
    {
467 1
        if (0 === $collection->count()) {
468
            throw new RuntimeException('Collection is empty');
469
        }
470 1
        $dtoFqn              = null;
471 1
        $collectionEntityFqn = null;
472 1
        foreach ($collection as $dto) {
473 1
            if ($dto instanceof EntityInterface) {
474 1
                $collectionEntityFqn = get_class($dto);
475 1
                continue;
476
            }
477 1
            if (false === ($dto instanceof DataTransferObjectInterface)) {
478
                throw new InvalidArgumentException(
479
                    'Found none DTO item in collection, was instance of ' . get_class($dto)
480
                );
481
            }
482 1
            if (null === $dtoFqn) {
483 1
                $dtoFqn = get_class($dto);
484 1
                continue;
485
            }
486 1
            if (false === ($dto instanceof $dtoFqn)) {
487
                throw new InvalidArgumentException(
488
                    'Mismatched collection, expecting dtoFqn ' .
489
                    $dtoFqn .
490
                    ' but found ' .
491
                    get_class($dto)
492
                );
493
            }
494
        }
495 1
        if (null === $dtoFqn && null === $collectionEntityFqn) {
496
            throw new RuntimeException('Failed deriving either the DTO or Entity FQN from the collection');
497
        }
498 1
        if (null === $collectionEntityFqn) {
499 1
            $collectionEntityFqn = $this->namespaceHelper->getEntityFqnFromEntityDtoFqn($dtoFqn);
0 ignored issues
show
Deprecated Code introduced by
The function EdmondsCommerce\Doctrine...tyFqnFromEntityDtoFqn() has been deprecated: please use the static method on the DTO directly ( Ignorable by Annotation )

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

499
            $collectionEntityFqn = /** @scrutinizer ignore-deprecated */ $this->namespaceHelper->getEntityFqnFromEntityDtoFqn($dtoFqn);

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...
500
        }
501 1
        if (null === $dtoFqn) {
502 1
            $dtoFqn = $this->namespaceHelper->getEntityDtoFqnFromEntityFqn($collectionEntityFqn);
503
        }
504
505 1
        return [$dtoFqn, $collectionEntityFqn];
506
    }
507
508
    /**
509
     * Loop through all created entities and reset the transaction running property to false,
510
     * then remove the list of created entities
511
     *
512
     * @throws MultipleValidationException
513
     */
514 5
    private function stopTransaction(): void
515
    {
516 5
        $validationExceptions = [];
517 5
        foreach (self::$created as $entities) {
518 5
            foreach ($entities as $entity) {
519
                $transactionProperty =
520 5
                    $entity::getDoctrineStaticMeta()
521 5
                           ->getReflectionClass()
522 5
                           ->getProperty(AlwaysValidInterface::CREATION_TRANSACTION_RUNNING_PROPERTY);
523 5
                $transactionProperty->setAccessible(true);
524 5
                $transactionProperty->setValue($entity, false);
525
                try {
526 5
                    $entity->getValidator()->validate();
527 1
                } catch (ValidationException $validationException) {
528 1
                    $validationExceptions[] = $validationException;
529 1
                    continue;
530
                }
531
            }
532
        }
533 5
        if ([] !== $validationExceptions) {
534 1
            throw new MultipleValidationException($validationExceptions);
535
        }
536 4
        self::$created       = [];
537 4
        $this->dtosProcessed = [];
538 4
    }
539
}
540