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.
Test Failed
Push — master ( 63e980...f7805d )
by joseph
07:14
created

AbstractEntityTest::testEmailFieldValidation()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
cc 3
eloc 14
nc 2
nop 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
        $entityManager = $this->getEntityManager();
179
        $class         = $this->getTestedEntityFqn();
180
        $generated     = $this->generateEntity($class);
181
        $saver         = $this->getSaver($entityManager, $generated);
182
        $this->addAssociationEntities($entityManager, $generated);
183
        $this->assertInstanceOf($class, $generated);
184
        $this->validateEntity($generated);
185
        $meta = $entityManager->getClassMetadata($class);
186
        foreach ($meta->getFieldNames() as $f) {
187
            $method = 'get'.$f;
188
            $reflectionMethod = new \ReflectionMethod($generated, $method);
189
            if ($reflectionMethod->hasReturnType()) {
190
                $returnType = $reflectionMethod->getReturnType();
191
                $allowsNull = $returnType->allowsNull();
192
                if ($allowsNull) {
193
                    // As we can't assert anything here so simply call
194
                    // the method and allow the type hint to raise any
195
                    // errors.
196
                    $generated->$method();
197
                    continue;
198
                }
199
            }
200
            $this->assertNotEmpty($generated->$method(), "$f getter returned empty");
201
        }
202
        $saver->save($generated);
203
        $entityManager = $this->getEntityManager(true);
204
        $loaded        = $this->loadEntity($class, $generated->getId(), $entityManager);
205
        $this->assertInstanceOf($class, $loaded);
206
        $this->validateEntity($loaded);
207
        foreach ($meta->getAssociationMappings() as $mapping) {
208
            $getter = 'get'.$mapping['fieldName'];
209
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
210
                $collection = $loaded->$getter()->toArray();
211
                $this->assertNotEmpty(
212
                    $collection,
213
                    'Failed to load the collection of the associated entity ['.$mapping['fieldName']
214
                    .'] from the generated '.$class
215
                    .', make sure you have reciprocal adding of the association'
216
                );
217
                $this->assertCorrectMapping($class, $mapping, $entityManager);
218
                continue;
219
            }
220
            $association = $loaded->$getter();
221
            $this->assertNotEmpty(
222
                $association,
223
                'Failed to load the associated entity: ['.$mapping['fieldName']
224
                .'] from the generated '.$class
225
            );
226
            $this->assertNotEmpty(
227
                $association->getId(),
228
                'Failed to get the ID of the associated entity: ['.$mapping['fieldName']
229
                .'] from the generated '.$class
230
            );
231
        }
232
    }
233
234
    public function testIpAddressFieldValidation()
235
    {
236
        $entityManager = $this->getEntityManager();
237
        $class         = $this->getTestedEntityFqn();
238
        $entity        = $this->generateEntity($class);
239
240
        if (! $entity instanceof ValidateInterface
241
            || ! $entity instanceof IpAddressFieldInterface
242
        ) {
243
            $this->assertTrue(true);
244
            return;
245
        }
246
247
        $saver = $this->getSaver($entityManager, $entity);
248
        $this->addAssociationEntities($entityManager, $entity);
249
        $this->assertInstanceOf($class, $entity);
250
251
        $this->expectException(ValidationException::class);
252
253
        $entity->setIpAddress('invalid_ip');
254
        $saver->save($entity);
255
    }
256
257
    public function testEmailFieldValidation()
258
    {
259
        $entityManager = $this->getEntityManager();
260
        $class         = $this->getTestedEntityFqn();
261
        $entity        = $this->generateEntity($class);
262
263
        if (! $entity instanceof ValidateInterface
264
            || ! $entity instanceof EmailFieldInterface
265
        ) {
266
            $this->assertTrue(true);
267
            return;
268
        }
269
270
        $saver = $this->getSaver($entityManager, $entity);
271
        $this->addAssociationEntities($entityManager, $entity);
272
        $this->assertInstanceOf($class, $entity);
273
274
        $this->expectException(ValidationException::class);
275
276
        $entity->setEmail('invalid_email');
277
        $saver->save($entity);
278
    }
279
280
    /**
281
     * Check the mapping of our class and the associated entity to make sure it's configured properly on both sides.
282
     * Very easy to get wrong. This is in addition to the standard Schema Validation
283
     *
284
     * @param string        $classFqn
285
     * @param array         $mapping
286
     * @param EntityManager $entityManager
287
     */
288
    protected function assertCorrectMapping(string $classFqn, array $mapping, EntityManager $entityManager)
289
    {
290
        $pass                                 = false;
291
        $associationFqn                       = $mapping['targetEntity'];
292
        $associationMeta                      = $entityManager->getClassMetadata($associationFqn);
293
        $classTraits                          = $entityManager->getClassMetadata($classFqn)
294
                                                              ->getReflectionClass()
295
                                                              ->getTraits();
296
        $unidirectionalTraitShortNamePrefixes = [
297
            'Has'.$associationFqn::getSingular().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
298
            'Has'.$associationFqn::getPlural().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
299
        ];
300
        foreach ($classTraits as $trait) {
301
            foreach ($unidirectionalTraitShortNamePrefixes as $namePrefix) {
302
                if (0 === \stripos($trait->getShortName(), $namePrefix)) {
303
                    return;
304
                }
305
            }
306
        }
307
        foreach ($associationMeta->getAssociationMappings() as $associationMapping) {
308
            if ($classFqn === $associationMapping['targetEntity']) {
309
                if (empty($mapping['joinTable'])) {
310
                    $this->assertArrayNotHasKey(
311
                        'joinTable',
312
                        $associationMapping,
313
                        $classFqn.' join table is empty,
314
                        but association '.$mapping['targetEntity'].' join table is not empty'
315
                    );
316
                    $pass = true;
317
                    break;
318
                }
319
                $this->assertNotEmpty(
320
                    $associationMapping['joinTable'],
321
                    "$classFqn joinTable is set to ".$mapping['joinTable']['name']
322
                    ." \n association ".$mapping['targetEntity'].' join table is empty'
323
                );
324
                $this->assertSame(
325
                    $mapping['joinTable']['name'],
326
                    $associationMapping['joinTable']['name'],
327
                    "join tables not the same: \n * $classFqn = ".$mapping['joinTable']['name']
328
                    ." \n * association ".$mapping['targetEntity']
329
                    .' = '.$associationMapping['joinTable']['name']
330
                );
331
                $this->assertArrayHasKey(
332
                    'inverseJoinColumns',
333
                    $associationMapping['joinTable'],
334
                    "join table join columns not the same: \n * $classFqn joinColumn = "
335
                    .$mapping['joinTable']['joinColumns'][0]['name']
336
                    ." \n * association ".$mapping['targetEntity']
337
                    .' inverseJoinColumn is not set'
338
                );
339
                $this->assertSame(
340
                    $mapping['joinTable']['joinColumns'][0]['name'],
341
                    $associationMapping['joinTable']['inverseJoinColumns'][0]['name'],
342
                    "join table join columns not the same: \n * $classFqn joinColumn = "
343
                    .$mapping['joinTable']['joinColumns'][0]['name']
344
                    ." \n * association ".$mapping['targetEntity']
345
                    .' inverseJoinColumn = '.$associationMapping['joinTable']['inverseJoinColumns'][0]['name']
346
                );
347
                $this->assertSame(
348
                    $mapping['joinTable']['inverseJoinColumns'][0]['name'],
349
                    $associationMapping['joinTable']['joinColumns'][0]['name'],
350
                    "join table join columns  not the same: \n * $classFqn inverseJoinColumn = "
351
                    .$mapping['joinTable']['inverseJoinColumns'][0]['name']
352
                    ." \n * association ".$mapping['targetEntity'].' joinColumn = '
353
                    .$associationMapping['joinTable']['joinColumns'][0]['name']
354
                );
355
                $pass = true;
356
                break;
357
            }
358
        }
359
        $this->assertTrue($pass, 'Failed finding association mapping to test for '."\n".$mapping['targetEntity']);
360
    }
361
362
    /**
363
     * @param string $class
364
     *
365
     * @return IdFieldInterface
366
     * @throws ConfigException
367
     * @throws \Exception
368
     * @SuppressWarnings(PHPMD.StaticAccess)
369
     */
370
    protected function generateEntity(string $class): IdFieldInterface
371
    {
372
        $entityManager = $this->getEntityManager();
373
        if (!$this->generator) {
374
            $this->generator = Faker\Factory::create();
375
        }
376
        $customColumnFormatters = $this->generateAssociationColumnFormatters($entityManager, $class);
377
        $populator              = new Populator($this->generator, $entityManager);
378
        $populator->addEntity($class, 1, $customColumnFormatters);
379
380
        $entity = $populator->execute()[$class][0];
381
382
        return $entity;
383
    }
384
385
    protected function validateEntity($entity): void
386
    {
387
        $errors = $this->entityValidator->setEntity($entity)->validate();
388
        $this->assertEmpty($errors);
389
    }
390
391
    /**
392
     * @param EntityManager $entityManager
393
     * @param string        $class
394
     *
395
     * @return array
396
     */
397
    protected function generateAssociationColumnFormatters(EntityManager $entityManager, string $class): array
398
    {
399
        $return   = [];
400
        $meta     = $entityManager->getClassMetadata($class);
401
        $mappings = $meta->getAssociationMappings();
402
        if (empty($mappings)) {
403
            return $return;
404
        }
405
        foreach ($mappings as $mapping) {
406
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
407
                $return[$mapping['fieldName']] = new ArrayCollection();
408
                continue;
409
            }
410
            $return[$mapping['fieldName']] = null;
411
        }
412
413
        return $return;
414
    }
415
416
    /**
417
     * @param EntityManager    $entityManager
418
     * @param IdFieldInterface $generated
419
     *
420
     * @throws \Doctrine\ORM\ORMException
421
     * @throws ConfigException
422
     * @throws \Exception
423
     * @throws \ReflectionException
424
     * @SuppressWarnings(PHPMD.ElseExpression)
425
     */
426
    protected function addAssociationEntities(
427
        EntityManager $entityManager,
428
        IdFieldInterface $generated
429
    ) {
430
        $entityReflection = $this->getTestedEntityReflectionClass();
431
        $class            = $entityReflection->getName();
432
        $meta             = $entityManager->getClassMetadata($class);
433
        $mappings         = $meta->getAssociationMappings();
434
        if (empty($mappings)) {
435
            return;
436
        }
437
        $namespaceHelper = new NamespaceHelper();
438
        $methods         = array_map('strtolower', get_class_methods($generated));
439
        foreach ($mappings as $mapping) {
440
            $mappingEntityClass = $mapping['targetEntity'];
441
            $mappingEntity      = $this->generateEntity($mappingEntityClass);
442
            $errorMessage       = "Error adding association entity $mappingEntityClass to $class: %s";
443
            $saver = $this->getSaver($entityManager, $mappingEntity);
444
            $saver->save($mappingEntity);
445
            $mappingEntityPluralInterface = $namespaceHelper->getHasPluralInterfaceFqnForEntity($mappingEntityClass);
446
            if ($entityReflection->implementsInterface($mappingEntityPluralInterface)) {
447
                $this->assertEquals(
448
                    $mappingEntityClass::getPlural(),
449
                    $mapping['fieldName'],
450
                    sprintf($errorMessage, ' mapping should be plural')
451
                );
452
                $method = 'add'.$mappingEntityClass::getSingular();
453
            } else {
454
                $this->assertEquals(
455
                    $mappingEntityClass::getSingular(),
456
                    $mapping['fieldName'],
457
                    sprintf($errorMessage, ' mapping should be singular')
458
                );
459
                $method = 'set'.$mappingEntityClass::getSingular();
460
            }
461
            $this->assertContains(
462
                strtolower($method),
463
                $methods,
464
                sprintf($errorMessage, $method.' method is not defined')
465
            );
466
            $generated->$method($mappingEntity);
467
        }
468
    }
469
470
    /**
471
     * Get the fully qualified name of the Entity we are testing,
472
     * assumes EntityNameTest as the entity class short name
473
     *
474
     * @return string
475
     * @throws \ReflectionException
476
     */
477
    protected function getTestedEntityFqn(): string
478
    {
479
        if (!$this->testedEntityFqn) {
480
            $ref                   = new \ReflectionClass($this);
481
            $namespace             = $ref->getNamespaceName();
482
            $shortName             = $ref->getShortName();
483
            $className             = substr($shortName, 0, strpos($shortName, 'Test'));
484
            $this->testedEntityFqn = $namespace.'\\'.$className;
485
        }
486
487
        return $this->testedEntityFqn;
488
    }
489
490
    /**
491
     * Get the fully qualified name of the saver for the entity we are testing.
492
     *
493
     * @return string
494
     * @throws \ReflectionException
495
     */
496
    protected function getSaverFqn(
497
        IdFieldInterface $entity
498
    ): string {
499
        $ref             = new \ReflectionClass($entity);
500
        $entityNamespace = $ref->getNamespaceName();
501
        $saverNamespace  = \str_replace(
502
            'Entities',
503
            'Entity\\Savers',
504
            $entityNamespace
505
        );
506
        $shortName = $ref->getShortName();
507
        return $saverNamespace.'\\'.$shortName.'Saver';
508
    }
509
510
    /**
511
     * Get a \ReflectionClass for the currently tested Entity
512
     *
513
     * @return \ReflectionClass
514
     * @throws \ReflectionException
515
     */
516
    protected function getTestedEntityReflectionClass(): \ReflectionClass
517
    {
518
        if (!$this->testedEntityReflectionClass) {
519
            $this->testedEntityReflectionClass = new \ReflectionClass($this->getTestedEntityFqn());
520
        }
521
522
        return $this->testedEntityReflectionClass;
523
    }
524
}
525