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

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