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 (#29)
by Ross
03:51
created

RelationsGenerator::useRelationInterfaceInClass()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator;
4
5
use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException;
6
use EdmondsCommerce\DoctrineStaticMeta\MappingHelper;
7
use gossi\codegen\generator\CodeFileGenerator;
8
use gossi\codegen\model\PhpClass;
9
use gossi\codegen\model\PhpInterface;
10
use gossi\codegen\model\PhpTrait;
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
     * Generator that yields relative paths of all the files in the relations template path and the SplFileInfo objects
141
     *
142
     * Use a PHP Generator to iterate over a recursive iterator iterator and then yield:
143
     * - key: string $relativePath
144
     * - value: \SplFileInfo $fileInfo
145
     *
146
     * The `finally` step unsets the recursiveIterator once everything is done
147
     *
148
     * @return \Generator
149
     */
150
    public function getRelativePathRelationsGenerator(): \Generator
151
    {
152
        try {
153
            $recursiveIterator = new \RecursiveIteratorIterator(
154
                new \RecursiveDirectoryIterator(
155
                    \realpath(AbstractGenerator::RELATIONS_TEMPLATE_PATH),
156
                    \RecursiveDirectoryIterator::SKIP_DOTS
157
                ),
158
                \RecursiveIteratorIterator::SELF_FIRST
159
            );
160
            foreach ($recursiveIterator as $path => $fileInfo) {
161
                $relativePath = rtrim(
162
                    $this->getFilesystem()->makePathRelative(
163
                        $path,
164
                        \realpath(AbstractGenerator::RELATIONS_TEMPLATE_PATH)
165
                    ),
166
                    '/'
167
                );
168
                yield $relativePath => $fileInfo;
169
            }
170
        } finally {
171
            $recursiveIterator = null;
172
            unset($recursiveIterator);
173
        }
174
    }
175
176
177
    /**
178
     * Generate the relation traits for specified Entity
179
     *
180
     * This works by copying the template traits folder over and then updating the file contents, name and path
181
     *
182
     * @param string $entityFqn Fully Qualified Name of Entity
183
     *
184
     * @throws DoctrineStaticMetaException
185
     * @SuppressWarnings(PHPMD.StaticAccess)
186
     */
187
    public function generateRelationCodeForEntity(string $entityFqn)
188
    {
189
        try {
190
            list($className, , $subDirsNoEntities) = $this->parseFullyQualifiedName($entityFqn);
191
192
            $singularNamespacedName = $this->namespaceHelper->getSingularNamespacedName($entityFqn, $subDirsNoEntities);
193
            $pluralNamespacedName   = $this->namespaceHelper->getPluralNamespacedName($entityFqn, $subDirsNoEntities);
194
195
            $subDirsNoEntities    = \array_slice($subDirsNoEntities, 2);
196
            $destinationDirectory = $this->codeHelper->resolvePath(
197
                $this->pathToProjectRoot
198
                .'/'.$this->srcSubFolderName
199
                .AbstractGenerator::ENTITY_RELATIONS_FOLDER_NAME
200
                .\implode(
201
                    '/',
202
                    $subDirsNoEntities
203
                )
204
                .'/'.$className
205
            );
206
207
            $this->copyTemplateDirectoryAndGetPath(
208
                AbstractGenerator::RELATIONS_TEMPLATE_PATH,
209
                $destinationDirectory
210
            );
211
212
            $plural                   = \ucfirst(MappingHelper::getPluralForFqn($entityFqn));
213
            $singular                 = \ucfirst(MappingHelper::getSingularForFqn($entityFqn));
214
            $nsNoEntities             = \implode('\\', $subDirsNoEntities);
215
            $singularWithNs           = \ltrim($nsNoEntities.'\\'.$singular, '\\');
216
            $pluralWithNs             = \ltrim($nsNoEntities.'\\'.$plural, '\\');
217
            $dirsToRename             = [];
218
            $filesCreated             = [];
219
            //update file contents apart from namespace
220
            foreach ($this->getRelativePathRelationsGenerator() as $path => $fileInfo) {
221
                $realPath = \realpath("$destinationDirectory/$path");
222
                if (false === $realPath) {
223
                    throw new \RuntimeException("path $destinationDirectory/$path does not exist");
224
                }
225
                $path = $realPath;
226
                if (!$fileInfo->isDir()) {
227
                    $this->findReplace(
228
                        'use '.self::FIND_ENTITIES_NAMESPACE.'\\'.self::FIND_ENTITY_NAME,
229
                        "use $entityFqn",
230
                        $path
231
                    );
232
                    $this->findReplaceRegex(
233
                        '%use(.+?)Relations\\\TemplateEntity(.+?);%',
234
                        'use ${1}Relations\\'.$singularWithNs.'${2};',
235
                        $path
236
                    );
237
                    $this->findReplaceRegex(
238
                        '%use(.+?)Relations\\\TemplateEntity(.+?);%',
239
                        'use ${1}Relations\\'.$pluralWithNs.'${2};',
240
                        $path
241
                    );
242
243
                    $this->replaceName($singularNamespacedName, $path);
244
                    $this->replacePluralName($pluralNamespacedName, $path);
245
                    $this->replaceProjectNamespace($this->projectRootNamespace, $path);
246
                    $filesCreated[] = function () use ($path, $singularNamespacedName, $pluralNamespacedName) {
247
                        return $this->renamePathBasenameSingularOrPlural(
248
                            $path,
249
                            $singularNamespacedName,
250
                            $pluralNamespacedName
251
                        );
252
                    };
253
                    continue;
254
                }
255
                $dirsToRename[] = $path;
256
            }
257
            foreach ($filesCreated as $k => $closure) {
258
                $filesCreated[$k] = $closure();
259
            }
260
            //update directory names and update file created paths accordingly
261
            foreach ($dirsToRename as $dirPath) {
262
                $updateDirPath = $this->renamePathBasenameSingularOrPlural(
263
                    $dirPath,
264
                    $singularNamespacedName,
265
                    $pluralNamespacedName
266
                );
267
                foreach ($filesCreated as $k => $filePath) {
268
                    $filesCreated[$k] = \str_replace($dirPath, $updateDirPath, $filePath);
269
                }
270
            }
271
            //now path is totally sorted, update namespace based on path
272
            foreach ($filesCreated as $filePath) {
273
                $this->setNamespaceFromPath($filePath);
274
            }
275
        } catch (\Exception $e) {
276
            throw new DoctrineStaticMetaException(
277
                'Exception generating relation for entity '.$entityFqn.': '.$e->getMessage(),
278
                $e->getCode(),
279
                $e
280
            );
281
        }
282
    }
283
284
    /**
285
     * Add the specified interface to the specified class
286
     *
287
     * @param string $classPath
288
     * @param string $interfacePath
289
     * @SuppressWarnings(PHPMD.StaticAccess)
290
     */
291
    protected function useRelationInterfaceInClass(string $classPath, string $interfacePath)
292
    {
293
        $class     = PhpClass::fromFile($classPath);
294
        $interface = PhpInterface::fromFile($interfacePath);
295
        $class->addInterface($interface);
296
        $this->codeHelper->generate($class, $classPath);
297
    }
298
299
    /**
300
     * Add the specified trait to the specified class
301
     *
302
     * @param string $classPath
303
     * @param string $traitPath
304
     * @SuppressWarnings(PHPMD.StaticAccess)
305
     */
306
    protected function useRelationTraitInClass(string $classPath, string $traitPath)
307
    {
308
        $class     = PhpClass::fromFile($classPath);
309
        $trait     = PhpTrait::fromFile($traitPath);
310
        $class->addTrait($trait);
311
        $this->codeHelper->generate($class, $classPath);
312
    }
313
314
    /**
315
     * Get the absolute paths for the owning traits and interfaces for the specified relation type
316
     * Will ensure that the files exists
317
     *
318
     * @param string $hasType
319
     * @param string $ownedEntityFqn
320
     *
321
     * @return array [
322
     *  $owningTraitPath,
323
     *  $owningInterfacePath,
324
     *  $reciprocatingInterfacePath
325
     * ]
326
     * @throws DoctrineStaticMetaException
327
     * @SuppressWarnings(PHPMD.StaticAccess)
328
     */
329
    protected function getPathsForOwningTraitsAndInterfaces(string $hasType, string $ownedEntityFqn): array
330
    {
331
        try {
332
            $ownedHasName = $this->namespaceHelper->getOwnedHasName(
333
                $hasType,
334
                $ownedEntityFqn,
335
                $this->srcSubFolderName,
336
                $this->projectRootNamespace
337
            );
338
            $reciprocatedHasName = $this->namespaceHelper->getReciprocatedHasName(
339
                $ownedEntityFqn,
340
                $this->srcSubFolderName,
341
                $this->projectRootNamespace
342
            );
343
            $owningTraitFqn      = $this->getOwningTraitFqn($hasType, $ownedEntityFqn);
344
            list($traitName, , $traitSubDirsNoEntities) = $this->parseFullyQualifiedName($owningTraitFqn);
345
            $owningTraitPath = $this->getPathFromNameAndSubDirs($traitName, $traitSubDirsNoEntities);
346
            if (!\file_exists($owningTraitPath)) {
347
                $this->generateRelationCodeForEntity($ownedEntityFqn);
348
            }
349
            $owningInterfaceFqn = $this->getOwningInterfaceFqn($hasType, $ownedEntityFqn);
350
            list($interfaceName, , $interfaceSubDirsNoEntities) = $this->parseFullyQualifiedName($owningInterfaceFqn);
351
            $owningInterfacePath = $this->getPathFromNameAndSubDirs($interfaceName, $interfaceSubDirsNoEntities);
352
            $reciprocatingInterfacePath = \str_replace(
353
                'Has'.$ownedHasName,
354
                'Reciprocates'.$reciprocatedHasName,
355
                $owningInterfacePath
356
            );
357
358
            return [
359
                $owningTraitPath,
360
                $owningInterfacePath,
361
                $reciprocatingInterfacePath,
362
            ];
363
        } catch (\Exception $e) {
364
            throw new DoctrineStaticMetaException('Exception in '.__METHOD__.': '.$e->getMessage(), $e->getCode(), $e);
365
        }
366
    }
367
368
    /**
369
     * @param string $hasType
370
     * @param string $ownedEntityFqn
371
     *
372
     * @return string
373
     * @throws DoctrineStaticMetaException
374
     */
375
    public function getOwningTraitFqn(string $hasType, string $ownedEntityFqn): string
376
    {
377
        return $this->namespaceHelper->getOwningTraitFqn(
378
            $hasType,
379
            $ownedEntityFqn,
380
            $this->projectRootNamespace,
381
            $this->srcSubFolderName
382
        );
383
    }
384
385
    /**
386
     * @param string $hasType
387
     * @param string $ownedEntityFqn
388
     *
389
     * @return string
390
     * @throws DoctrineStaticMetaException
391
     */
392
    public function getOwningInterfaceFqn(string $hasType, string $ownedEntityFqn): string
393
    {
394
        return $this->namespaceHelper->getOwningInterfaceFqn(
395
            $hasType,
396
            $ownedEntityFqn,
397
            $this->projectRootNamespace,
398
            $this->srcSubFolderName
399
        );
400
    }
401
402
    /**
403
     * @param string $hasType
404
     *
405
     * @throws \InvalidArgumentException
406
     */
407
    protected function validateHasType(string $hasType)
408
    {
409
        if (!\in_array($hasType, static::HAS_TYPES, true)) {
410
            throw new \InvalidArgumentException(
411
                'Invalid $hasType '.$hasType.', must be one of: '
412
                .\print_r(static::HAS_TYPES, true)
413
            );
414
        }
415
    }
416
417
    /**
418
     * Set a relationship from one Entity to Another Entity.
419
     *
420
     * Also used internally to set the reciprocal side. Uses an undocumented 4th bool parameter to kill recursion.
421
     *
422
     * @param string $owningEntityFqn
423
     * @param string $hasType
424
     * @param string $ownedEntityFqn
425
     * @param bool   $reciprocate
426
     *
427
     * @throws DoctrineStaticMetaException
428
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
429
     */
430
    public function setEntityHasRelationToEntity(
431
        string $owningEntityFqn,
432
        string $hasType,
433
        string $ownedEntityFqn,
434
        bool $reciprocate = true
435
    ) {
436
        try {
437
            $this->validateHasType($hasType);
438
            list(
439
                $owningTraitPath,
440
                $owningInterfacePath,
441
                $reciprocatingInterfacePath,
442
                ) = $this->getPathsForOwningTraitsAndInterfaces(
443
                    $hasType,
444
                    $ownedEntityFqn
445
                );
446
            list($owningClass, , $owningClassSubDirs) = $this->parseFullyQualifiedName($owningEntityFqn);
447
            $owningClassPath = $this->getPathFromNameAndSubDirs($owningClass, $owningClassSubDirs);
448
            $this->useRelationTraitInClass($owningClassPath, $owningTraitPath);
449
            $this->useRelationInterfaceInClass($owningClassPath, $owningInterfacePath);
450
            if (\in_array($hasType, self::HAS_TYPES_RECIPROCATED, true)) {
451
                $this->useRelationInterfaceInClass($owningClassPath, $reciprocatingInterfacePath);
452
                if (true === $reciprocate) {
453
                    $inverseType = $this->getInverseHasType($hasType);
454
                    $this->setEntityHasRelationToEntity(
455
                        $ownedEntityFqn,
456
                        $inverseType,
457
                        $owningEntityFqn,
458
                        false
459
                    );
460
                }
461
            }
462
        } catch (\Exception $e) {
463
            throw new DoctrineStaticMetaException('Exception in '.__METHOD__.': '.$e->getMessage(), $e->getCode(), $e);
464
        }
465
    }
466
467
    /**
468
     * Get the inverse of a hasType
469
     *
470
     * @param string $hasType
471
     *
472
     * @return string
473
     * @throws DoctrineStaticMetaException
474
     */
475
    protected function getInverseHasType(string $hasType): string
476
    {
477
        switch ($hasType) {
478
            case self::HAS_ONE_TO_ONE:
479
            case self::HAS_MANY_TO_MANY:
480
                return \str_replace(
481
                    self::PREFIX_OWNING,
482
                    self::PREFIX_INVERSE,
483
                    $hasType
484
                );
485
486
            case self::HAS_INVERSE_ONE_TO_ONE:
487
            case self::HAS_INVERSE_MANY_TO_MANY:
488
                return \str_replace(
489
                    self::PREFIX_INVERSE,
490
                    self::PREFIX_OWNING,
491
                    $hasType
492
                );
493
494
            case self::HAS_MANY_TO_ONE:
495
                return self::HAS_ONE_TO_MANY;
496
497
            case self::HAS_ONE_TO_MANY:
498
                return self::HAS_MANY_TO_ONE;
499
500
            default:
501
                throw new DoctrineStaticMetaException(
502
                    'invalid $hasType '.$hasType.' when trying to set the inverted relation'
503
                );
504
        }
505
    }
506
507
508
    /**
509
     * @param string $path
510
     * @param string $singular
511
     * @param string $plural
512
     *
513
     * @return string
514
     * @throws DoctrineStaticMetaException
515
     */
516
    protected function renamePathBasenameSingularOrPlural(
517
        string $path,
518
        string $singular,
519
        string $plural
520
    ): string {
521
        $find     = self::FIND_ENTITY_NAME;
522
        $replace  = $singular;
523
        $basename = \basename($path);
524
        if (false !== \strpos($basename, self::FIND_ENTITY_NAME_PLURAL)) {
525
            $find    = self::FIND_ENTITY_NAME_PLURAL;
526
            $replace = $plural;
527
        }
528
529
        return $this->renamePathBasename($find, $replace, $path);
530
    }
531
}
532