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 (#193)
by joseph
33:53
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\Exception\ValidationException;
17
use ts\Reflection\ReflectionClass;
18
19
/**
20
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
21
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
22
 */
23
class EntityFactory implements 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
        try {
154 5
            $entity->update($dto);
155 1
        } catch (ValidationException | \TypeError $e) {
156 1
            $this->entityManager->getUnitOfWork()->detach($entity);
157 1
            throw $e;
158
        }
159
160 4
        return $entity;
161
    }
162
163
    /**
164
     * Build a new instance, bypassing PPP protections so that we can call private methods and set the private
165
     * transaction property
166
     *
167
     * @param string $entityFqn
168
     * @param mixed  $id
169
     *
170
     * @return EntityInterface
171
     */
172 5
    private function getNewInstance(string $entityFqn, $id): EntityInterface
173
    {
174 5
        if (isset(self::$created[$entityFqn][(string)$id])) {
175
            throw new \RuntimeException('Trying to get a new instance when one has already been created for this ID');
176
        }
177 5
        $reflection = $this->getDoctrineStaticMetaForEntityFqn($entityFqn)
178 5
                           ->getReflectionClass();
179 5
        $entity     = $reflection->newInstanceWithoutConstructor();
180
181 5
        $runInit = $reflection->getMethod(UsesPHPMetaDataInterface::METHOD_RUN_INIT);
182 5
        $runInit->setAccessible(true);
183 5
        $runInit->invoke($entity);
184
185 5
        $transactionProperty = $reflection->getProperty(AlwaysValidInterface::CREATION_TRANSACTION_RUNNING_PROPERTY);
186 5
        $transactionProperty->setAccessible(true);
187 5
        $transactionProperty->setValue($entity, true);
188
189 5
        $idSetter = $reflection->getMethod('set' . IdFieldInterface::PROP_ID);
190 5
        $idSetter->setAccessible(true);
191 5
        $idSetter->invoke($entity, $id);
192
193 5
        if ($entity instanceof EntityInterface) {
194 5
            $this->initialiseEntity($entity);
195
196 5
            $this->entityManager->persist($entity);
197
198 5
            return $entity;
199
        }
200
        throw new \LogicException('Failed to create an instance of EntityInterface');
201
    }
202
203 5
    private function getDoctrineStaticMetaForEntityFqn(string $entityFqn): DoctrineStaticMeta
204
    {
205 5
        return $entityFqn::getDoctrineStaticMeta();
206
    }
207
208
    /**
209
     * Take an already instantiated Entity and perform the final initialisation steps
210
     *
211
     * @param EntityInterface $entity
212
     */
213 5
    public function initialiseEntity(EntityInterface $entity): void
214
    {
215 5
        $entity->ensureMetaDataIsSet($this->entityManager);
216 5
        $this->addListenerToEntityIfRequired($entity);
217 5
        $this->entityDependencyInjector->injectEntityDependencies($entity);
218 5
        $debugInitMethod = $entity::getDoctrineStaticMeta()
219 5
                                  ->getReflectionClass()
220 5
                                  ->getMethod(UsesPHPMetaDataInterface::METHOD_DEBUG_INIT);
221 5
        $debugInitMethod->setAccessible(true);
222 5
        $debugInitMethod->invoke($entity);
223 5
    }
224
225
    /**
226
     * Generally DSM Entities are using the Notify change tracking policy.
227
     * This ensures that they are fully set up for that
228
     *
229
     * @param EntityInterface $entity
230
     */
231 5
    private function addListenerToEntityIfRequired(EntityInterface $entity): void
232
    {
233 5
        if (!$entity instanceof NotifyPropertyChanged) {
0 ignored issues
show
introduced by
$entity is always a sub-type of Doctrine\Common\NotifyPropertyChanged.
Loading history...
234
            return;
235
        }
236 5
        $listener = $this->entityManager->getUnitOfWork();
237 5
        $entity->addPropertyChangedListener($listener);
238 5
    }
239
240 5
    private function updateDto(
241
        EntityInterface $entity,
242
        DataTransferObjectInterface $dto
243
    ): void {
244 5
        $this->replaceNestedDtoWithEntityInstanceIfIdsMatch($dto, $entity);
245 5
        $this->replaceNestedDtosWithNewEntities($dto);
246 5
        $this->dtosProcessed[spl_object_hash($dto)] = true;
247 5
    }
248
249
    /**
250
     * @param DataTransferObjectInterface $dto
251
     * @param EntityInterface             $entity
252
     * @SuppressWarnings(PHPMD.NPathComplexity)
253
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
254
     */
255 5
    private function replaceNestedDtoWithEntityInstanceIfIdsMatch(
256
        DataTransferObjectInterface $dto,
257
        EntityInterface $entity
258
    ): void {
259 5
        $dtoHash = spl_object_hash($dto);
260 5
        if (isset($this->dtosProcessed[$dtoHash])) {
261 1
            return;
262
        }
263 5
        $this->dtosProcessed[$dtoHash] = true;
264 5
        $getters                       = $this->getGettersForDtosOrCollections($dto);
265 5
        if ([[], []] === $getters) {
266 4
            return;
267
        }
268 1
        list($dtoGetters, $collectionGetters) = array_values($getters);
269 1
        $entityFqn = \get_class($entity);
270 1
        foreach ($dtoGetters as $getter) {
271 1
            $propertyName        = substr($getter, 3, -3);
272 1
            $issetAsEntityMethod = 'isset' . $propertyName . 'AsEntity';
273 1
            if (true === $dto->$issetAsEntityMethod()) {
274
                continue;
275
            }
276
277 1
            $got = $dto->$getter();
278 1
            if (null === $got) {
279 1
                continue;
280
            }
281 1
            $gotHash = \spl_object_hash($got);
282 1
            if (isset($this->dtosProcessed[$gotHash])) {
283
                continue;
284
            }
285
286 1
            if ($got instanceof DataTransferObjectInterface) {
287 1
                if ($got::getEntityFqn() === $entityFqn && $got->getId() === $entity->getId()) {
288
                    $setter = 'set' . $propertyName;
289
                    $dto->$setter($entity);
290
                    continue;
291
                }
292 1
                $this->replaceNestedDtoWithEntityInstanceIfIdsMatch($got, $entity);
293 1
                continue;
294
            }
295
296
            throw new \LogicException('Unexpected got item ' . \get_class($got));
297
        }
298 1
        foreach ($collectionGetters as $getter) {
299 1
            $got = $dto->$getter();
300 1
            if (false === ($got instanceof Collection)) {
301
                continue;
302
            }
303 1
            foreach ($got as $key => $gotItem) {
304 1
                if (false === ($gotItem instanceof DataTransferObjectInterface)) {
305
                    continue;
306
                }
307 1
                if ($gotItem::getEntityFqn() === $entityFqn && $gotItem->getId() === $entity->getId()) {
308 1
                    $got->set($key, $entity);
309 1
                    continue;
310
                }
311 1
                $this->replaceNestedDtoWithEntityInstanceIfIdsMatch($gotItem, $entity);
312
            }
313
        }
314 1
    }
315
316 5
    private function getGettersForDtosOrCollections(DataTransferObjectInterface $dto): array
317
    {
318 5
        $dtoReflection     = new ReflectionClass(\get_class($dto));
319 5
        $dtoGetters        = [];
320 5
        $collectionGetters = [];
321 5
        foreach ($dtoReflection->getMethods() as $method) {
322 5
            $methodName = $method->getName();
323 5
            if (0 !== strpos($methodName, 'get')) {
324 5
                continue;
325
            }
326 5
            $returnType = $method->getReturnType();
327 5
            if (null === $returnType) {
328 5
                continue;
329
            }
330 5
            $returnTypeName = $returnType->getName();
331 5
            if (false === \ts\stringContains($returnTypeName, '\\')) {
332 5
                continue;
333
            }
334 5
            $returnTypeReflection = new ReflectionClass($returnTypeName);
335
336 5
            if ($returnTypeReflection->implementsInterface(DataTransferObjectInterface::class)) {
337 1
                $dtoGetters[] = $methodName;
338 1
                continue;
339
            }
340 5
            if ($returnTypeReflection->implementsInterface(Collection::class)) {
341 1
                $collectionGetters[] = $methodName;
342 1
                continue;
343
            }
344
        }
345
346 5
        return [$dtoGetters, $collectionGetters];
347
    }
348
349
    /**
350
     * @param DataTransferObjectInterface $dto
351
     *
352
     * @throws \ReflectionException
353
     * @SuppressWarnings(PHPMD.NPathComplexity)
354
     */
355 5
    private function replaceNestedDtosWithNewEntities(DataTransferObjectInterface $dto)
356
    {
357 5
        $getters = $this->getGettersForDtosOrCollections($dto);
358 5
        if ([[], []] === $getters) {
359 4
            return;
360
        }
361 1
        list($dtoGetters, $collectionGetters) = array_values($getters);
362 1
        foreach ($dtoGetters as $getter) {
363 1
            $propertyName        = substr($getter, 3, -3);
364 1
            $issetAsEntityMethod = 'isset' . $propertyName . 'AsEntity';
365 1
            if (true === $dto->$issetAsEntityMethod()) {
366
                continue;
367
            }
368
369 1
            $nestedDto = $dto->$getter();
370 1
            if (null === $nestedDto) {
371 1
                continue;
372
            }
373 1
            $setter = 'set' . substr($getter, 3, -3);
374 1
            $dto->$setter($this->createEntity($nestedDto::getEntityFqn(), $nestedDto, false));
375
        }
376 1
        foreach ($collectionGetters as $getter) {
377 1
            $nestedDto = $dto->$getter();
378 1
            if (false === ($nestedDto instanceof Collection)) {
379
                continue;
380
            }
381 1
            $this->convertCollectionOfDtosToEntities($nestedDto);
382
        }
383 1
    }
384
385
    /**
386
     * This will take an ArrayCollection of DTO objects and replace them with the Entities
387
     *
388
     * @param Collection $collection
389
     *
390
     * @throws \ReflectionException
391
     */
392 1
    private function convertCollectionOfDtosToEntities(Collection $collection)
393
    {
394 1
        if (0 === $collection->count()) {
395 1
            return;
396
        }
397 1
        list($dtoFqn, $collectionEntityFqn) = $this->deriveDtoAndEntityFqnFromCollection($collection);
398
399 1
        foreach ($collection as $key => $dto) {
400 1
            if ($dto instanceof $collectionEntityFqn) {
401 1
                continue;
402
            }
403 1
            if (false === \is_object($dto)) {
404
                throw new \InvalidArgumentException('Unexpected DTO value ' .
405
                                                    \print_r($dto, true) .
406
                                                    ', expected an instance of' .
407
                                                    $dtoFqn);
408
            }
409 1
            if (false === ($dto instanceof DataTransferObjectInterface)) {
410
                throw new \InvalidArgumentException('Found none DTO item in collection, was instance of ' .
411
                                                    \get_class($dto));
412
            }
413 1
            if (false === ($dto instanceof $dtoFqn)) {
414
                throw new \InvalidArgumentException('Unexpected DTO ' . \get_class($dto) . ', expected ' . $dtoFqn);
415
            }
416 1
            $collection->set($key, $this->createEntity($collectionEntityFqn, $dto, false));
417
        }
418 1
    }
419
420
    /**
421
     * Loop through a collection and determine the DTO and Entity Fqn it contains
422
     *
423
     * @param Collection $collection
424
     *
425
     * @return array
426
     * @SuppressWarnings(PHPMD.NPathComplexity)
427
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
428
     */
429 1
    private function deriveDtoAndEntityFqnFromCollection(Collection $collection): array
430
    {
431 1
        if (0 === $collection->count()) {
432
            throw new \RuntimeException('Collection is empty');
433
        }
434 1
        $dtoFqn              = null;
435 1
        $collectionEntityFqn = null;
436 1
        foreach ($collection as $dto) {
437 1
            if ($dto instanceof EntityInterface) {
438 1
                $collectionEntityFqn = \get_class($dto);
439 1
                continue;
440
            }
441 1
            if (false === ($dto instanceof DataTransferObjectInterface)) {
442
                throw new \InvalidArgumentException(
443
                    'Found none DTO item in collection, was instance of ' . \get_class($dto)
444
                );
445
            }
446 1
            if (null === $dtoFqn) {
447 1
                $dtoFqn = \get_class($dto);
448 1
                continue;
449
            }
450 1
            if (false === ($dto instanceof $dtoFqn)) {
451
                throw new \InvalidArgumentException(
452
                    'Mismatched collection, expecting dtoFqn ' .
453
                    $dtoFqn .
454
                    ' but found ' .
455
                    \get_class($dto)
456
                );
457
            }
458
        }
459 1
        if (null === $dtoFqn && null === $collectionEntityFqn) {
460
            throw new \RuntimeException('Failed deriving either the DTO or Entity FQN from the collection');
461
        }
462 1
        if (null === $collectionEntityFqn) {
463 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

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