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 (#180)
by joseph
22:44 queued 11s
created

EntityFactory::stopTransaction()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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

457
            $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...
458
        }
459
        if (null === $dtoFqn) {
460
            $dtoFqn = $this->namespaceHelper->getEntityDtoFqnFromEntityFqn($collectionEntityFqn);
461
        }
462
463
        return [$dtoFqn, $collectionEntityFqn];
464
    }
465
466
    /**
467
     * Loop through all created entities and reset the transaction running property to false,
468
     * then remove the list of created entities
469
     */
470
    private function stopTransaction(): void
471
    {
472
        foreach (self::$created as $entities) {
473
            foreach ($entities as $entity) {
474
                $transactionProperty =
475
                    $entity::getDoctrineStaticMeta()
476
                           ->getReflectionClass()
477
                           ->getProperty(AlwaysValidInterface::CREATION_TRANSACTION_RUNNING_PROPERTY);
478
                $transactionProperty->setAccessible(true);
479
                $transactionProperty->setValue($entity, false);
480
            }
481
        }
482
        self::$created       = [];
483
        $this->dtosProcessed = [];
484
    }
485
}
486