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 (#178)
by joseph
127:53 queued 124:55
created

EntityFactory::initialiseEntity()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 1
dl 0
loc 10
ccs 9
cts 9
cp 1
crap 1
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 10
    public function __construct(
53
        NamespaceHelper $namespaceHelper,
54
        EntityDependencyInjector $entityDependencyInjector,
55
        DtoFactory $dtoFactory
56
    ) {
57 10
        $this->namespaceHelper          = $namespaceHelper;
58 10
        $this->entityDependencyInjector = $entityDependencyInjector;
59 10
        $this->dtoFactory               = $dtoFactory;
60 10
    }
61
62 10
    public function setEntityManager(EntityManagerInterface $entityManager): EntityFactoryInterface
63
    {
64 10
        $this->entityManager = $entityManager;
65
66 10
        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 2
    public function createFactoryForEntity(string $entityFqn)
80
    {
81 2
        $this->assertEntityManagerSet();
82 2
        $factoryFqn = $this->namespaceHelper->getFactoryFqnFromEntityFqn($entityFqn);
83
84 2
        return new $factoryFqn($this, $this->entityManager);
85
    }
86
87 10
    private function assertEntityManagerSet(): void
88
    {
89 10
        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 10
    }
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 10
    public function create(string $entityFqn, DataTransferObjectInterface $dto = null)
113
    {
114 10
        $this->assertEntityManagerSet();
115
116 10
        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 10
    private function createEntity(
132
        string $entityFqn,
133
        DataTransferObjectInterface $dto = null,
134
        $isRootEntity = true
135
    ): EntityInterface {
136 10
        if ($isRootEntity) {
137 10
            $this->dtosProcessed = [];
138
        }
139 10
        if (null === $dto) {
140
            $dto = $this->dtoFactory->createEmptyDtoFromEntityFqn($entityFqn);
141
        }
142 10
        $idString = (string)$dto->getId();
143 10
        if (isset(self::$created[$entityFqn][$idString])) {
144 2
            return self::$created[$entityFqn][$idString];
145
        }
146 10
        $entity                   = $this->getNewInstance($entityFqn, $dto->getId());
147 10
        self::$created[$entityFqn][$idString] = $entity;
148
149 10
        $this->updateDto($entity, $dto);
150 10
        if ($isRootEntity) {
151 10
            $this->stopTransaction();
152
        }
153 10
        $entity->update($dto);
154
155 8
        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 10
    private function getNewInstance(string $entityFqn, $id): EntityInterface
168
    {
169 10
        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 10
        $reflection = $this->getDoctrineStaticMetaForEntityFqn($entityFqn)
173 10
                           ->getReflectionClass();
174 10
        $entity     = $reflection->newInstanceWithoutConstructor();
175
176 10
        $runInit = $reflection->getMethod(UsesPHPMetaDataInterface::METHOD_RUN_INIT);
177 10
        $runInit->setAccessible(true);
178 10
        $runInit->invoke($entity);
179
180 10
        $transactionProperty = $reflection->getProperty(AlwaysValidInterface::CREATION_TRANSACTION_RUNNING_PROPERTY);
181 10
        $transactionProperty->setAccessible(true);
182 10
        $transactionProperty->setValue($entity, true);
183
184 10
        $idSetter = $reflection->getMethod('set' . IdFieldInterface::PROP_ID);
185 10
        $idSetter->setAccessible(true);
186 10
        $idSetter->invoke($entity, $id);
187
188 10
        if ($entity instanceof EntityInterface) {
189 10
            $this->initialiseEntity($entity);
190
191 10
            $this->entityManager->persist($entity);
192
193 10
            return $entity;
194
        }
195
        throw new \LogicException('Failed to create an instance of EntityInterface');
196
    }
197
198 10
    private function getDoctrineStaticMetaForEntityFqn(string $entityFqn): DoctrineStaticMeta
199
    {
200 10
        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 10
    public function initialiseEntity(EntityInterface $entity): void
209
    {
210 10
        $entity->ensureMetaDataIsSet($this->entityManager);
211 10
        $this->addListenerToEntityIfRequired($entity);
212 10
        $this->entityDependencyInjector->injectEntityDependencies($entity);
213 10
        $debugInitMethod = $entity::getDoctrineStaticMeta()
214 10
                                  ->getReflectionClass()
215 10
                                  ->getMethod(UsesPHPMetaDataInterface::METHOD_DEBUG_INIT);
216 10
        $debugInitMethod->setAccessible(true);
217 10
        $debugInitMethod->invoke($entity);
218 10
    }
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 10
    private function addListenerToEntityIfRequired(EntityInterface $entity): void
227
    {
228 10
        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 10
        $listener = $this->entityManager->getUnitOfWork();
232 10
        $entity->addPropertyChangedListener($listener);
233 10
    }
234
235 10
    private function updateDto(
236
        EntityInterface $entity,
237
        DataTransferObjectInterface $dto
238
    ): void {
239 10
        $this->replaceNestedDtoWithEntityInstanceIfIdsMatch($dto, $entity);
240 10
        $this->replaceNestedDtosWithNewEntities($dto);
241 10
        $this->dtosProcessed[spl_object_hash($dto)] = true;
242 10
    }
243
244
    /**
245
     * @param DataTransferObjectInterface $dto
246
     * @param EntityInterface             $entity
247
     * @SuppressWarnings(PHPMD.NPathComplexity)
248
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
249
     */
250 10
    private function replaceNestedDtoWithEntityInstanceIfIdsMatch(
251
        DataTransferObjectInterface $dto,
252
        EntityInterface $entity
253
    ): void {
254 10
        $dtoHash = spl_object_hash($dto);
255 10
        if (isset($this->dtosProcessed[$dtoHash])) {
256 2
            return;
257
        }
258 10
        $this->dtosProcessed[$dtoHash] = true;
259 10
        $getters                       = $this->getGettersForDtosOrCollections($dto);
260 10
        if ([[], []] === $getters) {
261 8
            return;
262
        }
263 2
        list($dtoGetters, $collectionGetters) = array_values($getters);
264 2
        $entityFqn = \get_class($entity);
265 2
        foreach ($dtoGetters as $getter) {
266 2
            $propertyName        = substr($getter, 3, -3);
267 2
            $issetAsEntityMethod = 'isset' . $propertyName . 'AsEntity';
268 2
            if (true === $dto->$issetAsEntityMethod()) {
269
                continue;
270
            }
271
272 2
            $got = $dto->$getter();
273 2
            if (null === $got) {
274 2
                continue;
275
            }
276 2
            $gotHash = \spl_object_hash($got);
277 2
            if (isset($this->dtosProcessed[$gotHash])) {
278
                continue;
279
            }
280
281 2
            if ($got instanceof DataTransferObjectInterface) {
282 2
                if ($got::getEntityFqn() === $entityFqn && $got->getId() === $entity->getId()) {
283
                    $setter = 'set' . $propertyName;
284
                    $dto->$setter($entity);
285
                    continue;
286
                }
287 2
                $this->replaceNestedDtoWithEntityInstanceIfIdsMatch($got, $entity);
288 2
                continue;
289
            }
290
291
            throw new \LogicException('Unexpected got item ' . \get_class($got));
292
        }
293 2
        foreach ($collectionGetters as $getter) {
294 2
            $got = $dto->$getter();
295 2
            if (false === ($got instanceof Collection)) {
296
                continue;
297
            }
298 2
            foreach ($got as $key => $gotItem) {
299 2
                if (false === ($gotItem instanceof DataTransferObjectInterface)) {
300
                    continue;
301
                }
302 2
                if ($gotItem::getEntityFqn() === $entityFqn && $gotItem->getId() === $entity->getId()) {
303 2
                    $got->set($key, $entity);
304 2
                    continue;
305
                }
306 2
                $this->replaceNestedDtoWithEntityInstanceIfIdsMatch($gotItem, $entity);
307
            }
308
        }
309 2
    }
310
311 10
    private function getGettersForDtosOrCollections(DataTransferObjectInterface $dto): array
312
    {
313 10
        $dtoReflection     = new ReflectionClass(\get_class($dto));
314 10
        $dtoGetters        = [];
315 10
        $collectionGetters = [];
316 10
        foreach ($dtoReflection->getMethods() as $method) {
317 10
            $methodName = $method->getName();
318 10
            if (0 !== strpos($methodName, 'get')) {
319 10
                continue;
320
            }
321 10
            $returnType = $method->getReturnType();
322 10
            if (null === $returnType) {
323 10
                continue;
324
            }
325 10
            $returnTypeName = $returnType->getName();
326 10
            if (false === \ts\stringContains($returnTypeName, '\\')) {
327 10
                continue;
328
            }
329 10
            $returnTypeReflection = new ReflectionClass($returnTypeName);
330
331 10
            if ($returnTypeReflection->implementsInterface(DataTransferObjectInterface::class)) {
332 2
                $dtoGetters[] = $methodName;
333 2
                continue;
334
            }
335 10
            if ($returnTypeReflection->implementsInterface(Collection::class)) {
336 2
                $collectionGetters[] = $methodName;
337 2
                continue;
338
            }
339
        }
340
341 10
        return [$dtoGetters, $collectionGetters];
342
    }
343
344
    /**
345
     * @param DataTransferObjectInterface $dto
346
     *
347
     * @throws \ReflectionException
348
     * @SuppressWarnings(PHPMD.NPathComplexity)
349
     */
350 10
    private function replaceNestedDtosWithNewEntities(DataTransferObjectInterface $dto)
351
    {
352 10
        $getters = $this->getGettersForDtosOrCollections($dto);
353 10
        if ([[], []] === $getters) {
354 8
            return;
355
        }
356 2
        list($dtoGetters, $collectionGetters) = array_values($getters);
357 2
        foreach ($dtoGetters as $getter) {
358 2
            $propertyName        = substr($getter, 3, -3);
359 2
            $issetAsEntityMethod = 'isset' . $propertyName . 'AsEntity';
360 2
            if (true === $dto->$issetAsEntityMethod()) {
361
                continue;
362
            }
363
364 2
            $nestedDto = $dto->$getter();
365 2
            if (null === $nestedDto) {
366 2
                continue;
367
            }
368 2
            $setter = 'set' . substr($getter, 3, -3);
369 2
            $dto->$setter($this->createEntity($nestedDto::getEntityFqn(), $nestedDto, false));
370
        }
371 2
        foreach ($collectionGetters as $getter) {
372 2
            $nestedDto = $dto->$getter();
373 2
            if (false === ($nestedDto instanceof Collection)) {
374
                continue;
375
            }
376 2
            $this->convertCollectionOfDtosToEntities($nestedDto);
377
        }
378 2
    }
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 2
    private function convertCollectionOfDtosToEntities(Collection $collection)
388
    {
389 2
        if (0 === $collection->count()) {
390 2
            return;
391
        }
392 2
        list($dtoFqn, $collectionEntityFqn) = $this->deriveDtoAndEntityFqnFromCollection($collection);
393
394 2
        foreach ($collection as $key => $dto) {
395 2
            if ($dto instanceof $collectionEntityFqn) {
396 2
                continue;
397
            }
398 2
            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 2
            if (false === ($dto instanceof DataTransferObjectInterface)) {
405
                throw new \InvalidArgumentException('Found none DTO item in collection, was instance of ' .
406
                                                    \get_class($dto));
407
            }
408 2
            if (false === ($dto instanceof $dtoFqn)) {
409
                throw new \InvalidArgumentException('Unexpected DTO ' . \get_class($dto) . ', expected ' . $dtoFqn);
410
            }
411 2
            $collection->set($key, $this->createEntity($collectionEntityFqn, $dto, false));
412
        }
413 2
    }
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 2
    private function deriveDtoAndEntityFqnFromCollection(Collection $collection): array
425
    {
426 2
        if (0 === $collection->count()) {
427
            throw new \RuntimeException('Collection is empty');
428
        }
429 2
        $dtoFqn              = null;
430 2
        $collectionEntityFqn = null;
431 2
        foreach ($collection as $dto) {
432 2
            if ($dto instanceof EntityInterface) {
433 2
                $collectionEntityFqn = \get_class($dto);
434 2
                continue;
435
            }
436 2
            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 2
            if (null === $dtoFqn) {
442 2
                $dtoFqn = \get_class($dto);
443 2
                continue;
444
            }
445 2
            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 2
        if (null === $dtoFqn && null === $collectionEntityFqn) {
455
            throw new \RuntimeException('Failed deriving either the DTO or Entity FQN from the collection');
456
        }
457 2
        if (null === $collectionEntityFqn) {
458 2
            $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 2
        if (null === $dtoFqn) {
461 2
            $dtoFqn = $this->namespaceHelper->getEntityDtoFqnFromEntityFqn($collectionEntityFqn);
462
        }
463
464 2
        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 10
    private function stopTransaction(): void
472
    {
473 10
        foreach (self::$created as $entities) {
474 10
            foreach ($entities as $entity) {
475
                $transactionProperty =
476 10
                    $entity::getDoctrineStaticMeta()
477 10
                           ->getReflectionClass()
478 10
                           ->getProperty(AlwaysValidInterface::CREATION_TRANSACTION_RUNNING_PROPERTY);
479 10
                $transactionProperty->setAccessible(true);
480 10
                $transactionProperty->setValue($entity, false);
481
            }
482
        }
483
        //self::$created       = [];
484 10
        $this->dtosProcessed = [];
485 10
    }
486
}
487