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 (#214)
by joseph
16:19
created

EntityFactory::getNewInstance()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

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