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
Pull Request — master (#194)
by joseph
29:01
created

EntityFactory::assertEntityManagerSet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

485
            $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...
486
        }
487
        if (null === $dtoFqn) {
488
            $dtoFqn = $this->namespaceHelper->getEntityDtoFqnFromEntityFqn($collectionEntityFqn);
489
        }
490
491
        return [$dtoFqn, $collectionEntityFqn];
492
    }
493
494
    /**
495
     * Loop through all created entities and reset the transaction running property to false,
496
     * then remove the list of created entities
497
     *
498
     * @param EntityInterface $rootEntity
499
     *
500
     * @throws MultipleValidationException
501
     */
502
    private function stopTransaction(EntityInterface $rootEntity): void
503
    {
504
        $validationExceptions = [];
505
        foreach (self::$created as $entities) {
506
            foreach ($entities as $entity) {
507
                if ($entity === $rootEntity) {
508
                    continue;
509
                }
510
                $transactionProperty =
511
                    $entity::getDoctrineStaticMeta()
512
                           ->getReflectionClass()
513
                           ->getProperty(AlwaysValidInterface::CREATION_TRANSACTION_RUNNING_PROPERTY);
514
                $transactionProperty->setAccessible(true);
515
                $transactionProperty->setValue($entity, false);
516
                try {
517
                    $entity->getValidator()->validate();
518
                } catch (ValidationException $validationException) {
519
                    $validationExceptions[] = $validationException;
520
                    continue;
521
                }
522
            }
523
        }
524
        if ([] !== $validationExceptions) {
525
            throw new MultipleValidationException($validationExceptions);
526
        }
527
        self::$created       = [];
528
        $this->dtosProcessed = [];
529
    }
530
}
531