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.

assertCorrectInterfacesSet()   A
last analyzed

Complexity

Conditions 5
Paths 9

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 21
nc 9
nop 5
dl 0
loc 34
rs 9.2728
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace EdmondsCommerce\DoctrineStaticMeta\Tests\Large\F\CodeGeneration\Generator;
6
7
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Command\AbstractCommand;
8
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\AbstractGenerator;
9
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\EntityGenerator;
10
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
11
use EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException;
12
use EdmondsCommerce\DoctrineStaticMeta\Tests\Assets\AbstractTest;
13
use ReflectionException;
14
use SplFileInfo;
15
use ts\Reflection\ReflectionClass;
16
17
use function in_array;
18
use function str_replace;
19
use function strlen;
20
use function ucwords;
21
22
/**
23
 * @covers \EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator
24
 * @covers \EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\Relations\GenerateRelationCodeForEntity
25
 * @large
26
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
27
 */
28
class RelationsGeneratorTest extends AbstractTest
29
{
30
    public const WORK_DIR = AbstractTest::VAR_PATH . '/' . self::TEST_TYPE_LARGE . '/RelationsGeneratorTest/';
31
32
    public const TEST_ENTITY_BASKET = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
33
                                      . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Basket';
34
35
    public const TEST_ENTITY_BASKET_ITEM = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
36
                                           . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Basket\\Item';
37
38
    public const TEST_ENTITY_BASKET_ITEM_OFFER = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
39
                                                 . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Basket\\Item\\Offer';
40
41
    public const TEST_ENTITY_NESTED_THING = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
42
                                            . AbstractGenerator::ENTITIES_FOLDER_NAME
43
                                            . '\\GeneratedRelations\\Testing\\RelationsTestEntity';
44
45
    public const TEST_ENTITY_NESTED_THING2 = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
46
                                             . AbstractGenerator::ENTITIES_FOLDER_NAME
47
                                             . '\\GeneratedRelations\\ExtraTesting\\Test\\AnotherRelationsTestEntity';
48
49
    public const TEST_ENTITY_NAMESPACING_COMPANY = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
50
                                                   . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Company';
51
52
    public const TEST_ENTITY_NAMESPACING_SOME_CLIENT = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
53
                                                       . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Some\\Client';
54
55
    public const TEST_ENTITY_NAMESPACING_ANOTHER_CLIENT = self::TEST_PROJECT_ROOT_NAMESPACE .
56
                                                          '\\'
57
                                                          .
58
                                                          AbstractGenerator::ENTITIES_FOLDER_NAME .
59
                                                          '\\Another\\Client';
60
61
    public const TEST_ENTITIES = [
62
        self::TEST_ENTITY_BASKET,
63
        self::TEST_ENTITY_BASKET_ITEM,
64
        self::TEST_ENTITY_BASKET_ITEM_OFFER,
65
        self::TEST_ENTITY_NESTED_THING,
66
        self::TEST_ENTITY_NESTED_THING2,
67
    ];
68
69
    public const TEST_ENTITIES_NAMESPACING = [
70
        self::TEST_ENTITY_NAMESPACING_COMPANY,
71
        self::TEST_ENTITY_NAMESPACING_SOME_CLIENT,
72
        self::TEST_ENTITY_NAMESPACING_ANOTHER_CLIENT,
73
    ];
74
    protected static $buildOnce = true;
75
    protected static $built     = false;
76
    /**
77
     * @var EntityGenerator
78
     */
79
    private $entityGenerator;
80
    /**
81
     * @var RelationsGenerator
82
     */
83
    private $relationsGenerator;
84
    /**
85
     * @var  ReflectionClass
86
     */
87
    private $reflection;
88
    /**
89
     * @var string
90
     */
91
    private $copiedExtraSuffix = '';
92
93
    /**
94
     * @test
95
     * @large
96
     * @coversNothing
97
     */
98
    public function allHasTypesInConstantArrays(): void
99
    {
100
        $hasTypes  = [];
101
        $constants = $this->getReflection()->getConstants();
102
        foreach ($constants as $constantName => $constantValue) {
103
            if (0 === strpos($constantName, 'HAS') && false === strpos($constantName, 'HAS_TYPES')) {
104
                $hasTypes[$constantName] = $constantValue;
105
            }
106
        }
107
        $hasTypesCounted                = count($hasTypes);
108
        $hasTypesDefinedInConstantArray = count(RelationsGenerator::HAS_TYPES);
109
        $fullDiff                       = static function (array $arrayX, array $arrayY): array {
110
            $intersect = array_intersect($arrayX, $arrayY);
111
112
            return array_merge(array_diff($arrayX, $intersect), array_diff($arrayY, $intersect));
113
        };
114
        self::assertSame(
115
            $hasTypesCounted,
116
            $hasTypesDefinedInConstantArray,
117
            'The number of defined in the constant array RelationsGenerator::HAS_TYPES is not correct:'
118
            . " \n\nfull diff:\n "
119
            . print_r($fullDiff($hasTypes, RelationsGenerator::HAS_TYPES), true)
120
        );
121
    }
122
123
    /**
124
     * @return  ReflectionClass
125
     * @throws ReflectionException
126
     */
127
    private function getReflection(): ReflectionClass
128
    {
129
        if (null === $this->reflection) {
130
            $this->reflection = new  ReflectionClass(RelationsGenerator::class);
131
        }
132
133
        return $this->reflection;
134
    }
135
136
    /**
137
     * @test
138
     * @throws DoctrineStaticMetaException
139
     * @throws ReflectionException
140
     */
141
    public function generateRelations(): void
142
    {
143
        /**
144
         * @var SplFileInfo $i
145
         */
146
        foreach (self::TEST_ENTITIES as $entityFqn) {
147
            $entityFqn = $this->getCopiedFqn($entityFqn);
148
            foreach ($this->relationsGenerator->getRelativePathRelationsGenerator() as $relativePath => $i) {
149
                if ($i->isDir()) {
150
                    continue;
151
                }
152
                $entityRefl          = new  ReflectionClass($entityFqn);
153
                $namespace           = $entityRefl->getNamespaceName();
154
                $className           = $entityRefl->getShortName();
155
                $namespaceNoEntities = substr(
156
                    $namespace,
157
                    strpos(
158
                        $namespace,
159
                        AbstractGenerator::ENTITIES_FOLDER_NAME
160
                    ) + strlen(AbstractGenerator::ENTITIES_FOLDER_NAME)
161
                );
162
                $subPathNoEntites    = str_replace('\\', '/', $namespaceNoEntities);
163
                $plural              = ucfirst($entityFqn::getDoctrineStaticMeta()->getPlural());
164
                $singular            = ucfirst($entityFqn::getDoctrineStaticMeta()->getSingular());
165
                $relativePath        = str_replace(
166
                    ['TemplateEntity', 'TemplateEntities'],
167
                    [$singular, $plural],
168
                    $relativePath
169
                );
170
                $createdFile         = realpath($this->copiedWorkDir)
171
                                       . '/' . AbstractCommand::DEFAULT_SRC_SUBFOLDER
172
                                       . '/' . AbstractGenerator::ENTITY_RELATIONS_FOLDER_NAME
173
                                       . '/' . $subPathNoEntites . '/'
174
                                       . $className . '/' . $relativePath;
175
                $this->assertNoMissedReplacements($createdFile);
176
            }
177
        }
178
        $this->qaGeneratedCode();
179
    }
180
181
    /**
182
     * @test
183
     * @large
184
     *      * @throws ReflectionException
185
     * @throws DoctrineStaticMetaException
186
     */
187
    public function setRelationsBetweenEntities(): void
188
    {
189
        $errors = [];
190
        foreach (RelationsGenerator::HAS_TYPES as $hasType) {
191
            foreach ([true, false] as $requiredReciprocation) {
192
                try {
193
                    if (false !== strpos($hasType, RelationsGenerator::PREFIX_INVERSE)) {
194
                        //inverse types are tested implicitly
195
                        continue;
196
                    }
197
                    $this->copiedExtraSuffix =
198
                        $hasType . ($requiredReciprocation ? 'RecipRequired' : 'RecipNotRequired');
199
                    $this->setUp();
200
201
                    $this->relationsGenerator->setEntityHasRelationToEntity(
202
                        $this->getCopiedFqn(self::TEST_ENTITY_BASKET),
203
                        $hasType,
204
                        $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM),
205
                        $requiredReciprocation
206
                    );
207
                    $this->assertCorrectInterfacesSet(
208
                        $this->getCopiedFqn(self::TEST_ENTITY_BASKET),
209
                        $hasType,
210
                        $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM),
211
                        $requiredReciprocation
212
                    );
213
214
                    $this->relationsGenerator->setEntityHasRelationToEntity(
215
                        $this->getCopiedFqn(self::TEST_ENTITY_BASKET),
216
                        $hasType,
217
                        $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM_OFFER),
218
                        $requiredReciprocation
219
                    );
220
                    $this->assertCorrectInterfacesSet(
221
                        $this->getCopiedFqn(self::TEST_ENTITY_BASKET),
222
                        $hasType,
223
                        $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM_OFFER),
224
                        $requiredReciprocation
225
                    );
226
227
                    $this->relationsGenerator->setEntityHasRelationToEntity(
228
                        $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING),
229
                        $hasType,
230
                        $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING2),
231
                        $requiredReciprocation
232
                    );
233
                    $this->assertCorrectInterfacesSet(
234
                        $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING),
235
                        $hasType,
236
                        $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING2),
237
                        $requiredReciprocation
238
                    );
239
                    $this->qaGeneratedCode();
240
                } catch (DoctrineStaticMetaException $e) {
241
                    $errors[] = [
242
                        'Failed setting relations using' =>
243
                            [
244
                                $this->getCopiedFqn(self::TEST_ENTITIES[0]),
245
                                $hasType,
246
                                $this->getCopiedFqn(self::TEST_ENTITIES[1]),
247
                            ],
248
                        'Exception message'              => $e->getMessage(),
249
                        'Exception trace'                => $e->getTraceAsStringRelativePath(),
250
                    ];
251
                }
252
            }
253
        }
254
        self::assertEmpty(
255
            $errors,
256
            'Found ' . count($errors) . ' errors: '
257
            . print_r($errors, true)
258
        );
259
        $this->copiedExtraSuffix   = null;
260
        $this->copiedRootNamespace = null;
261
    }
262
263
    public function setUp()
264
    {
265
        parent::setUp();
266
        $this->entityGenerator    = $this->getEntityGenerator();
267
        $this->relationsGenerator = $this->getRelationsGenerator();
268
        if (false === self::$built) {
269
            foreach (self::TEST_ENTITIES as $fqn) {
270
                $this->entityGenerator->generateEntity($fqn);
271
                $this->relationsGenerator->generateRelationCodeForEntity($fqn);
272
            }
273
            self::$built = true;
274
        }
275
        $this->setupCopiedWorkDir();
276
        $this->relationsGenerator->setPathToProjectRoot($this->copiedWorkDir)
277
                                 ->setProjectRootNamespace($this->copiedRootNamespace);
278
    }
279
280
    /**
281
     * Inspect the generated class and ensure that all required interfaces have been implemented
282
     *
283
     * @param string $owningEntityFqn
284
     * @param string $hasType
285
     * @param string $ownedEntityFqn
286
     * @param bool   $requiredReciprocation
287
     * @param bool   $assertInverse
288
     *
289
     * @return void
290
     * @throws ReflectionException
291
     * @SuppressWarnings(PHPMD)
292
     */
293
    private function assertCorrectInterfacesSet(
294
        string $owningEntityFqn,
295
        string $hasType,
296
        string $ownedEntityFqn,
297
        bool $requiredReciprocation,
298
        bool $assertInverse = true
299
    ): void {
300
        $owningInterfaces   = $this->getOwningEntityInterfaces($owningEntityFqn);
301
        $expectedInterfaces = $this->getExpectedInterfacesForEntityFqn($ownedEntityFqn, $hasType);
302
303
        $missingOwningInterfaces = [];
304
        foreach ($expectedInterfaces as $expectedInterface) {
305
            if (!in_array($expectedInterface, $owningInterfaces, true)) {
306
                $missingOwningInterfaces[] = $expectedInterface;
307
            }
308
        }
309
        self::assertEmpty(
310
            $missingOwningInterfaces,
311
            'Entity ' . $owningEntityFqn . ' has some expected owning interfaces missing for hasType: '
312
            . $hasType . "\n\n"
313
            . print_r($missingOwningInterfaces, true)
314
        );
315
316
        if ($assertInverse) {
317
            $inverseHasType = $this->getInverseHasType($hasType, $requiredReciprocation);
318
            if (null === $inverseHasType) {
319
                return;
320
            }
321
            $this->assertCorrectInterfacesSet(
322
                $ownedEntityFqn,
323
                $inverseHasType,
324
                $owningEntityFqn,
325
                false,
326
                false
327
            );
328
        }
329
    }
330
331
    /**
332
     * Get an array of all the interfaces for a class
333
     *
334
     * @param string $classFqn
335
     *
336
     * @return array
337
     * @throws ReflectionException
338
     */
339
    private function getOwningEntityInterfaces(string $classFqn): array
340
    {
341
        $owningReflection = new  ReflectionClass($classFqn);
342
343
        return $this->getImplementedInterfacesFromClassFile($owningReflection->getFileName());
344
    }
345
346
    /**
347
     * Get a an array of interfaces (short names) by reading the PHP file itself
348
     *
349
     * @param string $classFilePath
350
     *
351
     * @return array
352
     */
353
    private function getImplementedInterfacesFromClassFile(string $classFilePath): array
354
    {
355
        $interfaceFilePath = str_replace(
356
            [
357
                '/Entities/',
358
                '.php',
359
            ],
360
            [
361
                '/Entity/Interfaces/',
362
                'Interface.php',
363
            ],
364
            $classFilePath
365
        );
366
        $contents          = file_get_contents($interfaceFilePath);
367
        preg_match('%extends([^{]+){%', $contents, $matches);
368
        if (isset($matches[1])) {
369
            return array_map('trim', explode(',', $matches[1]));
370
        }
371
372
        return [];
373
    }
374
375
    /**
376
     * Get an array of the interfaces we expect an entity to implement based on the has type
377
     *
378
     * @param string $entityFqn
379
     * @param string $hasType
380
     *
381
     * @return array
382
     */
383
    private function getExpectedInterfacesForEntityFqn(string $entityFqn, string $hasType): array
384
    {
385
        $expectedInterfaces   = [];
386
        $required             = (
387
        0 === strpos($hasType, RelationsGenerator::PREFIX_REQUIRED)
388
            ? RelationsGenerator::PREFIX_REQUIRED
389
            : ''
390
        );
391
        $expectedInterfaces[] = in_array($hasType, RelationsGenerator::HAS_TYPES_PLURAL, true)
392
            ? 'Has' . $required . ucwords($entityFqn::getDoctrineStaticMeta()->getPlural()) . 'Interface'
393
            : 'Has' . $required . ucwords($entityFqn::getDoctrineStaticMeta()->getSingular()) . 'Interface';
394
        if (
395
            !in_array($hasType, RelationsGenerator::HAS_TYPES_UNIDIRECTIONAL, true)
396
            || in_array($hasType, RelationsGenerator::HAS_TYPES_RECIPROCATED, true)
397
        ) {
398
            $expectedInterfaces[] = 'Reciprocates' . ucwords($entityFqn::getDoctrineStaticMeta()->getSingular())
399
                                    . 'Interface';
400
        }
401
402
        return $expectedInterfaces;
403
    }
404
405
    /**
406
     * Get the inverse has type name, or false if there is no inverse has type
407
     *
408
     * @param string $hasType
409
     *
410
     * @param bool   $requiredReciprocation
411
     *
412
     * @return string|false
413
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
414
     * @SuppressWarnings(PHPMD.NPathComplexity)
415
     */
416
    private function getInverseHasType(string $hasType, bool $requiredReciprocation): ?string
417
    {
418
        $inverseHasType = null;
419
        switch ($hasType) {
420
            case RelationsGenerator::HAS_ONE_TO_ONE:
421
            case RelationsGenerator::HAS_MANY_TO_MANY:
422
            case RelationsGenerator::HAS_REQUIRED_ONE_TO_ONE:
423
            case RelationsGenerator::HAS_REQUIRED_MANY_TO_MANY:
424
                $inverseHasType = str_replace(
425
                    RelationsGenerator::PREFIX_OWNING,
426
                    RelationsGenerator::PREFIX_INVERSE,
427
                    $hasType
428
                );
429
                break;
430
431
            case RelationsGenerator::HAS_INVERSE_ONE_TO_ONE:
432
            case RelationsGenerator::HAS_INVERSE_MANY_TO_MANY:
433
            case RelationsGenerator::HAS_REQUIRED_INVERSE_ONE_TO_ONE:
434
            case RelationsGenerator::HAS_REQUIRED_INVERSE_MANY_TO_MANY:
435
                $inverseHasType = str_replace(
436
                    RelationsGenerator::PREFIX_INVERSE,
437
                    RelationsGenerator::PREFIX_OWNING,
438
                    $hasType
439
                );
440
                break;
441
442
            case RelationsGenerator::HAS_MANY_TO_ONE:
443
            case RelationsGenerator::HAS_REQUIRED_MANY_TO_ONE:
444
                $inverseHasType = RelationsGenerator::HAS_ONE_TO_MANY;
445
                break;
446
            case RelationsGenerator::HAS_ONE_TO_MANY:
447
            case RelationsGenerator::HAS_REQUIRED_ONE_TO_MANY:
448
                $inverseHasType = RelationsGenerator::HAS_MANY_TO_ONE;
449
                break;
450
            case RelationsGenerator::HAS_UNIDIRECTIONAL_ONE_TO_ONE:
451
            case RelationsGenerator::HAS_UNIDIRECTIONAL_ONE_TO_MANY:
452
            case RelationsGenerator::HAS_UNIDIRECTIONAL_MANY_TO_ONE:
453
            case RelationsGenerator::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_ONE:
454
            case RelationsGenerator::HAS_REQUIRED_UNIDIRECTIONAL_ONE_TO_MANY:
455
            case RelationsGenerator::HAS_REQUIRED_UNIDIRECTIONAL_MANY_TO_ONE:
456
                return null;
457
            default:
458
                $this->fail('Failed getting $inverseHasType for $hasType ' . $hasType);
459
        }
460
        if (true === $requiredReciprocation && 0 === strpos($inverseHasType, RelationsGenerator::PREFIX_REQUIRED)) {
461
            return $inverseHasType;
462
        }
463
        if (true === $requiredReciprocation && 0 !== strpos($inverseHasType, RelationsGenerator::PREFIX_REQUIRED)) {
464
            return RelationsGenerator::PREFIX_REQUIRED . $inverseHasType;
465
        }
466
        if (false === $requiredReciprocation && 0 !== strpos($inverseHasType, RelationsGenerator::PREFIX_REQUIRED)) {
467
            return $inverseHasType;
468
        }
469
        if (false === $requiredReciprocation && 0 === strpos($inverseHasType, RelationsGenerator::PREFIX_REQUIRED)) {
470
            return substr($inverseHasType, 8);
471
        }
472
    }
473
474
    protected function getCopiedNamespaceRoot(): string
475
    {
476
        return parent::getCopiedNamespaceRoot() . $this->copiedExtraSuffix;
477
    }
478
}
479