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

getProjectNamespaceRootFromEntityFqn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\CodeGeneration;
4
5
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Command\AbstractCommand;
6
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\AbstractGenerator;
7
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
8
use EdmondsCommerce\DoctrineStaticMeta\Config;
9
use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException;
10
use EdmondsCommerce\DoctrineStaticMeta\MappingHelper;
11
12
/**
13
 * Class NamespaceHelper
14
 *
15
 * Pure functions for working with namespaces and to calculate namespaces
16
 *
17
 * @package EdmondsCommerce\DoctrineStaticMeta\CodeGeneration
18
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
19
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
20
 */
21
class NamespaceHelper
22
{
23
    /**
24
     * Get the basename of a namespace
25
     *
26
     * @param string $namespace
27
     *
28
     * @return string
29
     */
30
    public function basename(string $namespace): string
31
    {
32
        $strrpos = strrpos($namespace, '\\');
33
        if (false === $strrpos) {
34
            return $namespace;
35
        }
36
37
        return $this->tidy(substr($namespace, $strrpos + 1));
38
    }
39
40
    /**
41
     * Checks and tidies up a given namespace
42
     *
43
     * @param string $namespace
44
     *
45
     * @return string
46
     * @throws \RuntimeException
47
     */
48
    public function tidy(string $namespace): string
49
    {
50
        if (false !== strpos($namespace, '/')) {
51
            throw new \RuntimeException('Invalid namespace '.$namespace);
52
        }
53
        #remove repeated separators
54
        $namespace = preg_replace(
55
            '#'.'\\\\'.'+#',
56
            '\\',
57
            $namespace
58
        );
59
60
        return $namespace;
61
    }
62
63
    /**
64
     * Generate a tidy root namespace without a leading \
65
     *
66
     * @param string $namespace
67
     *
68
     * @return string
69
     */
70
    public function root(string $namespace): string
71
    {
72
        return $this->tidy(ltrim($namespace, '\\'));
73
    }
74
75
    /**
76
     * Work out the entity namespace root from a single entity reflection object.
77
     *
78
     * @param \ReflectionClass $entityReflection
79
     *
80
     * @return string
81
     */
82
    public function getEntityNamespaceRootFromEntityReflection(
83
        \ReflectionClass $entityReflection
84
    ): string {
85
        return $this->tidy(
86
            $this->getNamespaceRootToDirectoryFromFqn(
87
                $entityReflection->getName(),
88
                AbstractGenerator::ENTITIES_FOLDER_NAME
89
            )
90
        );
91
    }
92
93
    /**
94
     * Get the namespace root up to and including a specified directory
95
     *
96
     * @param string $fqn
97
     * @param string $directory
98
     *
99
     * @return null|string
100
     */
101
    public function getNamespaceRootToDirectoryFromFqn(string $fqn, string $directory): ?string
102
    {
103
        $strPos = \strrpos(
104
            $fqn,
105
            $directory
106
        );
107
        if (false !== $strPos) {
108
            return $this->tidy(\substr($fqn, 0, $strPos + \strlen($directory)));
109
        }
110
111
        return null;
112
    }
113
114
    /**
115
     * Get the sub path for an Entity file, start from the Entities path - normally `/path/to/project/src/Entities`
116
     *
117
     * @param string $entityFqn
118
     *
119
     * @return string
120
     */
121
    public function getEntityFileSubPath(
122
        string $entityFqn
123
    ): string {
124
        return $this->getEntitySubPath($entityFqn).'.php';
125
    }
126
127
    /**
128
     * Get the folder structure for an Entity, start from the Entities path - normally `/path/to/project/src/Entities`
129
     *
130
     * This is not the path to the file, but the sub path of directories for storing entity related items.
131
     *
132
     * @param string $entityFqn
133
     *
134
     * @return string
135
     */
136
    public function getEntitySubPath(
137
        string $entityFqn
138
    ): string {
139
        $entityPath = str_replace(
140
            '\\',
141
            '/',
142
            $this->getEntitySubNamespace($entityFqn)
143
        );
144
145
        return '/'.$entityPath;
146
    }
147
148
    /**
149
     * Get the Namespace for an Entity, start from the Entities Fully Qualified Name base - normally
150
     * `\My\Project\Entities\`
151
     *
152
     * @param string $entityFqn
153
     *
154
     * @return string
155
     */
156
    public function getEntitySubNamespace(
157
        string $entityFqn
158
    ): string {
159
        return $this->tidy(
160
            \substr(
161
                $entityFqn,
162
                \strrpos(
163
                    $entityFqn,
164
                    '\\'.AbstractGenerator::ENTITIES_FOLDER_NAME.'\\'
165
                )
166
                + \strlen('\\'.AbstractGenerator::ENTITIES_FOLDER_NAME.'\\')
167
            )
168
        );
169
    }
170
171
    /**
172
     * Get the Fully Qualified Namespace root for Traits for the specified Entity
173
     *
174
     * @param string $entityFqn
175
     *
176
     * @return string
177
     */
178
    public function getTraitsNamespaceForEntity(
179
        string $entityFqn
180
    ): string {
181
        $traitsNamespace = $this->getProjectNamespaceRootFromEntityFqn($entityFqn)
182
                           .AbstractGenerator::ENTITY_RELATIONS_NAMESPACE
183
                           .'\\'.$this->getEntitySubNamespace($entityFqn)
184
                           .'\\Traits';
185
186
        return $traitsNamespace;
187
    }
188
189
    /**
190
     * Use the fully qualified name of two Entities to calculate the Project Namespace Root
191
     *
192
     * - note: this assumes a single namespace level for entities, eg `Entities`
193
     *
194
     * @param string $entityFqn
195
     *
196
     * @return string
197
     */
198
    public function getProjectNamespaceRootFromEntityFqn(string $entityFqn): string
199
    {
200
        return $this->tidy(
201
            \substr(
202
                $entityFqn,
203
                0,
204
                \strrpos(
205
                    $entityFqn,
206
                    '\\'.AbstractGenerator::ENTITIES_FOLDER_NAME.'\\'
207
                )
208
            )
209
        );
210
    }
211
212
    /**
213
     * Get the Fully Qualified Namespace for the "HasEntities" interface for the specified Entity
214
     *
215
     * @param string $entityFqn
216
     *
217
     * @return string
218
     */
219
    public function getHasPluralInterfaceFqnForEntity(
220
        string $entityFqn
221
    ): string {
222
        $interfaceNamespace = $this->getInterfacesNamespaceForEntity($entityFqn);
223
224
        return $interfaceNamespace.'\\Has'.ucfirst($entityFqn::getPlural()).'Interface';
225
    }
226
227
    /**
228
     * Get the Fully Qualified Namespace root for Interfaces for the specified Entity
229
     *
230
     * @param string $entityFqn
231
     *
232
     * @return string
233
     */
234
    public function getInterfacesNamespaceForEntity(
235
        string $entityFqn
236
    ): string {
237
        $interfacesNamespace = $this->getProjectNamespaceRootFromEntityFqn($entityFqn)
238
                               .AbstractGenerator::ENTITY_RELATIONS_NAMESPACE
239
                               .'\\'.$this->getEntitySubNamespace($entityFqn)
240
                               .'\\Interfaces';
241
242
        return $this->tidy($interfacesNamespace);
243
    }
244
245
    /**
246
     * Get the Fully Qualified Namespace for the "HasEntity" interface for the specified Entity
247
     *
248
     * @param string $entityFqn
249
     *
250
     * @return string
251
     * @throws DoctrineStaticMetaException
252
     */
253
    public function getHasSingularInterfaceFqnForEntity(
254
        string $entityFqn
255
    ): string {
256
        try {
257
            $interfaceNamespace = $this->getInterfacesNamespaceForEntity($entityFqn);
258
259
            return $interfaceNamespace.'\\Has'.ucfirst($entityFqn::getSingular()).'Interface';
260
        } catch (\Exception $e) {
261
            throw new DoctrineStaticMetaException('Exception in '.__METHOD__.': '.$e->getMessage(), $e->getCode(), $e);
262
        }
263
    }
264
265
    /**
266
     * Get the Fully Qualified Namespace for the Relation Trait for a specific Entity and hasType
267
     *
268
     * @param string      $hasType
269
     * @param string      $ownedEntityFqn
270
     * @param string|null $projectRootNamespace
271
     * @param string      $srcFolder
272
     *
273
     * @return string
274
     * @throws DoctrineStaticMetaException
275
     */
276
    public function getOwningTraitFqn(
277
        string $hasType,
278
        string $ownedEntityFqn,
279
        ?string $projectRootNamespace = null,
280
        string $srcFolder = AbstractCommand::DEFAULT_SRC_SUBFOLDER
281
    ): string {
282
        try {
283
            $ownedHasName = $this->getOwnedHasName($hasType, $ownedEntityFqn, $srcFolder, $projectRootNamespace);
0 ignored issues
show
Bug introduced by
It seems like $projectRootNamespace can also be of type null; however, parameter $projectRootNamespace of EdmondsCommerce\Doctrine...lper::getOwnedHasName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

283
            $ownedHasName = $this->getOwnedHasName($hasType, $ownedEntityFqn, $srcFolder, /** @scrutinizer ignore-type */ $projectRootNamespace);
Loading history...
284
            if (null === $projectRootNamespace) {
285
                $projectRootNamespace = $this->getProjectRootNamespaceFromComposerJson($srcFolder);
286
            }
287
            list($ownedClassName, , $ownedSubDirectories) = $this->parseFullyQualifiedName(
288
                $ownedEntityFqn,
289
                $srcFolder,
290
                $projectRootNamespace
291
            );
292
            $traitSubDirectories = \array_slice($ownedSubDirectories, 2);
293
            $owningTraitFqn      = $this->getOwningRelationsRootFqn(
294
                $projectRootNamespace,
295
                $traitSubDirectories
296
            );
297
            $owningTraitFqn      .= $ownedClassName.'\\Traits\\Has'.$ownedHasName
298
                                    .'\\Has'.$ownedHasName.$this->stripPrefixFromHasType($hasType);
299
300
            return $this->tidy($owningTraitFqn);
301
        } catch (\Exception $e) {
302
            throw new DoctrineStaticMetaException('Exception in '.__METHOD__.': '.$e->getMessage(), $e->getCode(), $e);
303
        }
304
    }
305
306
    /**
307
     * Based on the $hasType, we calculate exactly what type of `Has` we have
308
     *
309
     * @param string $hasType
310
     * @param string $ownedEntityFqn
311
     * @param string $srcOrTestSubFolder
312
     *
313
     * @param string $projectRootNamespace
314
     * @return string
315
     * @SuppressWarnings(PHPMD.StaticAccess)
316
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
317
     */
318
    public function getOwnedHasName(
319
        string $hasType,
320
        string $ownedEntityFqn,
321
        string $srcOrTestSubFolder,
322
        string $projectRootNamespace
323
    ): string {
324
        $parsedFqn = $this->parseFullyQualifiedName(
325
            $ownedEntityFqn,
326
            $srcOrTestSubFolder,
327
            $projectRootNamespace
328
        );
329
330
        $subDirectories = $parsedFqn[2];
331
332
        if (\in_array(
333
            $hasType,
334
            RelationsGenerator::HAS_TYPES_PLURAL,
335
            true
336
        )) {
337
            return $this->getPluralNamespacedName($ownedEntityFqn, $subDirectories);
338
        }
339
340
        return $this->getSingularNamespacedName($ownedEntityFqn, $subDirectories);
341
    }
342
343
    /**
344
     * @param string $ownedEntityFqn
345
     * @param string $srcOrTestSubFolder
346
     * @param string $projectRootNamespace
347
     * @return string
348
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
349
     */
350
    public function getReciprocatedHasName(
351
        string $ownedEntityFqn,
352
        string $srcOrTestSubFolder,
353
        string $projectRootNamespace
354
    ): string {
355
        $parsedFqn = $this->parseFullyQualifiedName(
356
            $ownedEntityFqn,
357
            $srcOrTestSubFolder,
358
            $projectRootNamespace
359
        );
360
361
        $subDirectories = $parsedFqn[2];
362
363
        return $this->getSingularNamespacedName($ownedEntityFqn, $subDirectories);
364
    }
365
366
    /**
367
     * @param string $entityFqn
368
     * @param array $subDirectories
369
     * @return string
370
     * @SuppressWarnings(PHPMD.StaticAccess)
371
     */
372
    public function getSingularNamespacedName(string $entityFqn, array $subDirectories): string
373
    {
374
        $singular = \ucfirst(MappingHelper::getSingularForFqn($entityFqn));
375
376
        return $this->getNamespacedName($singular, $subDirectories);
377
    }
378
379
    /**
380
     * @param string $entityFqn
381
     * @param array $subDirectories
382
     * @return string
383
     * @SuppressWarnings(PHPMD.StaticAccess)
384
     */
385
    public function getPluralNamespacedName(string $entityFqn, array $subDirectories): string
386
    {
387
        $plural = \ucfirst(MappingHelper::getPluralForFqn($entityFqn));
388
389
        return $this->getNamespacedName($plural, $subDirectories);
390
    }
391
392
    /**
393
     * @param string $entityName
394
     * @param array $subDirectories
395
     * @return string
396
     */
397
    public function getNamespacedName(string $entityName, array $subDirectories): string
398
    {
399
        $noEntitiesDirectory = \array_slice($subDirectories, 2);
400
        $namespacedName      = \array_merge($noEntitiesDirectory, [$entityName]);
401
        return \ucfirst(\implode('', $namespacedName));
402
    }
403
404
    /**
405
     * Read src autoloader from composer json
406
     *
407
     * @param string $dirForNamespace
408
     *
409
     * @return string
410
     * @throws DoctrineStaticMetaException
411
     * @SuppressWarnings(PHPMD.StaticAccess)
412
     */
413
    public function getProjectRootNamespaceFromComposerJson(
414
        string $dirForNamespace = 'src'
415
    ): string {
416
        try {
417
            $dirForNamespace = trim($dirForNamespace, '/');
418
            $json            = json_decode(
419
                file_get_contents(Config::getProjectRootDirectory().'/composer.json'),
420
                true
421
            );
422
            /**
423
             * @var string[][][][] $json
424
             */
425
            if (isset($json['autoload']['psr-4'])) {
426
                foreach ($json['autoload']['psr-4'] as $namespace => $dirs) {
427
                    foreach ($dirs as $dir) {
428
                        $dir = trim($dir, '/');
429
                        if ($dir === $dirForNamespace) {
430
                            return $this->tidy(rtrim($namespace, '\\'));
431
                        }
432
                    }
433
                }
434
            }
435
        } catch (\Exception $e) {
436
            throw new DoctrineStaticMetaException('Exception in '.__METHOD__.': '.$e->getMessage(), $e->getCode(), $e);
437
        }
438
        throw new DoctrineStaticMetaException('Failed to find psr-4 namespace root');
439
    }
440
441
    /**
442
     * From the fully qualified name, parse out:
443
     *  - class name,
444
     *  - namespace
445
     *  - the namespace parts not including the project root namespace
446
     *
447
     * @param string $fqn
448
     *
449
     * @param string $srcOrTestSubFolder
450
     *
451
     * @param string $projectRootNamespace
452
     *
453
     * @return array [$className,$namespace,$subDirectories]
454
     * @throws DoctrineStaticMetaException
455
     */
456
    public function parseFullyQualifiedName(
457
        string $fqn,
458
        string $srcOrTestSubFolder,
459
        string $projectRootNamespace = null
460
    ): array {
461
        try {
462
            $fqn                  = $this->root($fqn);
463
            $projectRootNamespace = $this->root($projectRootNamespace);
0 ignored issues
show
Bug introduced by
It seems like $projectRootNamespace can also be of type null; however, parameter $namespace of EdmondsCommerce\Doctrine...NamespaceHelper::root() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

463
            $projectRootNamespace = $this->root(/** @scrutinizer ignore-type */ $projectRootNamespace);
Loading history...
464
            if (null === $projectRootNamespace) {
0 ignored issues
show
introduced by
The condition null === $projectRootNamespace is always false.
Loading history...
465
                $projectRootNamespace = $this->getProjectRootNamespaceFromComposerJson($srcOrTestSubFolder);
466
            }
467
            if (false === \strpos($fqn, $projectRootNamespace)) {
468
                throw new DoctrineStaticMetaException(
469
                    'The $fqn ['.$fqn.'] does not contain the project root namespace'
470
                    .' ['.$projectRootNamespace.'] - are you sure it is the correct FQN?'
471
                );
472
            }
473
            $fqnParts       = explode('\\', $fqn);
474
            $className      = array_pop($fqnParts);
475
            $namespace      = implode('\\', $fqnParts);
476
            $rootParts      = explode('\\', $projectRootNamespace);
477
            $subDirectories = [];
478
            foreach ($fqnParts as $k => $fqnPart) {
479
                if (isset($rootParts[$k]) && $rootParts[$k] === $fqnPart) {
480
                    continue;
481
                }
482
                $subDirectories[] = $fqnPart;
483
            }
484
            array_unshift($subDirectories, $srcOrTestSubFolder);
485
486
            return [
487
                $className,
488
                $this->root($namespace),
489
                $subDirectories,
490
            ];
491
        } catch (\Exception $e) {
492
            throw new DoctrineStaticMetaException('Exception in '.__METHOD__.': '.$e->getMessage(), $e->getCode(), $e);
493
        }
494
    }
495
496
    /**
497
     * Get the Namespace root for Entity Relations
498
     *
499
     * @param string $projectRootNamespace
500
     * @param array  $subDirectories
501
     *
502
     * @return string
503
     */
504
    public function getOwningRelationsRootFqn(
505
        string $projectRootNamespace,
506
        array $subDirectories
507
    ): string {
508
        $relationsRootFqn = $projectRootNamespace
509
                            .AbstractGenerator::ENTITY_RELATIONS_NAMESPACE.'\\';
510
        if (count($subDirectories) > 0) {
511
            $relationsRootFqn .= implode('\\', $subDirectories).'\\';
512
        }
513
514
        return $this->tidy($relationsRootFqn);
515
    }
516
517
    /**
518
     * Normalise a has type, removing prefixes that are not required
519
     *
520
     * Inverse hasTypes use the standard template without the prefix
521
     * The exclusion ot this are the ManyToMany and OneToOne relations
522
     *
523
     * @param string $hasType
524
     *
525
     * @return string
526
     */
527
    public function stripPrefixFromHasType(
528
        string $hasType
529
    ): string {
530
        foreach ([
531
                     RelationsGenerator::INTERNAL_TYPE_MANY_TO_MANY,
532
                     RelationsGenerator::INTERNAL_TYPE_ONE_TO_ONE,
533
                 ] as $noStrip) {
534
            if (false !== strpos($hasType, $noStrip)) {
535
                return $hasType;
536
            }
537
        }
538
539
        foreach ([
540
                     RelationsGenerator::INTERNAL_TYPE_ONE_TO_MANY,
541
                     RelationsGenerator::INTERNAL_TYPE_MANY_TO_ONE,
542
                 ] as $stripAll) {
543
            if (false !== strpos($hasType, $stripAll)) {
544
                return str_replace(
545
                    [
546
                        RelationsGenerator::PREFIX_OWNING,
547
                        RelationsGenerator::PREFIX_INVERSE,
548
                    ],
549
                    '',
550
                    $hasType
551
                );
552
            }
553
        }
554
555
        return str_replace(
556
            [
557
                RelationsGenerator::PREFIX_INVERSE,
558
            ],
559
            '',
560
            $hasType
561
        );
562
    }
563
564
    /**
565
     * Get the Fully Qualified Namespace for the Relation Interface for a specific Entity and hasType
566
     *
567
     * @param string      $hasType
568
     * @param string      $ownedEntityFqn
569
     * @param string|null $projectRootNamespace
570
     * @param string      $srcFolder
571
     *
572
     * @return string
573
     * @throws DoctrineStaticMetaException
574
     */
575
    public function getOwningInterfaceFqn(
576
        string $hasType,
577
        string $ownedEntityFqn,
578
        string $projectRootNamespace = null,
579
        string $srcFolder = AbstractCommand::DEFAULT_SRC_SUBFOLDER
580
    ): string {
581
        try {
582
            $ownedHasName = $this->getOwnedHasName($hasType, $ownedEntityFqn, $srcFolder, $projectRootNamespace);
0 ignored issues
show
Bug introduced by
It seems like $projectRootNamespace can also be of type null; however, parameter $projectRootNamespace of EdmondsCommerce\Doctrine...lper::getOwnedHasName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

582
            $ownedHasName = $this->getOwnedHasName($hasType, $ownedEntityFqn, $srcFolder, /** @scrutinizer ignore-type */ $projectRootNamespace);
Loading history...
583
            if (null === $projectRootNamespace) {
584
                $projectRootNamespace = $this->getProjectRootNamespaceFromComposerJson($srcFolder);
585
            }
586
            list($ownedClassName, , $ownedSubDirectories) = $this->parseFullyQualifiedName(
587
                $ownedEntityFqn,
588
                $srcFolder,
589
                $projectRootNamespace
590
            );
591
            $interfaceSubDirectories = \array_slice($ownedSubDirectories, 2);
592
            $owningInterfaceFqn      = $this->getOwningRelationsRootFqn(
593
                $projectRootNamespace,
594
                $interfaceSubDirectories
595
            );
596
            $owningInterfaceFqn      .= '\\'.$ownedClassName.'\\Interfaces\\Has'.$ownedHasName.'Interface';
597
598
            return $this->tidy($owningInterfaceFqn);
599
        } catch (\Exception $e) {
600
            throw new DoctrineStaticMetaException('Exception in '.__METHOD__.': '.$e->getMessage(), $e->getCode(), $e);
601
        }
602
    }
603
}
604