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 (#152)
by joseph
17:58
created

assertCorrectInterfacesSet()   A

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