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
Push — master ( fd432d...bef2be )
by Ross
25s queued 11s
created

EntityFactory::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1.125

Importance

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