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

AbstractEntityTest::getSchemaErrors()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 9
rs 10
c 0
b 0
f 0
ccs 0
cts 6
cp 0
cc 3
nc 2
nop 1
crap 12
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\Entity\Testing;
4
5
use Doctrine\Common\Cache\ArrayCache;
6
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
7
use Doctrine\ORM\EntityManager;
8
use Doctrine\ORM\Mapping\ClassMetadataInfo;
9
use Doctrine\ORM\Tools\SchemaValidator;
10
use Doctrine\ORM\Utility\PersisterHelper;
11
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\CodeHelper;
12
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
13
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
14
use EdmondsCommerce\DoctrineStaticMeta\Config;
15
use EdmondsCommerce\DoctrineStaticMeta\ConfigInterface;
16
use EdmondsCommerce\DoctrineStaticMeta\Entity\Embeddable\Objects\AbstractEmbeddableObject;
17
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
18
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\EntitySaver;
19
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\EntitySaverFactory;
20
use EdmondsCommerce\DoctrineStaticMeta\Entity\Validation\EntityValidatorFactory;
21
use EdmondsCommerce\DoctrineStaticMeta\EntityManager\EntityManagerFactory;
22
use EdmondsCommerce\DoctrineStaticMeta\Exception\ConfigException;
23
use EdmondsCommerce\DoctrineStaticMeta\SimpleEnv;
24
use PHPUnit\Framework\TestCase;
25
use Symfony\Component\Validator\Mapping\Cache\DoctrineCache;
26
27
/**
28
 * Class AbstractEntityTest
29
 *
30
 * This abstract test is designed to give you a good level of test coverage for your entities without any work required.
31
 *
32
 * You should extend the test with methods that test your specific business logic, your validators and anything else.
33
 *
34
 * You can override the methods, properties and constants as you see fit.
35
 *
36
 * @package EdmondsCommerce\DoctrineStaticMeta\Entity
37
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
38
 * @SuppressWarnings(PHPMD.NumberOfChildren)
39
 */
40
abstract class AbstractEntityTest extends TestCase implements EntityTestInterface
41
{
42
    /**
43
     * The fully qualified name of the Entity being tested, as calculated by the test class name
44
     *
45
     * @var string
46
     */
47
    protected $testedEntityFqn;
48
49
    /**
50
     * Reflection of the tested entity
51
     *
52
     * @var \ts\Reflection\ReflectionClass
53
     */
54
    protected $testedEntityReflectionClass;
55
56
57
    /**
58
     * @var EntityValidatorFactory
59
     */
60
    protected $entityValidatorFactory;
61
62
    /**
63
     * @var EntityManager
64
     */
65
    protected $entityManager;
66
67
    /**
68
     * @var array
69
     */
70
    protected $schemaErrors = [];
71
72
73
    /**
74
     * @var TestEntityGenerator
75
     */
76
    protected $testEntityGenerator;
77
78
    /**
79
     * @var EntitySaverFactory
80
     */
81
    protected $entitySaverFactory;
82
83
    /**
84
     * @var CodeHelper
85
     */
86
    protected $codeHelper;
87
88
    /**
89
     * @throws ConfigException
90
     * @throws \Exception
91
     * @SuppressWarnings(PHPMD.StaticAccess)
92
     */
93
    protected function setup()
94
    {
95
        $this->getEntityManager(true);
96
        $this->entityValidatorFactory = new EntityValidatorFactory(new DoctrineCache(new ArrayCache()));
97
        $this->entitySaverFactory = new EntitySaverFactory(
98
            $this->entityManager,
99
            new EntitySaver($this->entityManager),
100
            new NamespaceHelper()
101
        );
102
        $this->testEntityGenerator = new TestEntityGenerator(
103
            static::SEED,
104
            static::FAKER_DATA_PROVIDERS,
105
            $this->getTestedEntityReflectionClass(),
106
            $this->entitySaverFactory
107
        );
108
        $this->codeHelper = new CodeHelper(new NamespaceHelper());
109
    }
110
111
    /**
112
     * Use Doctrine's standard schema validation to get errors for the whole schema
113
     *
114
     * @param bool $update
115
     *
116
     * @return array
117
     * @throws \Exception
118
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
119
     */
120
    protected function getSchemaErrors(bool $update = false): array
121
    {
122
        if (empty($this->schemaErrors) || true === $update) {
123
            $entityManager = $this->getEntityManager();
124
            $validator = new SchemaValidator($entityManager);
125
            $this->schemaErrors = $validator->validateMapping();
126
        }
127
128
        return $this->schemaErrors;
129
    }
130
131
    /**
132
     * If a global function dsmGetEntityManagerFactory is defined, we use this
133
     *
134
     * Otherwise, we use the standard DevEntityManagerFactory,
135
     * we define a DB name which is the main DB from env but with `_test` suffixed
136
     *
137
     * @param bool $new
138
     *
139
     * @return EntityManager
140
     * @throws ConfigException
141
     * @throws \Exception
142
     * @SuppressWarnings(PHPMD)
143
     */
144
    protected function getEntityManager(bool $new = false): EntityManager
145
    {
146
        if (null === $this->entityManager || true === $new) {
147
            if (\function_exists(self::GET_ENTITY_MANAGER_FUNCTION_NAME)) {
148
                $this->entityManager = \call_user_func(self::GET_ENTITY_MANAGER_FUNCTION_NAME);
149
            } else {
150
                SimpleEnv::setEnv(Config::getProjectRootDirectory().'/.env');
151
                $testConfig = $_SERVER;
152
                $testConfig[ConfigInterface::PARAM_DB_NAME] = $_SERVER[ConfigInterface::PARAM_DB_NAME].'_test';
153
                $config = new Config($testConfig);
154
                $this->entityManager = (new EntityManagerFactory(new ArrayCache()))
155
                    ->getEntityManager($config);
156
            }
157
        }
158
159
        return $this->entityManager;
160
    }
161
162
163
    /**
164
     * Use Doctrine's built in schema validation tool to catch issues
165
     */
166
    public function testValidateSchema()
167
    {
168
        $errors = $this->getSchemaErrors();
169
        $class = $this->getTestedEntityFqn();
170
        $message = '';
171
        if (isset($errors[$class])) {
172
            $message = "Failed ORM Validate Schema:\n";
173
            foreach ($errors[$class] as $err) {
174
                $message .= "\n * $err \n";
175
            }
176
        }
177
        $this->assertEmpty($message);
178
    }
179
180
181
    /**
182
     * @param string $class
183
     * @param int|string $id
184
     * @param EntityManager $entityManager
185
     *
186
     * @return EntityInterface|null
187
     */
188
    protected function loadEntity(string $class, $id, EntityManager $entityManager): ?EntityInterface
189
    {
190
        return $entityManager->getRepository($class)->find($id);
191
    }
192
193
    /**
194
     * Test that we have correctly generated an instance of our test entity
195
     *
196
     * @throws ConfigException
197
     * @throws \Doctrine\ORM\Query\QueryException
198
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
199
     * @throws \Exception
200
     * @throws \ReflectionException
201
     * @SuppressWarnings(PHPMD.StaticAccess)
202
     */
203
    public function testGeneratedCreate()
204
    {
205
        $entityManager = $this->getEntityManager();
206
        $class = $this->getTestedEntityFqn();
207
        $generated = $this->testEntityGenerator->generateEntity($entityManager, $class);
208
        $this->assertInstanceOf($class, $generated);
209
        $saver = $this->entitySaverFactory->getSaverForEntity($generated);
210
        $this->testEntityGenerator->addAssociationEntities($entityManager, $generated);
211
        $this->validateEntity($generated);
212
        $meta = $entityManager->getClassMetadata($class);
213
        foreach ($meta->getFieldNames() as $fieldName) {
214
            $type = PersisterHelper::getTypeOfField($fieldName, $meta, $entityManager)[0];
215
            $method = $this->getGetterNameForField($fieldName, $type);
216
            if (\ts\stringContains($method, '.')) {
217
                list($getEmbeddableMethod,) = explode('.', $method);
218
                $embeddable = $generated->$getEmbeddableMethod();
219
                $this->assertInstanceOf(AbstractEmbeddableObject::class, $embeddable);
220
                continue;
221
            }
222
            $reflectionMethod = new \ReflectionMethod($generated, $method);
223
            if ($reflectionMethod->hasReturnType()) {
224
                $returnType = $reflectionMethod->getReturnType();
225
                $allowsNull = $returnType->allowsNull();
226
                if ($allowsNull) {
227
                    // As we can't assert anything here so simply call
228
                    // the method and allow the type hint to raise any
229
                    // errors.
230
                    $generated->$method();
231
                    continue;
232
                }
233
            }
234
            $this->assertNotNull($generated->$method(), "$fieldName getter returned null");
235
        }
236
        $saver->save($generated);
237
        $entityManager = $this->getEntityManager(true);
238
        $loaded = $this->loadEntity($class, $generated->getId(), $entityManager);
239
        $this->assertInstanceOf($class, $loaded);
240
        $this->validateEntity($loaded);
241
        foreach ($meta->getAssociationMappings() as $mapping) {
242
            $getter = 'get'.$mapping['fieldName'];
243
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
244
                $collection = $loaded->$getter()->toArray();
245
                $this->assertNotEmpty(
246
                    $collection,
247
                    'Failed to load the collection of the associated entity ['.$mapping['fieldName']
248
                    .'] from the generated '.$class
249
                    .', make sure you have reciprocal adding of the association'
250
                );
251
                $this->assertCorrectMappings($class, $mapping, $entityManager);
252
                continue;
253
            }
254
            $association = $loaded->$getter();
255
            $this->assertNotEmpty(
256
                $association,
257
                'Failed to load the associated entity: ['.$mapping['fieldName']
258
                .'] from the generated '.$class
259
            );
260
            $this->assertNotEmpty(
261
                $association->getId(),
262
                'Failed to get the ID of the associated entity: ['.$mapping['fieldName']
263
                .'] from the generated '.$class
264
            );
265
        }
266
        $this->assertUniqueFieldsMustBeUnique($meta);
267
    }
268
269
    public function testConstructor(): EntityInterface
270
    {
271
        $class = $this->getTestedEntityFqn();
272
        $entity = new $class($this->entityValidatorFactory);
273
        $this->assertInstanceOf($class, $entity);
274
275
        return $entity;
276
    }
277
278
    /**
279
     * @depends testConstructor
280
     * @param EntityInterface $entity
281
     */
282
    public function testGetGetters(EntityInterface $entity)
283
    {
284
        $getters = $entity->getGetters();
285
        self::assertNotEmpty($getters);
286
        foreach ($getters as $getter) {
287
            self::assertRegExp('%^(get|is|has).+%', $getter);
288
        }
289
    }
290
291
    /**
292
     * @depends testConstructor
293
     * @param EntityInterface $entity
294
     */
295
    public function testSetSetters(EntityInterface $entity)
296
    {
297
        $setters = $entity->getSetters();
298
        self::assertNotEmpty($setters);
299
        foreach ($setters as $setter) {
300
            self::assertRegExp('%^(set|add).+%', $setter);
301
        }
302
    }
303
304
305
    protected function getGetterNameForField(string $fieldName, string $type): string
306
    {
307
        if ($type === 'boolean') {
308
            return $this->codeHelper->getGetterMethodNameForBoolean($fieldName);
309
        }
310
311
        return 'get'.$fieldName;
312
    }
313
314
    /**
315
     * Loop through entity fields and find unique ones
316
     *
317
     * Then ensure that the unique rule is being enforced as expected
318
     *
319
     * @param ClassMetadataInfo $meta
320
     *
321
     * @throws ConfigException
322
     * @throws \Doctrine\ORM\Mapping\MappingException
323
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
324
     * @throws \ReflectionException
325
     * @throws \Doctrine\ORM\Mapping\MappingException
326
     */
327
    protected function assertUniqueFieldsMustBeUnique(ClassMetadataInfo $meta): void
328
    {
329
        $uniqueFields = [];
330
        foreach ($meta->getFieldNames() as $fieldName) {
331
            $fieldMapping = $meta->getFieldMapping($fieldName);
332
            if (array_key_exists('unique', $fieldMapping) && true === $fieldMapping['unique']) {
333
                $uniqueFields[$fieldName] = $fieldMapping;
334
            }
335
        }
336
        if ([] === $uniqueFields) {
337
            return;
338
        }
339
        $class = $this->getTestedEntityFqn();
340
        $entityManager = $this->getEntityManager();
341
        foreach ($uniqueFields as $fieldName => $fieldMapping) {
342
            $primary = $this->testEntityGenerator->generateEntity($entityManager, $class);
343
            $secondary = $this->testEntityGenerator->generateEntity($entityManager, $class);
344
            $getter = 'get'.$fieldName;
345
            $setter = 'set'.$fieldName;
346
            $primaryValue = $primary->$getter();
347
            $secondary->$setter($primaryValue);
348
            $saver = $this->entitySaverFactory->getSaverForEntity($primary);
349
            $this->expectException(UniqueConstraintViolationException::class);
350
            $saver->saveAll([$primary, $secondary]);
351
        }
352
    }
353
354
    /**
355
     * Check the mapping of our class and the associated entity to make sure it's configured properly on both sides.
356
     * Very easy to get wrong. This is in addition to the standard Schema Validation
357
     *
358
     * @param string $classFqn
359
     * @param array $mapping
360
     * @param EntityManager $entityManager
361
     */
362
    protected function assertCorrectMappings(string $classFqn, array $mapping, EntityManager $entityManager)
363
    {
364
        $pass = false;
365
        $associationFqn = $mapping['targetEntity'];
366
        $associationMeta = $entityManager->getClassMetadata($associationFqn);
367
        $classTraits = $entityManager->getClassMetadata($classFqn)
368
            ->getReflectionClass()
369
            ->getTraits();
370
        $unidirectionalTraitShortNamePrefixes = [
371
            'Has'.$associationFqn::getSingular().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
372
            'Has'.$associationFqn::getPlural().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
373
        ];
374
        foreach ($classTraits as $trait) {
375
            foreach ($unidirectionalTraitShortNamePrefixes as $namePrefix) {
376
                if (0 === \stripos($trait->getShortName(), $namePrefix)) {
377
                    return;
378
                }
379
            }
380
        }
381
        foreach ($associationMeta->getAssociationMappings() as $associationMapping) {
382
            if ($classFqn === $associationMapping['targetEntity']) {
383
                $pass = $this->assertCorrectMapping($mapping, $associationMapping, $classFqn);
384
                break;
385
            }
386
        }
387
        $this->assertTrue($pass, 'Failed finding association mapping to test for '."\n".$mapping['targetEntity']);
388
    }
389
390
    /**
391
     * @param array $mapping
392
     * @param array $associationMapping
393
     * @param string $classFqn
394
     *
395
     * @return bool
396
     */
397
    protected function assertCorrectMapping(array $mapping, array $associationMapping, string $classFqn): bool
398
    {
399
        if (empty($mapping['joinTable'])) {
400
            $this->assertArrayNotHasKey(
401
                'joinTable',
402
                $associationMapping,
403
                $classFqn.' join table is empty,
404
                        but association '.$mapping['targetEntity'].' join table is not empty'
405
            );
406
407
            return true;
408
        }
409
        $this->assertNotEmpty(
410
            $associationMapping['joinTable'],
411
            "$classFqn joinTable is set to ".$mapping['joinTable']['name']
412
            ." \n association ".$mapping['targetEntity'].' join table is empty'
413
        );
414
        $this->assertSame(
415
            $mapping['joinTable']['name'],
416
            $associationMapping['joinTable']['name'],
417
            "join tables not the same: \n * $classFqn = ".$mapping['joinTable']['name']
418
            ." \n * association ".$mapping['targetEntity']
419
            .' = '.$associationMapping['joinTable']['name']
420
        );
421
        $this->assertArrayHasKey(
422
            'inverseJoinColumns',
423
            $associationMapping['joinTable'],
424
            "join table join columns not the same: \n * $classFqn joinColumn = "
425
            .$mapping['joinTable']['joinColumns'][0]['name']
426
            ." \n * association ".$mapping['targetEntity']
427
            .' inverseJoinColumn is not set'
428
        );
429
        $this->assertSame(
430
            $mapping['joinTable']['joinColumns'][0]['name'],
431
            $associationMapping['joinTable']['inverseJoinColumns'][0]['name'],
432
            "join table join columns not the same: \n * $classFqn joinColumn = "
433
            .$mapping['joinTable']['joinColumns'][0]['name']
434
            ." \n * association ".$mapping['targetEntity']
435
            .' inverseJoinColumn = '.$associationMapping['joinTable']['inverseJoinColumns'][0]['name']
436
        );
437
        $this->assertSame(
438
            $mapping['joinTable']['inverseJoinColumns'][0]['name'],
439
            $associationMapping['joinTable']['joinColumns'][0]['name'],
440
            "join table join columns  not the same: \n * $classFqn inverseJoinColumn = "
441
            .$mapping['joinTable']['inverseJoinColumns'][0]['name']
442
            ." \n * association ".$mapping['targetEntity'].' joinColumn = '
443
            .$associationMapping['joinTable']['joinColumns'][0]['name']
444
        );
445
446
        return true;
447
    }
448
449
450
    protected function validateEntity(EntityInterface $entity): void
451
    {
452
        $entity->injectValidator($this->entityValidatorFactory->getEntityValidator());
453
        $entity->validate();
454
    }
455
456
457
    /**
458
     * Get the fully qualified name of the Entity we are testing,
459
     * assumes EntityNameTest as the entity class short name
460
     *
461
     * @return string
462
     */
463
    protected function getTestedEntityFqn(): string
464
    {
465
        if (null === $this->testedEntityFqn) {
466
            $this->testedEntityFqn = \substr(static::class, 0, -4);
467
        }
468
469
        return $this->testedEntityFqn;
470
    }
471
472
473
    /**
474
     * Get a \ReflectionClass for the currently tested Entity
475
     *
476
     * @return \ts\Reflection\ReflectionClass
477
     * @throws \ReflectionException
478
     */
479
    protected function getTestedEntityReflectionClass(): \ts\Reflection\ReflectionClass
480
    {
481
        if (null === $this->testedEntityReflectionClass) {
482
            $this->testedEntityReflectionClass = new \ts\Reflection\ReflectionClass(
483
                $this->getTestedEntityFqn()
484
            );
485
        }
486
487
        return $this->testedEntityReflectionClass;
488
    }
489
}
490