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 (#101)
by joseph
18:59
created

RelationsGeneratorTest::generateRelations()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 38
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 28
dl 0
loc 38
rs 9.472
c 0
b 0
f 0
cc 4
nc 4
nop 0
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\Tests\Large\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
 * @coversDefaultClass \EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator
14
 */
15
class RelationsGeneratorTest extends AbstractTest
16
{
17
    public const WORK_DIR = AbstractTest::VAR_PATH . '/' . self::TEST_TYPE_LARGE . '/RelationsGeneratorTest/';
18
19
    public const TEST_PROJECT_ROOT_NAMESPACE = parent::TEST_PROJECT_ROOT_NAMESPACE
20
                                               . '\\RelationsGeneratorTest';
21
22
    public const TEST_ENTITY_BASKET = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
23
                                      . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Basket';
24
25
    public const TEST_ENTITY_BASKET_ITEM = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
26
                                           . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Basket\\Item';
27
28
    public const TEST_ENTITY_BASKET_ITEM_OFFER = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
29
                                                 . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Basket\\Item\\Offer';
30
31
    public const TEST_ENTITY_NESTED_THING = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
32
                                            . AbstractGenerator::ENTITIES_FOLDER_NAME
33
                                            . '\\GeneratedRelations\\Testing\\RelationsTestEntity';
34
35
    public const TEST_ENTITY_NESTED_THING2 = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
36
                                             . AbstractGenerator::ENTITIES_FOLDER_NAME
37
                                             . '\\GeneratedRelations\\ExtraTesting\\Test\\AnotherRelationsTestEntity';
38
39
    public const TEST_ENTITY_NAMESPACING_COMPANY = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
40
                                                   . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Company';
41
42
    public const TEST_ENTITY_NAMESPACING_SOME_CLIENT = self::TEST_PROJECT_ROOT_NAMESPACE . '\\'
43
                                                       . AbstractGenerator::ENTITIES_FOLDER_NAME . '\\Some\\Client';
44
45
    public const TEST_ENTITY_NAMESPACING_ANOTHER_CLIENT = self::TEST_PROJECT_ROOT_NAMESPACE .
46
                                                          '\\'
47
                                                          .
48
                                                          AbstractGenerator::ENTITIES_FOLDER_NAME .
49
                                                          '\\Another\\Client';
50
51
    public const TEST_ENTITIES = [
52
        self::TEST_ENTITY_BASKET,
53
        self::TEST_ENTITY_BASKET_ITEM,
54
        self::TEST_ENTITY_BASKET_ITEM_OFFER,
55
        self::TEST_ENTITY_NESTED_THING,
56
        self::TEST_ENTITY_NESTED_THING2,
57
    ];
58
59
    public const TEST_ENTITIES_NAMESPACING = [
60
        self::TEST_ENTITY_NAMESPACING_COMPANY,
61
        self::TEST_ENTITY_NAMESPACING_SOME_CLIENT,
62
        self::TEST_ENTITY_NAMESPACING_ANOTHER_CLIENT,
63
    ];
64
    protected static $buildOnce = true;
65
    protected static $built = false;
66
    /**
67
     * @var EntityGenerator
68
     */
69
    private $entityGenerator;
70
    /**
71
     * @var RelationsGenerator
72
     */
73
    private $relationsGenerator;
74
    /**
75
     * @var  \ts\Reflection\ReflectionClass
76
     */
77
    private $reflection;
78
    /**
79
     * @var string
80
     */
81
    private $copiedExtraSuffix = '';
82
83
    /**
84
     * @test
85
     * @large
86
     * @coversNothing
87
     */
88
    public function allHasTypesInConstantArrays(): void
89
    {
90
        $hasTypes  = [];
91
        $constants = $this->getReflection()->getConstants();
92
        foreach ($constants as $constantName => $constantValue) {
93
            if (0 === strpos($constantName, 'HAS') && false === strpos($constantName, 'HAS_TYPES')) {
94
                $hasTypes[$constantName] = $constantValue;
95
            }
96
        }
97
        $hasTypesCounted                = count($hasTypes);
98
        $hasTypesDefinedInConstantArray = count(RelationsGenerator::HAS_TYPES);
99
        $fullDiff                       = function (array $arrayX, array $arrayY): array {
100
            $intersect = array_intersect($arrayX, $arrayY);
101
102
            return array_merge(array_diff($arrayX, $intersect), array_diff($arrayY, $intersect));
103
        };
104
        self::assertSame(
105
            $hasTypesCounted,
106
            $hasTypesDefinedInConstantArray,
107
            'The number of defined in the constant array RelationsGenerator::HAS_TYPES is not correct:'
108
            . " \n\nfull diff:\n "
109
            . print_r($fullDiff($hasTypes, RelationsGenerator::HAS_TYPES), true)
110
        );
111
    }
112
113
    /**
114
     * @return  \ts\Reflection\ReflectionClass
115
     * @throws \ReflectionException
116
     */
117
    private function getReflection(): \ts\Reflection\ReflectionClass
118
    {
119
        if (null === $this->reflection) {
120
            $this->reflection = new  \ts\Reflection\ReflectionClass(RelationsGenerator::class);
121
        }
122
123
        return $this->reflection;
124
    }
125
126
    /**
127
     * @test
128
     * @large
129
     * @covers ::generateRelationCodeForEntity
130
     * @throws \ReflectionException
131
     */
132
    public function generateRelations(): void
133
    {
134
        /**
135
         * @var \SplFileInfo $i
136
         */
137
        foreach (self::TEST_ENTITIES as $entityFqn) {
138
            $entityFqn = $this->getCopiedFqn($entityFqn);
139
            foreach ($this->relationsGenerator->getRelativePathRelationsGenerator() as $relativePath => $i) {
140
                if ($i->isDir()) {
141
                    continue;
142
                }
143
                $entityRefl          = new  \ts\Reflection\ReflectionClass($entityFqn);
144
                $namespace           = $entityRefl->getNamespaceName();
145
                $className           = $entityRefl->getShortName();
146
                $namespaceNoEntities = substr(
147
                    $namespace,
148
                    strpos(
149
                        $namespace,
150
                        AbstractGenerator::ENTITIES_FOLDER_NAME
151
                    ) + \strlen(AbstractGenerator::ENTITIES_FOLDER_NAME)
152
                );
153
                $subPathNoEntites    = str_replace('\\', '/', $namespaceNoEntities);
154
                $plural              = ucfirst($entityFqn::getPlural());
155
                $singular            = ucfirst($entityFqn::getSingular());
156
                $relativePath        = str_replace(
157
                    ['TemplateEntity', 'TemplateEntities'],
158
                    [$singular, $plural],
159
                    $relativePath
160
                );
161
                $createdFile         = realpath($this->copiedWorkDir)
162
                                       . '/' . AbstractCommand::DEFAULT_SRC_SUBFOLDER
163
                                       . '/' . AbstractGenerator::ENTITY_RELATIONS_FOLDER_NAME
164
                                       . '/' . $subPathNoEntites . '/'
165
                                       . $className . '/' . $relativePath;
166
                $this->assertNoMissedReplacements($createdFile);
167
            }
168
        }
169
        $this->qaGeneratedCode();
170
    }
171
172
    /**
173
     * @test
174
     * @large
175
     * @covers ::setEntityHasRelationToEntity
176
     * @throws \ReflectionException
177
     */
178
    public function testSetRelationsBetweenEntities(): void
179
    {
180
        $errors = [];
181
        foreach (RelationsGenerator::HAS_TYPES as $hasType) {
182
            try {
183
                if (false !== strpos($hasType, RelationsGenerator::PREFIX_INVERSE)) {
184
                    //inverse types are tested implicitly
185
                    continue;
186
                }
187
                $this->copiedExtraSuffix = $hasType;
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
                );
195
                $this->assertCorrectInterfacesSet(
196
                    $this->getCopiedFqn(self::TEST_ENTITY_BASKET),
197
                    $hasType,
198
                    $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM)
199
                );
200
201
                $this->relationsGenerator->setEntityHasRelationToEntity(
202
                    $this->getCopiedFqn(self::TEST_ENTITY_BASKET),
203
                    $hasType,
204
                    $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM_OFFER)
205
                );
206
                $this->assertCorrectInterfacesSet(
207
                    $this->getCopiedFqn(self::TEST_ENTITY_BASKET),
208
                    $hasType,
209
                    $this->getCopiedFqn(self::TEST_ENTITY_BASKET_ITEM_OFFER)
210
                );
211
212
                $this->relationsGenerator->setEntityHasRelationToEntity(
213
                    $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING),
214
                    $hasType,
215
                    $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING2)
216
                );
217
                $this->assertCorrectInterfacesSet(
218
                    $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING),
219
                    $hasType,
220
                    $this->getCopiedFqn(self::TEST_ENTITY_NESTED_THING2)
221
                );
222
                $this->qaGeneratedCode();
223
            } catch (DoctrineStaticMetaException $e) {
224
                $errors[] = [
225
                    'Failed setting relations using' =>
226
                        [
227
                            $this->getCopiedFqn(self::TEST_ENTITIES[0]),
228
                            $hasType,
229
                            $this->getCopiedFqn(self::TEST_ENTITIES[1]),
230
                        ],
231
                    'Exception message'              => $e->getMessage(),
232
                    'Exception trace'                => $e->getTraceAsStringRelativePath(),
233
                ];
234
            }
235
        }
236
        self::assertEmpty(
237
            $errors,
238
            'Found ' . count($errors) . ' errors: '
239
            . print_r($errors, true)
240
        );
241
        $this->copiedExtraSuffix   = null;
242
        $this->copiedRootNamespace = null;
243
    }
244
245
    public function setup()
246
    {
247
        parent::setup();
248
        $this->entityGenerator    = $this->getEntityGenerator();
249
        $this->relationsGenerator = $this->getRelationsGenerator();
250
        if (false === self::$built) {
251
            foreach (self::TEST_ENTITIES as $fqn) {
252
                $this->entityGenerator->generateEntity($fqn);
253
                $this->relationsGenerator->generateRelationCodeForEntity($fqn);
254
            }
255
            self::$built = true;
256
        }
257
        $this->setupCopiedWorkDir();
258
        $this->relationsGenerator->setPathToProjectRoot($this->copiedWorkDir)
259
                                 ->setProjectRootNamespace($this->copiedRootNamespace);
260
    }
261
262
    /**
263
     * Inspect the generated class and ensure that all required interfaces have been implemented
264
     *
265
     * @param string $owningEntityFqn
266
     * @param string $hasType
267
     * @param string $ownedEntityFqn
268
     * @param bool   $assertInverse
269
     *
270
     * @return void
271
     * @throws \ReflectionException
272
     * @SuppressWarnings(PHPMD)
273
     */
274
    private function assertCorrectInterfacesSet(
275
        string $owningEntityFqn,
276
        string $hasType,
277
        string $ownedEntityFqn,
278
        bool $assertInverse = true
279
    ): void {
280
        $owningInterfaces   = $this->getOwningEntityInterfaces($owningEntityFqn);
281
        $expectedInterfaces = $this->getExpectedInterfacesForEntityFqn($ownedEntityFqn, $hasType);
282
283
        $missingOwningInterfaces = [];
284
        foreach ($expectedInterfaces as $expectedInterface) {
285
            if (!\in_array($expectedInterface, $owningInterfaces, true)) {
286
                $missingOwningInterfaces[] = $expectedInterface;
287
            }
288
        }
289
        self::assertEmpty(
290
            $missingOwningInterfaces,
291
            'Entity ' . $owningEntityFqn . ' has some expected owning interfaces missing for hasType: '
292
            . $hasType . "\n\n"
293
            . print_r($missingOwningInterfaces, true)
294
        );
295
296
        if ($assertInverse) {
297
            $inverseHasType = $this->getInverseHasType($hasType);
298
            if (false === $inverseHasType) {
299
                return;
300
            }
301
            $this->assertCorrectInterfacesSet(
302
                $ownedEntityFqn,
303
                $inverseHasType,
0 ignored issues
show
Bug introduced by
It seems like $inverseHasType can also be of type true; however, parameter $hasType of EdmondsCommerce\Doctrine...tCorrectInterfacesSet() 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

303
                /** @scrutinizer ignore-type */ $inverseHasType,
Loading history...
304
                $owningEntityFqn,
305
                false
306
            );
307
        }
308
    }
309
310
    /**
311
     * Get an array of all the interfaces for a class
312
     *
313
     * @param string $classFqn
314
     *
315
     * @return array
316
     * @throws \ReflectionException
317
     */
318
    private function getOwningEntityInterfaces(string $classFqn): array
319
    {
320
        $owningReflection = new  \ts\Reflection\ReflectionClass($classFqn);
321
322
        return $this->getImplementedInterfacesFromClassFile($owningReflection->getFileName());
323
    }
324
325
    /**
326
     * Get a an array of interfaces (short names) by reading the PHP file itself
327
     *
328
     * @param string $classFilePath
329
     *
330
     * @return array
331
     */
332
    private function getImplementedInterfacesFromClassFile(string $classFilePath): array
333
    {
334
        $interfaceFilePath = \str_replace(
335
            [
336
                '/Entities/',
337
                '.php',
338
            ],
339
            [
340
                '/Entity/Interfaces/',
341
                'Interface.php',
342
            ],
343
            $classFilePath
344
        );
345
        $contents          = file_get_contents($interfaceFilePath);
346
        preg_match('%extends([^{]+){%', $contents, $matches);
347
        if (isset($matches[1])) {
348
            return array_map('trim', explode(',', $matches[1]));
349
        }
350
351
        return [];
352
    }
353
354
    /**
355
     * Get an array of the interfaces we expect an entity to implement based on the has type
356
     *
357
     * @param string $entityFqn
358
     * @param string $hasType
359
     *
360
     * @return array
361
     */
362
    private function getExpectedInterfacesForEntityFqn(string $entityFqn, string $hasType): array
363
    {
364
        $expectedInterfaces   = [];
365
        $expectedInterfaces[] = \in_array($hasType, RelationsGenerator::HAS_TYPES_PLURAL, true)
366
            ? 'Has' . \ucwords($entityFqn::getPlural()) . 'Interface'
367
            : 'Has' . \ucwords($entityFqn::getSingular()) . 'Interface';
368
        if (!\in_array($hasType, RelationsGenerator::HAS_TYPES_UNIDIRECTIONAL, true)
369
            || \in_array($hasType, RelationsGenerator::HAS_TYPES_RECIPROCATED, true)
370
        ) {
371
            $expectedInterfaces[] = 'Reciprocates' . \ucwords($entityFqn::getSingular()) . 'Interface';
372
        }
373
374
        return $expectedInterfaces;
375
    }
376
377
    /**
378
     * Get the inverse has type name, or false if there is no inverse has type
379
     *
380
     * @param string $hasType
381
     *
382
     * @return bool|mixed|null|string
383
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
384
     */
385
    private function getInverseHasType(string $hasType)
386
    {
387
        $inverseHasType = null;
388
        switch ($hasType) {
389
            case RelationsGenerator::HAS_ONE_TO_ONE:
390
            case RelationsGenerator::HAS_MANY_TO_MANY:
391
                return \str_replace(
392
                    RelationsGenerator::PREFIX_OWNING,
393
                    RelationsGenerator::PREFIX_INVERSE,
394
                    $hasType
395
                );
396
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
397
398
            case RelationsGenerator::HAS_INVERSE_ONE_TO_ONE:
399
            case RelationsGenerator::HAS_INVERSE_MANY_TO_MANY:
400
                $inverseHasType = \str_replace(
401
                    RelationsGenerator::PREFIX_INVERSE,
402
                    RelationsGenerator::PREFIX_OWNING,
403
                    $hasType
404
                );
405
                break;
406
407
            case RelationsGenerator::HAS_MANY_TO_ONE:
408
                $inverseHasType = RelationsGenerator::HAS_ONE_TO_MANY;
409
                break;
410
            case RelationsGenerator::HAS_ONE_TO_MANY:
411
                $inverseHasType = RelationsGenerator::HAS_MANY_TO_ONE;
412
                break;
413
            case RelationsGenerator::HAS_UNIDIRECTIONAL_ONE_TO_ONE:
414
            case RelationsGenerator::HAS_UNIDIRECTIONAL_ONE_TO_MANY:
415
            case RelationsGenerator::HAS_UNIDIRECTIONAL_MANY_TO_ONE:
416
                $inverseHasType = false;
417
                break;
418
            default:
419
                $this->fail('Failed getting $inverseHasType for $hasType ' . $hasType);
420
        }
421
422
        return $inverseHasType;
423
    }
424
425
    protected function getCopiedNamespaceRoot(): string
426
    {
427
        return parent::getCopiedNamespaceRoot() . $this->copiedExtraSuffix;
428
    }
429
}
430