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 (#174)
by joseph
25:27
created

EntityFactory::getNewInstance()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3.072

Importance

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