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 (#77)
by joseph
15:21
created

RelationsGenerator   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 444
Duplicated Lines 0 %

Test Coverage

Coverage 81.05%

Importance

Changes 0
Metric Value
wmc 25
eloc 192
dl 0
loc 444
ccs 124
cts 153
cp 0.8105
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A validateHasType() 0 6 2
A getRelativePathRelationsGenerator() 0 23 2
A useRelationInterfaceInEntityInterface() 0 9 1
A getPathsForOwningTraitsAndInterfaces() 0 47 3
A setEntityHasRelationToEntity() 0 41 4
A useRelationTraitInClass() 0 22 3
B getInverseHasType() 0 28 7
A getOwningTraitFqn() 0 7 1
A getOwningInterfaceFqn() 0 7 1
A generateRelationCodeForEntity() 0 12 1
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator;
4
5
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\Relations\GenerateRelationCodeForEntity;
6
use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException;
7
use gossi\codegen\model\PhpClass;
8
use gossi\codegen\model\PhpInterface;
9
use gossi\codegen\model\PhpTrait;
10
use PhpParser\Error;
11
12
/**
13
 * Class RelationsGenerator
14
 *
15
 * @package EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator
16
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
17
 */
18
class RelationsGenerator extends AbstractGenerator
19
{
20
    public const PREFIX_OWNING         = 'Owning';
21
    public const PREFIX_INVERSE        = 'Inverse';
22
    public const PREFIX_UNIDIRECTIONAL = 'Unidirectional';
23
24
25
    /*******************************************************************************************************************
26
     * OneToOne - One instance of the current Entity refers to One instance of the referred Entity.
27
     */
28
    public const INTERNAL_TYPE_ONE_TO_ONE = 'OneToOne';
29
30
    /**
31
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntity/HasTemplateEntityOwningOneToOne.php
32
     */
33
    public const HAS_ONE_TO_ONE = self::PREFIX_OWNING . self::INTERNAL_TYPE_ONE_TO_ONE;
34
35
    /**
36
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntity/HasTemplateEntityInverseOneToOne.php
37
     */
38
    public const HAS_INVERSE_ONE_TO_ONE = self::PREFIX_INVERSE . self::INTERNAL_TYPE_ONE_TO_ONE;
39
40
    /**
41
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntity/HasTemplateEntityUnidrectionalOneToOne.php
42
     */
43
    public const HAS_UNIDIRECTIONAL_ONE_TO_ONE = self::PREFIX_UNIDIRECTIONAL . self::INTERNAL_TYPE_ONE_TO_ONE;
44
45
46
    /*******************************************************************************************************************
47
     * OneToMany - One instance of the current Entity has Many instances (references) to the referred Entity.
48
     */
49
    public const INTERNAL_TYPE_ONE_TO_MANY = 'OneToMany';
50
51
    /**
52
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntities/HasTemplateEntitiesOneToMany.php
53
     */
54
    public const HAS_ONE_TO_MANY = self::INTERNAL_TYPE_ONE_TO_MANY;
55
56
    /**
57
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntities/HasTemplateEntitiesOneToMany.php
58
     */
59
    public const HAS_UNIDIRECTIONAL_ONE_TO_MANY = self::PREFIX_UNIDIRECTIONAL . self::INTERNAL_TYPE_ONE_TO_MANY;
60
61
62
    /*******************************************************************************************************************
63
     * ManyToOne - Many instances of the current Entity refer to One instance of the referred Entity.
64
     */
65
    public const INTERNAL_TYPE_MANY_TO_ONE = 'ManyToOne';
66
    /**
67
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntity/HasTemplateEntityManyToOne.php
68
     */
69
    public const HAS_MANY_TO_ONE = self::INTERNAL_TYPE_MANY_TO_ONE;
70
71
    /**
72
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntity/HasTemplateEntityManyToOne.php
73
     */
74
    public const HAS_UNIDIRECTIONAL_MANY_TO_ONE = self::PREFIX_UNIDIRECTIONAL . self::INTERNAL_TYPE_MANY_TO_ONE;
75
76
77
    /*******************************************************************************************************************
78
     * ManyToMany - Many instances of the current Entity refer to Many instance of the referred Entity.
79
     */
80
    public const INTERNAL_TYPE_MANY_TO_MANY = 'ManyToMany';
81
82
    /**
83
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntities/HasTemplateEntitiesOwningManyToMany.php
84
     */
85
    public const HAS_MANY_TO_MANY = self::PREFIX_OWNING . self::INTERNAL_TYPE_MANY_TO_MANY;
86
87
    /**
88
     * @see codeTemplates/src/Entities/Traits/Relations/TemplateEntity/HasTemplateEntities/HasTemplateEntitiesInverseManyToMany.php
89
     */
90
    public const HAS_INVERSE_MANY_TO_MANY = self::PREFIX_INVERSE . self::INTERNAL_TYPE_MANY_TO_MANY;
91
92
93
    /**
94
     * The full list of possible relation types
95
     */
96
    public const HAS_TYPES = [
97
        self::HAS_ONE_TO_ONE,
98
        self::HAS_INVERSE_ONE_TO_ONE,
99
        self::HAS_UNIDIRECTIONAL_ONE_TO_ONE,
100
        self::HAS_ONE_TO_MANY,
101
        self::HAS_UNIDIRECTIONAL_ONE_TO_MANY,
102
        self::HAS_MANY_TO_ONE,
103
        self::HAS_UNIDIRECTIONAL_MANY_TO_ONE,
104
        self::HAS_MANY_TO_MANY,
105
        self::HAS_INVERSE_MANY_TO_MANY,
106
    ];
107
108
    /**
109
     * Of the full list, which ones will be automatically reciprocated in the generated code
110
     */
111
    public const HAS_TYPES_RECIPROCATED = [
112
        self::HAS_ONE_TO_ONE,
113
        self::HAS_INVERSE_ONE_TO_ONE,
114
        self::HAS_ONE_TO_MANY,
115
        self::HAS_MANY_TO_ONE,
116
        self::HAS_MANY_TO_MANY,
117
        self::HAS_INVERSE_MANY_TO_MANY,
118
    ];
119
120
    /**
121
     *Of the full list, which ones are unidirectional (i.e not reciprocated)
122
     */
123
    public const HAS_TYPES_UNIDIRECTIONAL = [
124
        self::HAS_UNIDIRECTIONAL_MANY_TO_ONE,
125
        self::HAS_UNIDIRECTIONAL_ONE_TO_MANY,
126
        self::HAS_UNIDIRECTIONAL_ONE_TO_ONE,
127
    ];
128
129
    /**
130
     * Of the full list, which ones are a plural relationship, i.e they have multiple of the related entity
131
     */
132
    public const HAS_TYPES_PLURAL = [
133
        self::HAS_MANY_TO_MANY,
134
        self::HAS_INVERSE_MANY_TO_MANY,
135
        self::HAS_ONE_TO_MANY,
136
        self::HAS_UNIDIRECTIONAL_ONE_TO_MANY,
137
    ];
138
139
    /**
140
     * Set a relationship from one Entity to Another Entity.
141
     *
142
     * Also used internally to set the reciprocal side. Uses an undocumented 4th bool parameter to kill recursion.
143
     *
144
     * @param string $owningEntityFqn
145
     * @param string $hasType
146
     * @param string $ownedEntityFqn
147
     * @param bool   $reciprocate
148
     *
149
     * @throws DoctrineStaticMetaException
150
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
151
     */
152 24
    public function setEntityHasRelationToEntity(
153
        string $owningEntityFqn,
154
        string $hasType,
155
        string $ownedEntityFqn,
156
        bool $reciprocate = true
157
    ): void {
158
        try {
159 24
            $this->validateHasType($hasType);
160
            list(
161
                $owningTraitPath,
162
                $owningInterfacePath,
163
                $reciprocatingInterfacePath,
164 24
                ) = $this->getPathsForOwningTraitsAndInterfaces(
165 24
                    $hasType,
166 24
                    $ownedEntityFqn
167
                );
168 24
            list($owningClass, , $owningClassSubDirs) = $this->parseFullyQualifiedName($owningEntityFqn);
169 24
            $owningClassPath = $this->pathHelper->getPathFromNameAndSubDirs(
170 24
                $this->pathToProjectRoot,
171 24
                $owningClass,
172 24
                $owningClassSubDirs
173
            );
174 24
            $this->useRelationTraitInClass($owningClassPath, $owningTraitPath);
175 24
            $this->useRelationInterfaceInEntityInterface($owningClassPath, $owningInterfacePath);
176 24
            if (\in_array($hasType, self::HAS_TYPES_RECIPROCATED, true)) {
177 24
                $this->useRelationInterfaceInEntityInterface($owningClassPath, $reciprocatingInterfacePath);
178 24
                if (true === $reciprocate) {
179 24
                    $inverseType = $this->getInverseHasType($hasType);
180 24
                    $this->setEntityHasRelationToEntity(
181 24
                        $ownedEntityFqn,
182 24
                        $inverseType,
183 24
                        $owningEntityFqn,
184 24
                        false
185
                    );
186
                }
187
            }
188
        } catch (\Exception $e) {
189
            throw new DoctrineStaticMetaException(
190
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
191
                $e->getCode(),
192
                $e
193
            );
194
        }
195 24
    }
196
197
    /**
198
     * @param string $hasType
199
     *
200
     * @throws \InvalidArgumentException
201
     */
202 24
    protected function validateHasType(string $hasType): void
203
    {
204 24
        if (!\in_array($hasType, static::HAS_TYPES, true)) {
205
            throw new \InvalidArgumentException(
206
                'Invalid $hasType ' . $hasType . ', must be one of: '
207
                . \print_r(static::HAS_TYPES, true)
208
            );
209
        }
210 24
    }
211
212
    /**
213
     * Get the absolute paths for the owning traits and interfaces for the specified relation type
214
     * Will ensure that the files exists
215
     *
216
     * @param string $hasType
217
     * @param string $ownedEntityFqn
218
     *
219
     * @return array [
220
     *  $owningTraitPath,
221
     *  $owningInterfacePath,
222
     *  $reciprocatingInterfacePath
223
     * ]
224
     * @throws DoctrineStaticMetaException
225
     * @SuppressWarnings(PHPMD.StaticAccess)
226
     */
227 24
    protected function getPathsForOwningTraitsAndInterfaces(string $hasType, string $ownedEntityFqn): array
228
    {
229
        try {
230 24
            $ownedHasName        = $this->namespaceHelper->getOwnedHasName(
231 24
                $hasType,
232 24
                $ownedEntityFqn,
233 24
                $this->srcSubFolderName,
234 24
                $this->projectRootNamespace
235
            );
236 24
            $reciprocatedHasName = $this->namespaceHelper->getReciprocatedHasName(
237 24
                $ownedEntityFqn,
238 24
                $this->srcSubFolderName,
239 24
                $this->projectRootNamespace
240
            );
241 24
            $owningTraitFqn      = $this->getOwningTraitFqn($hasType, $ownedEntityFqn);
242 24
            list($traitName, , $traitSubDirsNoEntities) = $this->parseFullyQualifiedName($owningTraitFqn);
243 24
            $owningTraitPath = $this->pathHelper->getPathFromNameAndSubDirs(
244 24
                $this->pathToProjectRoot,
245 24
                $traitName,
246 24
                $traitSubDirsNoEntities
247
            );
248 24
            if (!\file_exists($owningTraitPath)) {
249 5
                $this->generateRelationCodeForEntity($ownedEntityFqn);
250
            }
251 24
            $owningInterfaceFqn = $this->getOwningInterfaceFqn($hasType, $ownedEntityFqn);
252 24
            list($interfaceName, , $interfaceSubDirsNoEntities) = $this->parseFullyQualifiedName($owningInterfaceFqn);
253 24
            $owningInterfacePath        = $this->pathHelper->getPathFromNameAndSubDirs(
254 24
                $this->pathToProjectRoot,
255 24
                $interfaceName,
256 24
                $interfaceSubDirsNoEntities
257
            );
258 24
            $reciprocatingInterfacePath = \str_replace(
259 24
                'Has' . $ownedHasName,
260 24
                'Reciprocates' . $reciprocatedHasName,
261 24
                $owningInterfacePath
262
            );
263
264
            return [
265 24
                $owningTraitPath,
266 24
                $owningInterfacePath,
267 24
                $reciprocatingInterfacePath,
268
            ];
269
        } catch (\Exception $e) {
270
            throw new DoctrineStaticMetaException(
271
                'Exception in ' . __METHOD__ . ': ' . $e->getMessage(),
272
                $e->getCode(),
273
                $e
274
            );
275
        }
276
    }
277
278
    /**
279
     * @param string $hasType
280
     * @param string $ownedEntityFqn
281
     *
282
     * @return string
283
     * @throws DoctrineStaticMetaException
284
     */
285 24
    public function getOwningTraitFqn(string $hasType, string $ownedEntityFqn): string
286
    {
287 24
        return $this->namespaceHelper->getOwningTraitFqn(
288 24
            $hasType,
289 24
            $ownedEntityFqn,
290 24
            $this->projectRootNamespace,
291 24
            $this->srcSubFolderName
292
        );
293
    }
294
295
    /**
296
     * Generate the relation traits for specified Entity
297
     *
298
     * This works by copying the template traits folder over and then updating the file contents, name and path
299
     *
300
     * @param string $entityFqn Fully Qualified Name of Entity
301
     *
302
     * @throws DoctrineStaticMetaException
303
     * @SuppressWarnings(PHPMD.StaticAccess)
304
     */
305 28
    public function generateRelationCodeForEntity(string $entityFqn): void
306
    {
307 28
        $invokable = new GenerateRelationCodeForEntity(
308 28
            $entityFqn,
309 28
            $this->pathToProjectRoot,
310 28
            $this->projectRootNamespace,
311 28
            $this->srcSubFolderName,
312 28
            $this->namespaceHelper,
313 28
            $this->pathHelper,
314 28
            $this->findAndReplaceHelper
315
        );
316 28
        $invokable($this->getRelativePathRelationsGenerator());
317 28
    }
318
319
    /**
320
     * Generator that yields relative paths of all the files in the relations template path and the SplFileInfo objects
321
     *
322
     * Use a PHP Generator to iterate over a recursive iterator iterator and then yield:
323
     * - key: string $relativePath
324
     * - value: \SplFileInfo $fileInfo
325
     *
326
     * The `finally` step unsets the recursiveIterator once everything is done
327
     *
328
     * @return \Generator
329
     */
330 28
    public function getRelativePathRelationsGenerator(): \Generator
331
    {
332
        try {
333 28
            $recursiveIterator = new \RecursiveIteratorIterator(
334 28
                new \RecursiveDirectoryIterator(
335 28
                    \realpath(AbstractGenerator::RELATIONS_TEMPLATE_PATH),
336 28
                    \RecursiveDirectoryIterator::SKIP_DOTS
337
                ),
338 28
                \RecursiveIteratorIterator::SELF_FIRST
339
            );
340 28
            foreach ($recursiveIterator as $path => $fileInfo) {
341 28
                $relativePath = rtrim(
342 28
                    $this->getFilesystem()->makePathRelative(
343 28
                        $path,
344 28
                        \realpath(AbstractGenerator::RELATIONS_TEMPLATE_PATH)
345
                    ),
346 28
                    '/'
347
                );
348 28
                yield $relativePath => $fileInfo;
349
            }
350 28
        } finally {
351 28
            $recursiveIterator = null;
352 28
            unset($recursiveIterator);
353
        }
354 28
    }
355
356
    /**
357
     * @param string $hasType
358
     * @param string $ownedEntityFqn
359
     *
360
     * @return string
361
     * @throws DoctrineStaticMetaException
362
     */
363 24
    public function getOwningInterfaceFqn(string $hasType, string $ownedEntityFqn): string
364
    {
365 24
        return $this->namespaceHelper->getOwningInterfaceFqn(
366 24
            $hasType,
367 24
            $ownedEntityFqn,
368 24
            $this->projectRootNamespace,
369 24
            $this->srcSubFolderName
370
        );
371
    }
372
373
    /**
374
     * Add the specified trait to the specified class
375
     *
376
     * @param string $classPath
377
     * @param string $traitPath
378
     *
379
     * @throws DoctrineStaticMetaException
380
     * @SuppressWarnings(PHPMD.StaticAccess)
381
     */
382 24
    protected function useRelationTraitInClass(string $classPath, string $traitPath): void
383
    {
384
        try {
385 24
            $class = PhpClass::fromFile($classPath);
386
        } catch (Error $e) {
387
            throw new DoctrineStaticMetaException(
388
                'PHP parsing error when loading class ' . $classPath . ': ' . $e->getMessage(),
389
                $e->getCode(),
390
                $e
391
            );
392
        }
393
        try {
394 24
            $trait = PhpTrait::fromFile($traitPath);
395
        } catch (Error $e) {
396
            throw new DoctrineStaticMetaException(
397
                'PHP parsing error when loading class ' . $classPath . ': ' . $e->getMessage(),
398
                $e->getCode(),
399
                $e
400
            );
401
        }
402 24
        $class->addTrait($trait);
403 24
        $this->codeHelper->generate($class, $classPath);
404 24
    }
405
406
    /**
407
     * Add the specified interface to the specified entity interface
408
     *
409
     * @param string $classPath
410
     * @param string $interfacePath
411
     *
412
     * @throws \ReflectionException
413
     * @SuppressWarnings(PHPMD.StaticAccess)
414
     */
415 24
    protected function useRelationInterfaceInEntityInterface(string $classPath, string $interfacePath): void
416
    {
417 24
        $entityFqn           = PhpClass::fromFile($classPath)->getQualifiedName();
418 24
        $entityInterfaceFqn  = $this->namespaceHelper->getEntityInterfaceFromEntityFqn($entityFqn);
419 24
        $entityInterfacePath = (new \ts\Reflection\ReflectionClass($entityInterfaceFqn))->getFileName();
420 24
        $entityInterface     = PhpInterface::fromFile($entityInterfacePath);
421 24
        $relationInterface   = PhpInterface::fromFile($interfacePath);
422 24
        $entityInterface->addInterface($relationInterface);
423 24
        $this->codeHelper->generate($entityInterface, $entityInterfacePath);
424 24
    }
425
426
    /**
427
     * Get the inverse of a hasType
428
     *
429
     * @param string $hasType
430
     *
431
     * @return string
432
     * @throws DoctrineStaticMetaException
433
     */
434 24
    protected function getInverseHasType(string $hasType): string
435
    {
436
        switch ($hasType) {
437 24
            case self::HAS_ONE_TO_ONE:
438 23
            case self::HAS_MANY_TO_MANY:
439 23
                return \str_replace(
440 23
                    self::PREFIX_OWNING,
441 23
                    self::PREFIX_INVERSE,
442 23
                    $hasType
443
                );
444
445 4
            case self::HAS_INVERSE_ONE_TO_ONE:
446 4
            case self::HAS_INVERSE_MANY_TO_MANY:
447
                return \str_replace(
448
                    self::PREFIX_INVERSE,
449
                    self::PREFIX_OWNING,
450
                    $hasType
451
                );
452
453 4
            case self::HAS_MANY_TO_ONE:
454 3
                return self::HAS_ONE_TO_MANY;
455
456 4
            case self::HAS_ONE_TO_MANY:
457 4
                return self::HAS_MANY_TO_ONE;
458
459
            default:
460
                throw new DoctrineStaticMetaException(
461
                    'invalid $hasType ' . $hasType . ' when trying to set the inverted relation'
462
                );
463
        }
464
    }
465
}
466