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 (#173)
by joseph
88:03 queued 33:31
created

EntityFactory::getEntity()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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