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 (#29)
by Ross
03:51
created

AbstractEntityTest::getSaver()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\DoctrineStaticMeta\Entity;
4
5
use Doctrine\Common\Cache\ArrayCache;
6
use Doctrine\Common\Collections\ArrayCollection;
7
use Doctrine\ORM\EntityManager;
8
use Doctrine\ORM\Events;
9
use Doctrine\ORM\Tools\SchemaValidator;
10
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\Generator\RelationsGenerator;
11
use EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\NamespaceHelper;
12
use EdmondsCommerce\DoctrineStaticMeta\Config;
13
use EdmondsCommerce\DoctrineStaticMeta\ConfigInterface;
14
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Interfaces\Attribute\IpAddressFieldInterface;
15
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Interfaces\Attribute\QtyFieldInterface;
16
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Interfaces\Person\EmailFieldInterface;
17
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Interfaces\PrimaryKey\IdFieldInterface;
18
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\ValidateInterface;
19
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\AbstractSaver;
20
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\SaverValidationListener;
21
use EdmondsCommerce\DoctrineStaticMeta\Entity\Validation\EntityValidator;
22
use EdmondsCommerce\DoctrineStaticMeta\Entity\Validation\EntityValidatorFactory;
23
use EdmondsCommerce\DoctrineStaticMeta\EntityManager\EntityManagerFactory;
24
use EdmondsCommerce\DoctrineStaticMeta\Exception\ConfigException;
25
use EdmondsCommerce\DoctrineStaticMeta\Exception\ValidationException;
26
use EdmondsCommerce\DoctrineStaticMeta\SimpleEnv;
27
use Faker;
28
use Faker\ORM\Doctrine\Populator;
29
30
/**
31
 * Class AbstractEntityTest
32
 *
33
 * @package EdmondsCommerce\DoctrineStaticMeta\Entity
34
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
35
 */
36
abstract class AbstractEntityTest extends AbstractTest
37
{
38
    public const GET_ENTITY_MANAGER_FUNCTION_NAME = 'dsmGetEntityManagerFactory';
39
40
    protected $testedEntityFqn;
41
42
    protected $testedEntityReflectionClass;
43
44
    protected $generator;
45
46
    /**
47
     * @var EntityValidator
48
     */
49
    protected $entityValidator;
50
51
    /**
52
     * @var EntityValidatorFactory
53
     */
54
    protected $entityValidatorFactory;
55
56
    /**
57
     * @var EntityManager
58
     */
59
    protected $entityManager;
60
61
    protected $schemaErrors = [];
62
63
    protected function setup()
64
    {
65
        $this->getEntityManager(true);
66
        $this->entityValidatorFactory = new EntityValidatorFactory(new ArrayCache());
67
        $this->entityValidator = $this->entityValidatorFactory->getEntityValidator();
68
        $this->getEntityManager()
69
             ->getEventManager()
70
             ->addEventListener(
71
                 [
72
                    Events::onFlush
73
                 ],
74
                 new SaverValidationListener()
75
             );
76
    }
77
78
    /**
79
     * Use Doctrine's standard schema validation to get errors for the whole schema
80
     *
81
     * @param bool $update
82
     *
83
     * @return array
84
     * @throws \Exception
85
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
86
     */
87
    protected function getSchemaErrors(bool $update = false): array
88
    {
89
        if (empty($this->schemaErrors) || true === $update) {
90
            $entityManager      = $this->getEntityManager();
91
            $validator          = new SchemaValidator($entityManager);
92
            $this->schemaErrors = $validator->validateMapping();
93
        }
94
95
        return $this->schemaErrors;
96
    }
97
98
    /**
99
     * If a global function dsmGetEntityManagerFactory is defined, we use this
100
     *
101
     * Otherwise, we use the standard DevEntityManagerFactory,
102
     * we define a DB name which is the main DB from env but with `_test` suffixed
103
     *
104
     * @param bool $new
105
     *
106
     * @return EntityManager
107
     * @throws ConfigException
108
     * @throws \Exception
109
     * @SuppressWarnings(PHPMD)
110
     */
111
    protected function getEntityManager(bool $new = false): EntityManager
112
    {
113
        if (null === $this->entityManager || true === $new) {
114
            if (\function_exists(self::GET_ENTITY_MANAGER_FUNCTION_NAME)) {
115
                $this->entityManager = \call_user_func(self::GET_ENTITY_MANAGER_FUNCTION_NAME);
116
            } else {
117
                SimpleEnv::setEnv(Config::getProjectRootDirectory().'/.env');
118
                $testConfig                                 = $_SERVER;
119
                $testConfig[ConfigInterface::PARAM_DB_NAME] = $_SERVER[ConfigInterface::PARAM_DB_NAME].'_test';
120
                $config                                 = new Config($testConfig);
121
                $this->entityManager                    = (new EntityManagerFactory(new ArrayCache()))
122
                    ->getEntityManager($config);
123
            }
124
        }
125
126
        return $this->entityManager;
127
    }
128
129
    /**
130
     * @param EntityManager $entityManager
131
     * @param IdFieldInterface $entity
132
     * @return AbstractSaver
133
     * @throws \ReflectionException
134
     */
135
    protected function getSaver(
136
        EntityManager $entityManager,
137
        IdFieldInterface $entity
138
    ): AbstractSaver {
139
        $saverFqn = $this->getSaverFqn($entity);
140
        return new $saverFqn($entityManager, $this->entityValidatorFactory);
141
    }
142
143
    /**
144
     * Use Doctrine's built in schema validation tool to catch issues
145
     */
146
    public function testValidateSchema()
147
    {
148
        $errors  = $this->getSchemaErrors();
149
        $class   = $this->getTestedEntityFqn();
150
        $message = '';
151
        if (isset($errors[$class])) {
152
            $message = "Failed ORM Validate Schema:\n";
153
            foreach ($errors[$class] as $err) {
154
                $message .= "\n * $err \n";
155
            }
156
        }
157
        $this->assertEmpty($message);
158
    }
159
160
161
    /**
162
     * @param string        $class
163
     * @param int|string    $id
164
     * @param EntityManager $entityManager
165
     *
166
     * @return IdFieldInterface|null
167
     */
168
    protected function loadEntity(string $class, $id, EntityManager $entityManager): ?IdFieldInterface
169
    {
170
        return $entityManager->getRepository($class)->find($id);
171
    }
172
173
    /**
174
     * Test that we have correctly generated an instance of our test entity
175
     */
176
    public function testGeneratedCreate()
177
    {
178
        $this->markTestIncomplete(
179
            'We need to configure Faker to populate the fields correctly now they are being validated'
180
        );
181
182
        $entityManager = $this->getEntityManager();
183
        $class         = $this->getTestedEntityFqn();
184
        $generated     = $this->generateEntity($class);
185
        $saver         = $this->getSaver($entityManager, $generated);
186
        $this->addAssociationEntities($entityManager, $generated);
187
        $this->assertInstanceOf($class, $generated);
188
        $this->validateEntity($generated);
189
        $meta = $entityManager->getClassMetadata($class);
190
        foreach ($meta->getFieldNames() as $f) {
191
            $method = 'get'.$f;
192
            $reflectionMethod = new \ReflectionMethod($generated, $method);
193
            if ($reflectionMethod->hasReturnType()) {
194
                $returnType = $reflectionMethod->getReturnType();
195
                $allowsNull = $returnType->allowsNull();
196
                if ($allowsNull) {
197
                    // As we can't assert anything here so simply call
198
                    // the method and allow the type hint to raise any
199
                    // errors.
200
                    $generated->$method();
201
                    continue;
202
                }
203
            }
204
            $this->assertNotEmpty($generated->$method(), "$f getter returned empty");
205
        }
206
        $saver->save($generated);
207
        $entityManager = $this->getEntityManager(true);
208
        $loaded        = $this->loadEntity($class, $generated->getId(), $entityManager);
209
        $this->assertInstanceOf($class, $loaded);
210
        $this->validateEntity($loaded);
211
        foreach ($meta->getAssociationMappings() as $mapping) {
212
            $getter = 'get'.$mapping['fieldName'];
213
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
214
                $collection = $loaded->$getter()->toArray();
215
                $this->assertNotEmpty(
216
                    $collection,
217
                    'Failed to load the collection of the associated entity ['.$mapping['fieldName']
218
                    .'] from the generated '.$class
219
                    .', make sure you have reciprocal adding of the association'
220
                );
221
                $this->assertCorrectMapping($class, $mapping, $entityManager);
222
                continue;
223
            }
224
            $association = $loaded->$getter();
225
            $this->assertNotEmpty(
226
                $association,
227
                'Failed to load the associated entity: ['.$mapping['fieldName']
228
                .'] from the generated '.$class
229
            );
230
            $this->assertNotEmpty(
231
                $association->getId(),
232
                'Failed to get the ID of the associated entity: ['.$mapping['fieldName']
233
                .'] from the generated '.$class
234
            );
235
        }
236
    }
237
238
    public function testIpAddressFieldValidation()
239
    {
240
        $entityManager = $this->getEntityManager();
241
        $class         = $this->getTestedEntityFqn();
242
        $entity        = new $class();
243
244
        if ($this->shouldIValidateTheEntity($entity) === false) {
245
            return;
246
        }
247
248
        $saver = $this->getSaver($entityManager, $entity);
249
        $this->addAssociationEntities($entityManager, $entity);
250
        $this->assertInstanceOf($class, $entity);
251
252
        $this->expectException(ValidationException::class);
253
254
        $entity->setIpAddress('invalid_ip');
255
        $saver->save($entity);
256
    }
257
258
    public function testEmailFieldValidation()
259
    {
260
        $entityManager = $this->getEntityManager();
261
        $class         = $this->getTestedEntityFqn();
262
        $entity        = new $class();
263
264
        if ($this->shouldIValidateTheEntity($entity) === false) {
265
            return;
266
        }
267
268
        $saver = $this->getSaver($entityManager, $entity);
269
        $this->addAssociationEntities($entityManager, $entity);
270
        $this->assertInstanceOf($class, $entity);
271
272
        $this->expectException(ValidationException::class);
273
274
        $entity->setEmail('invalid_email');
275
        $saver->save($entity);
276
    }
277
278
    private function shouldIValidateTheEntity(IdFieldInterface $entity)
279
    {
280
        if (! $entity instanceof ValidateInterface || ! $entity instanceof EmailFieldInterface) {
281
            /** Need to carry out a test even if we aren't testing anything */
282
            $this->assertTrue(true);
283
            return false;
284
        }
285
286
        return true;
287
    }
288
289
    /**
290
     * Check the mapping of our class and the associated entity to make sure it's configured properly on both sides.
291
     * Very easy to get wrong. This is in addition to the standard Schema Validation
292
     *
293
     * @param string        $classFqn
294
     * @param array         $mapping
295
     * @param EntityManager $entityManager
296
     */
297
    protected function assertCorrectMapping(string $classFqn, array $mapping, EntityManager $entityManager)
298
    {
299
        $pass                                 = false;
300
        $associationFqn                       = $mapping['targetEntity'];
301
        $associationMeta                      = $entityManager->getClassMetadata($associationFqn);
302
        $classTraits                          = $entityManager->getClassMetadata($classFqn)
303
                                                              ->getReflectionClass()
304
                                                              ->getTraits();
305
        $unidirectionalTraitShortNamePrefixes = [
306
            'Has'.$associationFqn::getSingular().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
307
            'Has'.$associationFqn::getPlural().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
308
        ];
309
        foreach ($classTraits as $trait) {
310
            foreach ($unidirectionalTraitShortNamePrefixes as $namePrefix) {
311
                if (0 === \stripos($trait->getShortName(), $namePrefix)) {
312
                    return;
313
                }
314
            }
315
        }
316
        foreach ($associationMeta->getAssociationMappings() as $associationMapping) {
317
            if ($classFqn === $associationMapping['targetEntity']) {
318
                if (empty($mapping['joinTable'])) {
319
                    $this->assertArrayNotHasKey(
320
                        'joinTable',
321
                        $associationMapping,
322
                        $classFqn.' join table is empty,
323
                        but association '.$mapping['targetEntity'].' join table is not empty'
324
                    );
325
                    $pass = true;
326
                    break;
327
                }
328
                $this->assertNotEmpty(
329
                    $associationMapping['joinTable'],
330
                    "$classFqn joinTable is set to ".$mapping['joinTable']['name']
331
                    ." \n association ".$mapping['targetEntity'].' join table is empty'
332
                );
333
                $this->assertSame(
334
                    $mapping['joinTable']['name'],
335
                    $associationMapping['joinTable']['name'],
336
                    "join tables not the same: \n * $classFqn = ".$mapping['joinTable']['name']
337
                    ." \n * association ".$mapping['targetEntity']
338
                    .' = '.$associationMapping['joinTable']['name']
339
                );
340
                $this->assertArrayHasKey(
341
                    'inverseJoinColumns',
342
                    $associationMapping['joinTable'],
343
                    "join table join columns not the same: \n * $classFqn joinColumn = "
344
                    .$mapping['joinTable']['joinColumns'][0]['name']
345
                    ." \n * association ".$mapping['targetEntity']
346
                    .' inverseJoinColumn is not set'
347
                );
348
                $this->assertSame(
349
                    $mapping['joinTable']['joinColumns'][0]['name'],
350
                    $associationMapping['joinTable']['inverseJoinColumns'][0]['name'],
351
                    "join table join columns not the same: \n * $classFqn joinColumn = "
352
                    .$mapping['joinTable']['joinColumns'][0]['name']
353
                    ." \n * association ".$mapping['targetEntity']
354
                    .' inverseJoinColumn = '.$associationMapping['joinTable']['inverseJoinColumns'][0]['name']
355
                );
356
                $this->assertSame(
357
                    $mapping['joinTable']['inverseJoinColumns'][0]['name'],
358
                    $associationMapping['joinTable']['joinColumns'][0]['name'],
359
                    "join table join columns  not the same: \n * $classFqn inverseJoinColumn = "
360
                    .$mapping['joinTable']['inverseJoinColumns'][0]['name']
361
                    ." \n * association ".$mapping['targetEntity'].' joinColumn = '
362
                    .$associationMapping['joinTable']['joinColumns'][0]['name']
363
                );
364
                $pass = true;
365
                break;
366
            }
367
        }
368
        $this->assertTrue($pass, 'Failed finding association mapping to test for '."\n".$mapping['targetEntity']);
369
    }
370
371
    /**
372
     * @param string $class
373
     *
374
     * @return IdFieldInterface
375
     * @throws ConfigException
376
     * @throws \Exception
377
     * @SuppressWarnings(PHPMD.StaticAccess)
378
     */
379
    protected function generateEntity(string $class): IdFieldInterface
380
    {
381
        $entityManager = $this->getEntityManager();
382
        if (!$this->generator) {
383
            $this->generator = Faker\Factory::create();
384
        }
385
        $customColumnFormatters = $this->generateAssociationColumnFormatters($entityManager, $class);
386
        $populator              = new Populator($this->generator, $entityManager);
387
        $populator->addEntity($class, 1, $customColumnFormatters);
388
389
        $entity = $populator->execute()[$class][0];
390
391
        return $entity;
392
    }
393
394
    protected function validateEntity($entity): void
395
    {
396
        $errors = $this->entityValidator->setEntity($entity)->validate();
397
        $this->assertEmpty($errors);
398
    }
399
400
    /**
401
     * @param EntityManager $entityManager
402
     * @param string        $class
403
     *
404
     * @return array
405
     */
406
    protected function generateAssociationColumnFormatters(EntityManager $entityManager, string $class): array
407
    {
408
        $return   = [];
409
        $meta     = $entityManager->getClassMetadata($class);
410
        $mappings = $meta->getAssociationMappings();
411
        if (empty($mappings)) {
412
            return $return;
413
        }
414
        foreach ($mappings as $mapping) {
415
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
416
                $return[$mapping['fieldName']] = new ArrayCollection();
417
                continue;
418
            }
419
            $return[$mapping['fieldName']] = null;
420
        }
421
422
        return $return;
423
    }
424
425
    /**
426
     * @param EntityManager    $entityManager
427
     * @param IdFieldInterface $generated
428
     *
429
     * @throws \Doctrine\ORM\ORMException
430
     * @throws ConfigException
431
     * @throws \Exception
432
     * @throws \ReflectionException
433
     * @SuppressWarnings(PHPMD.ElseExpression)
434
     */
435
    protected function addAssociationEntities(
436
        EntityManager $entityManager,
437
        IdFieldInterface $generated
438
    ) {
439
        $entityReflection = $this->getTestedEntityReflectionClass();
440
        $class            = $entityReflection->getName();
441
        $meta             = $entityManager->getClassMetadata($class);
442
        $mappings         = $meta->getAssociationMappings();
443
        if (empty($mappings)) {
444
            return;
445
        }
446
        $namespaceHelper = new NamespaceHelper();
447
        $methods         = array_map('strtolower', get_class_methods($generated));
448
        foreach ($mappings as $mapping) {
449
            $mappingEntityClass = $mapping['targetEntity'];
450
            $mappingEntity      = $this->generateEntity($mappingEntityClass);
451
            $errorMessage       = "Error adding association entity $mappingEntityClass to $class: %s";
452
            $saver = $this->getSaver($entityManager, $mappingEntity);
453
            $saver->save($mappingEntity);
454
            $mappingEntityPluralInterface = $namespaceHelper->getHasPluralInterfaceFqnForEntity($mappingEntityClass);
455
            if ($entityReflection->implementsInterface($mappingEntityPluralInterface)) {
456
                $this->assertEquals(
457
                    $mappingEntityClass::getPlural(),
458
                    $mapping['fieldName'],
459
                    sprintf($errorMessage, ' mapping should be plural')
460
                );
461
                $method = 'add'.$mappingEntityClass::getSingular();
462
            } else {
463
                $this->assertEquals(
464
                    $mappingEntityClass::getSingular(),
465
                    $mapping['fieldName'],
466
                    sprintf($errorMessage, ' mapping should be singular')
467
                );
468
                $method = 'set'.$mappingEntityClass::getSingular();
469
            }
470
            $this->assertContains(
471
                strtolower($method),
472
                $methods,
473
                sprintf($errorMessage, $method.' method is not defined')
474
            );
475
            $generated->$method($mappingEntity);
476
        }
477
    }
478
479
    /**
480
     * Get the fully qualified name of the Entity we are testing,
481
     * assumes EntityNameTest as the entity class short name
482
     *
483
     * @return string
484
     * @throws \ReflectionException
485
     */
486
    protected function getTestedEntityFqn(): string
487
    {
488
        if (!$this->testedEntityFqn) {
489
            $ref                   = new \ReflectionClass($this);
490
            $namespace             = $ref->getNamespaceName();
491
            $shortName             = $ref->getShortName();
492
            $className             = substr($shortName, 0, strpos($shortName, 'Test'));
493
            $this->testedEntityFqn = $namespace.'\\'.$className;
494
        }
495
496
        return $this->testedEntityFqn;
497
    }
498
499
    /**
500
     * Get the fully qualified name of the saver for the entity we are testing.
501
     *
502
     * @return string
503
     * @throws \ReflectionException
504
     */
505
    protected function getSaverFqn(
506
        IdFieldInterface $entity
507
    ): string {
508
        $ref             = new \ReflectionClass($entity);
509
        $entityNamespace = $ref->getNamespaceName();
510
        $saverNamespace  = \str_replace(
511
            'Entities',
512
            'Entity\\Savers',
513
            $entityNamespace
514
        );
515
        $shortName = $ref->getShortName();
516
        return $saverNamespace.'\\'.$shortName.'Saver';
517
    }
518
519
    /**
520
     * Get a \ReflectionClass for the currently tested Entity
521
     *
522
     * @return \ReflectionClass
523
     * @throws \ReflectionException
524
     */
525
    protected function getTestedEntityReflectionClass(): \ReflectionClass
526
    {
527
        if (!$this->testedEntityReflectionClass) {
528
            $this->testedEntityReflectionClass = new \ReflectionClass($this->getTestedEntityFqn());
529
        }
530
531
        return $this->testedEntityReflectionClass;
532
    }
533
}
534