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 (#51)
by joseph
102:38 queued 99:24
created

getTestedEntityReflectionClass()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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