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 ( 37eca9...fbc101 )
by joseph
236:04 queued 233:05
created

EntityFactory::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1.125

Importance

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

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