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 (#194)
by joseph
188:50 queued 186:13
created

EntityFactory::createEntity()   B

Complexity

Conditions 7
Paths 60

Size

Total Lines 37
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7.0061

Importance

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

469
            $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...
470
        }
471 2
        if (null === $dtoFqn) {
472 2
            $dtoFqn = $this->namespaceHelper->getEntityDtoFqnFromEntityFqn($collectionEntityFqn);
473
        }
474
475 2
        return [$dtoFqn, $collectionEntityFqn];
476
    }
477
478
    /**
479
     * Loop through all created entities and reset the transaction running property to false,
480
     * then remove the list of created entities
481
     */
482 10
    private function stopTransaction(): void
483
    {
484 10
        self::$created       = [];
485 10
        $this->dtosProcessed = [];
486 10
    }
487
}
488