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 (#45)
by joseph
05:28 queued 02:25
created

AbstractEntityTest::generateColumnFormatters()   D

Complexity

Conditions 9
Paths 30

Size

Total Lines 37
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
dl 0
loc 37
rs 4.909
c 0
b 0
f 0
eloc 26
nc 30
nop 2
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\DBAL\Exception\UniqueConstraintViolationException;
8
use Doctrine\ORM\EntityManager;
9
use Doctrine\ORM\Mapping\ClassMetadataInfo;
10
use Doctrine\ORM\Tools\SchemaValidator;
11
use Doctrine\ORM\Utility\PersisterHelper;
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\Fields\FakerData\Attribute\IpAddressFakerData;
17
use EdmondsCommerce\DoctrineStaticMeta\Entity\Fields\Interfaces\Attribute\IpAddressFieldInterface;
18
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\EntityInterface;
19
use EdmondsCommerce\DoctrineStaticMeta\Entity\Interfaces\Validation\EntityValidatorInterface;
20
use EdmondsCommerce\DoctrineStaticMeta\Entity\Savers\AbstractSaver;
21
use EdmondsCommerce\DoctrineStaticMeta\Entity\Validation\EntityValidatorFactory;
22
use EdmondsCommerce\DoctrineStaticMeta\EntityManager\EntityManagerFactory;
23
use EdmondsCommerce\DoctrineStaticMeta\Exception\ConfigException;
24
use EdmondsCommerce\DoctrineStaticMeta\SimpleEnv;
25
use Faker;
26
use Faker\ORM\Doctrine\Populator;
27
use Symfony\Component\Validator\Mapping\Cache\DoctrineCache;
28
29
/**
30
 * Class AbstractEntityTest
31
 *
32
 * This abstract test is designed to give you a good level of test coverage for your entities without any work required.
33
 *
34
 * You should extend the test with methods that test your specific business logic, your validators and anything else.
35
 *
36
 * You can override the methods, properties and constants as you see fit.
37
 *
38
 * @package EdmondsCommerce\DoctrineStaticMeta\Entity
39
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
40
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
41
 */
42
abstract class AbstractEntityTest extends AbstractTest
43
{
44
    public const GET_ENTITY_MANAGER_FUNCTION_NAME = 'dsmGetEntityManagerFactory';
45
46
    /**
47
     * The fully qualified name of the Entity being tested, as calculated by the test class name
48
     *
49
     * @var string
50
     */
51
    protected $testedEntityFqn;
52
53
    /**
54
     * Reflection of the tested entity
55
     *
56
     * @var \ReflectionClass
57
     */
58
    protected $testedEntityReflectionClass;
59
60
    /**
61
     * @var Faker\Generator
62
     */
63
    protected $generator;
64
65
    /**
66
     * @var EntityValidatorInterface
67
     */
68
    protected $entityValidator;
69
70
    /**
71
     * @var EntityManager
72
     */
73
    protected $entityManager;
74
75
    /**
76
     * @var array
77
     */
78
    protected $schemaErrors = [];
79
80
    /**
81
     * Standard library faker data provider FQNs
82
     *
83
     * This const should be overridden in your child class and extended with any project specific field data providers
84
     * in addition to the standard library
85
     *
86
     * The key is the column/property name and the value is the FQN for the data provider
87
     */
88
    public const FAKER_DATA_PROVIDERS = [
89
        IpAddressFieldInterface::PROP_IP_ADDRESS => IpAddressFakerData::class,
90
    ];
91
92
    /**
93
     * Faker can be seeded with a number which makes the generation deterministic
94
     * This helps to avoid tests that fail randomly
95
     * If you do want randomness, override this and set it to null
96
     */
97
    public const SEED = 100111991161141051101013211511697116105993210910111697;
98
99
    /**
100
     * A cache of instantiated column data providers
101
     *
102
     * @var array
103
     */
104
    protected $fakerDataProviderObjects = [];
105
106
107
    /**
108
     * These two are used to keep track of unique fields and ensure we dont accidently make apply none unique values
109
     *
110
     * @var array
111
     */
112
    private static $uniqueStrings = [];
113
    private static $uniqueInt;
114
115
    /**
116
     * @throws ConfigException
117
     * @throws \Exception
118
     * @SuppressWarnings(PHPMD.StaticAccess)
119
     */
120
    protected function setup()
121
    {
122
        $this->getEntityManager(true);
123
        $this->entityValidator = (
124
        new EntityValidatorFactory(new DoctrineCache(new ArrayCache()))
125
        )->getEntityValidator();
126
        $this->generator       = Faker\Factory::create();
127
        if (null !== static::SEED) {
0 ignored issues
show
introduced by
The condition null !== static::SEED is always true.
Loading history...
128
            $this->generator->seed(static::SEED);
129
        }
130
    }
131
132
    /**
133
     * Use Doctrine's standard schema validation to get errors for the whole schema
134
     *
135
     * @param bool $update
136
     *
137
     * @return array
138
     * @throws \Exception
139
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
140
     */
141
    protected function getSchemaErrors(bool $update = false): array
142
    {
143
        if (empty($this->schemaErrors) || true === $update) {
144
            $entityManager      = $this->getEntityManager();
145
            $validator          = new SchemaValidator($entityManager);
146
            $this->schemaErrors = $validator->validateMapping();
147
        }
148
149
        return $this->schemaErrors;
150
    }
151
152
    /**
153
     * If a global function dsmGetEntityManagerFactory is defined, we use this
154
     *
155
     * Otherwise, we use the standard DevEntityManagerFactory,
156
     * we define a DB name which is the main DB from env but with `_test` suffixed
157
     *
158
     * @param bool $new
159
     *
160
     * @return EntityManager
161
     * @throws ConfigException
162
     * @throws \Exception
163
     * @SuppressWarnings(PHPMD)
164
     */
165
    protected function getEntityManager(bool $new = false): EntityManager
166
    {
167
        if (null === $this->entityManager || true === $new) {
168
            if (\function_exists(self::GET_ENTITY_MANAGER_FUNCTION_NAME)) {
169
                $this->entityManager = \call_user_func(self::GET_ENTITY_MANAGER_FUNCTION_NAME);
170
            } else {
171
                SimpleEnv::setEnv(Config::getProjectRootDirectory().'/.env');
172
                $testConfig                                 = $_SERVER;
173
                $testConfig[ConfigInterface::PARAM_DB_NAME] = $_SERVER[ConfigInterface::PARAM_DB_NAME].'_test';
174
                $config                                     = new Config($testConfig);
175
                $this->entityManager                        = (new EntityManagerFactory(new ArrayCache()))
176
                    ->getEntityManager($config);
177
            }
178
        }
179
180
        return $this->entityManager;
181
    }
182
183
    /**
184
     * @param EntityManager   $entityManager
185
     * @param EntityInterface $entity
186
     *
187
     * @return AbstractSaver
188
     * @throws \ReflectionException
189
     */
190
    protected function getSaver(
191
        EntityManager $entityManager,
192
        EntityInterface $entity
193
    ): AbstractSaver {
194
        $saverFqn = $this->getSaverFqn($entity);
195
196
        return new $saverFqn($entityManager);
197
    }
198
199
    /**
200
     * Use Doctrine's built in schema validation tool to catch issues
201
     */
202
    public function testValidateSchema()
203
    {
204
        $errors  = $this->getSchemaErrors();
205
        $class   = $this->getTestedEntityFqn();
206
        $message = '';
207
        if (isset($errors[$class])) {
208
            $message = "Failed ORM Validate Schema:\n";
209
            foreach ($errors[$class] as $err) {
210
                $message .= "\n * $err \n";
211
            }
212
        }
213
        $this->assertEmpty($message);
214
    }
215
216
217
    /**
218
     * @param string        $class
219
     * @param int|string    $id
220
     * @param EntityManager $entityManager
221
     *
222
     * @return EntityInterface|null
223
     */
224
    protected function loadEntity(string $class, $id, EntityManager $entityManager): ?EntityInterface
225
    {
226
        return $entityManager->getRepository($class)->find($id);
227
    }
228
229
    /**
230
     * Test that we have correctly generated an instance of our test entity
231
     *
232
     * @throws ConfigException
233
     * @throws \Doctrine\ORM\Query\QueryException
234
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
235
     * @throws \Exception
236
     * @throws \ReflectionException
237
     * @SuppressWarnings(PHPMD.StaticAccess)
238
     */
239
    public function testGeneratedCreate()
240
    {
241
        $entityManager = $this->getEntityManager();
242
        $class         = $this->getTestedEntityFqn();
243
        $generated     = $this->generateEntity($class);
244
        $this->assertInstanceOf($class, $generated);
245
        $saver = $this->getSaver($entityManager, $generated);
246
        $this->addAssociationEntities($entityManager, $generated);
247
        $this->validateEntity($generated);
248
        $meta = $entityManager->getClassMetadata($class);
249
        foreach ($meta->getFieldNames() as $fieldName) {
250
            $type             = PersisterHelper::getTypeOfField($fieldName, $meta, $entityManager);
251
            $method           = ($type[0] === 'boolean' ? 'is' : 'get').$fieldName;
252
            $reflectionMethod = new \ReflectionMethod($generated, $method);
253
            if ($reflectionMethod->hasReturnType()) {
254
                $returnType = $reflectionMethod->getReturnType();
255
                $allowsNull = $returnType->allowsNull();
256
                if ($allowsNull) {
257
                    // As we can't assert anything here so simply call
258
                    // the method and allow the type hint to raise any
259
                    // errors.
260
                    $generated->$method();
261
                    continue;
262
                }
263
            }
264
            $this->assertNotEmpty($generated->$method(), "$fieldName getter returned empty");
265
        }
266
        $saver->save($generated);
267
        $entityManager = $this->getEntityManager(true);
268
        $loaded        = $this->loadEntity($class, $generated->getId(), $entityManager);
269
        $this->assertInstanceOf($class, $loaded);
270
        $this->validateEntity($loaded);
271
        foreach ($meta->getAssociationMappings() as $mapping) {
272
            $getter = 'get'.$mapping['fieldName'];
273
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
274
                $collection = $loaded->$getter()->toArray();
275
                $this->assertNotEmpty(
276
                    $collection,
277
                    'Failed to load the collection of the associated entity ['.$mapping['fieldName']
278
                    .'] from the generated '.$class
279
                    .', make sure you have reciprocal adding of the association'
280
                );
281
                $this->assertCorrectMapping($class, $mapping, $entityManager);
282
                continue;
283
            }
284
            $association = $loaded->$getter();
285
            $this->assertNotEmpty(
286
                $association,
287
                'Failed to load the associated entity: ['.$mapping['fieldName']
288
                .'] from the generated '.$class
289
            );
290
            $this->assertNotEmpty(
291
                $association->getId(),
292
                'Failed to get the ID of the associated entity: ['.$mapping['fieldName']
293
                .'] from the generated '.$class
294
            );
295
        }
296
        $this->assertUniqueFieldsMustBeUnique($meta);
297
    }
298
299
    /**
300
     * Loop through entity fields and find unique ones
301
     *
302
     * Then ensure that the unique rule is being enforced as expected
303
     *
304
     * @param ClassMetadataInfo $meta
305
     *
306
     * @throws ConfigException
307
     * @throws \Doctrine\ORM\Mapping\MappingException
308
     * @throws \EdmondsCommerce\DoctrineStaticMeta\Exception\DoctrineStaticMetaException
309
     * @throws \ReflectionException
310
     * @throws \Doctrine\ORM\Mapping\MappingException
311
     */
312
    protected function assertUniqueFieldsMustBeUnique(ClassMetadataInfo $meta): void
313
    {
314
        $uniqueFields = [];
315
        foreach ($meta->getFieldNames() as $fieldName) {
316
            $fieldMapping = $meta->getFieldMapping($fieldName);
317
            if (array_key_exists('unique', $fieldMapping) && true === $fieldMapping['unique']) {
318
                $uniqueFields[$fieldName] = $fieldMapping;
319
            }
320
        }
321
        if ([] === $uniqueFields) {
322
            return;
323
        }
324
        $class = $this->getTestedEntityFqn();
325
        foreach ($uniqueFields as $fieldName => $fieldMapping) {
326
            $entityManager = $this->getEntityManager(true);
327
            $primary       = $this->generateEntity($class);
328
            $secondary     = $this->generateEntity($class);
329
            $getter        = 'get'.$fieldName;
330
            $setter        = 'set'.$fieldName;
331
            $primaryValue  = $primary->$getter();
332
            $secondary->$setter($primaryValue);
333
            $saver = $this->getSaver($entityManager, $primary);
334
            $this->expectException(UniqueConstraintViolationException::class);
335
            $saver->saveAll([$primary, $secondary]);
336
        }
337
    }
338
339
    /**
340
     * Check the mapping of our class and the associated entity to make sure it's configured properly on both sides.
341
     * Very easy to get wrong. This is in addition to the standard Schema Validation
342
     *
343
     * @param string        $classFqn
344
     * @param array         $mapping
345
     * @param EntityManager $entityManager
346
     */
347
    protected function assertCorrectMapping(string $classFqn, array $mapping, EntityManager $entityManager)
348
    {
349
        $pass                                 = false;
350
        $associationFqn                       = $mapping['targetEntity'];
351
        $associationMeta                      = $entityManager->getClassMetadata($associationFqn);
352
        $classTraits                          = $entityManager->getClassMetadata($classFqn)
353
                                                              ->getReflectionClass()
354
                                                              ->getTraits();
355
        $unidirectionalTraitShortNamePrefixes = [
356
            'Has'.$associationFqn::getSingular().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
357
            'Has'.$associationFqn::getPlural().RelationsGenerator::PREFIX_UNIDIRECTIONAL,
358
        ];
359
        foreach ($classTraits as $trait) {
360
            foreach ($unidirectionalTraitShortNamePrefixes as $namePrefix) {
361
                if (0 === \stripos($trait->getShortName(), $namePrefix)) {
362
                    return;
363
                }
364
            }
365
        }
366
        foreach ($associationMeta->getAssociationMappings() as $associationMapping) {
367
            if ($classFqn === $associationMapping['targetEntity']) {
368
                if (empty($mapping['joinTable'])) {
369
                    $this->assertArrayNotHasKey(
370
                        'joinTable',
371
                        $associationMapping,
372
                        $classFqn.' join table is empty,
373
                        but association '.$mapping['targetEntity'].' join table is not empty'
374
                    );
375
                    $pass = true;
376
                    break;
377
                }
378
                $this->assertNotEmpty(
379
                    $associationMapping['joinTable'],
380
                    "$classFqn joinTable is set to ".$mapping['joinTable']['name']
381
                    ." \n association ".$mapping['targetEntity'].' join table is empty'
382
                );
383
                $this->assertSame(
384
                    $mapping['joinTable']['name'],
385
                    $associationMapping['joinTable']['name'],
386
                    "join tables not the same: \n * $classFqn = ".$mapping['joinTable']['name']
387
                    ." \n * association ".$mapping['targetEntity']
388
                    .' = '.$associationMapping['joinTable']['name']
389
                );
390
                $this->assertArrayHasKey(
391
                    'inverseJoinColumns',
392
                    $associationMapping['joinTable'],
393
                    "join table join columns not the same: \n * $classFqn joinColumn = "
394
                    .$mapping['joinTable']['joinColumns'][0]['name']
395
                    ." \n * association ".$mapping['targetEntity']
396
                    .' inverseJoinColumn is not set'
397
                );
398
                $this->assertSame(
399
                    $mapping['joinTable']['joinColumns'][0]['name'],
400
                    $associationMapping['joinTable']['inverseJoinColumns'][0]['name'],
401
                    "join table join columns not the same: \n * $classFqn joinColumn = "
402
                    .$mapping['joinTable']['joinColumns'][0]['name']
403
                    ." \n * association ".$mapping['targetEntity']
404
                    .' inverseJoinColumn = '.$associationMapping['joinTable']['inverseJoinColumns'][0]['name']
405
                );
406
                $this->assertSame(
407
                    $mapping['joinTable']['inverseJoinColumns'][0]['name'],
408
                    $associationMapping['joinTable']['joinColumns'][0]['name'],
409
                    "join table join columns  not the same: \n * $classFqn inverseJoinColumn = "
410
                    .$mapping['joinTable']['inverseJoinColumns'][0]['name']
411
                    ." \n * association ".$mapping['targetEntity'].' joinColumn = '
412
                    .$associationMapping['joinTable']['joinColumns'][0]['name']
413
                );
414
                $pass = true;
415
                break;
416
            }
417
        }
418
        $this->assertTrue($pass, 'Failed finding association mapping to test for '."\n".$mapping['targetEntity']);
419
    }
420
421
    /**
422
     * @param string $class
423
     *
424
     * @return EntityInterface
425
     * @throws ConfigException
426
     * @throws \Exception
427
     * @SuppressWarnings(PHPMD.StaticAccess)
428
     */
429
    protected function generateEntity(string $class): EntityInterface
430
    {
431
        $entityManager          = $this->getEntityManager();
432
        $customColumnFormatters = $this->generateColumnFormatters($entityManager, $class);
433
        $populator              = new Populator($this->generator, $entityManager);
434
        $populator->addEntity($class, 1, $customColumnFormatters);
435
436
        $entity = $populator->execute(null, false)[$class][0];
437
438
        return $entity;
439
    }
440
441
    protected function validateEntity(EntityInterface $entity): void
442
    {
443
        $entity->injectValidator($this->entityValidator);
444
        $entity->validate();
445
    }
446
447
    /**
448
     * @param EntityManager $entityManager
449
     * @param string        $class
450
     *
451
     * @return array
452
     */
453
    protected function generateColumnFormatters(EntityManager $entityManager, string $class): array
454
    {
455
        $columnFormatters = [];
456
        $meta             = $entityManager->getClassMetadata($class);
457
        $mappings         = $meta->getAssociationMappings();
458
        foreach ($mappings as $mapping) {
459
            if ($meta->isCollectionValuedAssociation($mapping['fieldName'])) {
460
                $columnFormatters[$mapping['fieldName']] = new ArrayCollection();
461
                continue;
462
            }
463
            $columnFormatters[$mapping['fieldName']] = null;
464
        }
465
        $fieldNames = $meta->getFieldNames();
466
467
        foreach ($fieldNames as $fieldName) {
468
            if (!isset($columnFormatters[$fieldName])) {
469
                if (true === $this->setFakerDataProvider($columnFormatters, $fieldName)) {
470
                    continue;
471
                }
472
            }
473
            $fieldMapping = $meta->getFieldMapping($fieldName);
474
            if (true == ($fieldMapping['unique'] ?? false)) {
475
                switch ($fieldMapping['type']) {
476
                    case 'string':
477
                        $columnFormatters[$fieldName] = $this->getUniqueString();
478
                        break;
479
                    case 'integer':
480
                        $columnFormatters[$fieldName] = $this->getUniqueInt();
481
                        break;
482
                    default:
483
                        throw new \InvalidArgumentException('unique field has an unsupported type: '
484
                                                            .print_r($fieldMapping, true));
485
                }
486
            }
487
        }
488
489
        return $columnFormatters;
490
    }
491
492
    protected function getUniqueString(): string
493
    {
494
        $string = 'unique string: '.$this->getUniqueInt().md5((string)time());
495
        while (isset(self::$uniqueStrings[$string])) {
496
            $string                       = md5((string)time());
497
            self::$uniqueStrings[$string] = true;
498
        }
499
500
        return $string;
501
    }
502
503
    protected function getUniqueInt(): int
504
    {
505
        return ++self::$uniqueInt;
506
    }
507
508
    /**
509
     * Add a faker data provider to the columnFormatters array (by reference) if there is one available
510
     *
511
     * Handles instantiating and caching of the data providers
512
     *
513
     * @param array  $columnFormatters
514
     * @param string $fieldName
515
     */
516
    protected function setFakerDataProvider(array &$columnFormatters, string $fieldName): bool
517
    {
518
        if (!isset(static::FAKER_DATA_PROVIDERS[$fieldName])) {
519
            return false;
520
        }
521
        if (!isset($this->fakerDataProviderObjects[$fieldName])) {
522
            $class                                      = static::FAKER_DATA_PROVIDERS[$fieldName];
523
            $this->fakerDataProviderObjects[$fieldName] = new $class($this->generator);
524
        }
525
        $columnFormatters[$fieldName] = $this->fakerDataProviderObjects[$fieldName];
526
527
        return true;
528
    }
529
530
    /**
531
     * @param EntityManager   $entityManager
532
     * @param EntityInterface $generated
533
     *
534
     * @throws ConfigException
535
     * @throws \Exception
536
     * @throws \ReflectionException
537
     * @SuppressWarnings(PHPMD.ElseExpression)
538
     */
539
    protected function addAssociationEntities(
540
        EntityManager $entityManager,
541
        EntityInterface $generated
542
    ) {
543
        $entityReflection = $this->getTestedEntityReflectionClass();
544
        $class            = $entityReflection->getName();
545
        $meta             = $entityManager->getClassMetadata($class);
546
        $mappings         = $meta->getAssociationMappings();
547
        if (empty($mappings)) {
548
            return;
549
        }
550
        $namespaceHelper = new NamespaceHelper();
551
        $methods         = array_map('strtolower', get_class_methods($generated));
552
        foreach ($mappings as $mapping) {
553
            $mappingEntityClass = $mapping['targetEntity'];
554
            $mappingEntity      = $this->generateEntity($mappingEntityClass);
555
            $errorMessage       = "Error adding association entity $mappingEntityClass to $class: %s";
556
            $saver              = $this->getSaver($entityManager, $mappingEntity);
557
            $saver->save($mappingEntity);
558
            $mappingEntityPluralInterface = $namespaceHelper->getHasPluralInterfaceFqnForEntity($mappingEntityClass);
559
            if ($entityReflection->implementsInterface($mappingEntityPluralInterface)) {
560
                $this->assertEquals(
561
                    $mappingEntityClass::getPlural(),
562
                    $mapping['fieldName'],
563
                    sprintf($errorMessage, ' mapping should be plural')
564
                );
565
                $method = 'add'.$mappingEntityClass::getSingular();
566
            } else {
567
                $this->assertEquals(
568
                    $mappingEntityClass::getSingular(),
569
                    $mapping['fieldName'],
570
                    sprintf($errorMessage, ' mapping should be singular')
571
                );
572
                $method = 'set'.$mappingEntityClass::getSingular();
573
            }
574
            $this->assertContains(
575
                strtolower($method),
576
                $methods,
577
                sprintf($errorMessage, $method.' method is not defined')
578
            );
579
            $generated->$method($mappingEntity);
580
        }
581
    }
582
583
    /**
584
     * Get the fully qualified name of the Entity we are testing,
585
     * assumes EntityNameTest as the entity class short name
586
     *
587
     * @return string
588
     */
589
    protected function getTestedEntityFqn(): string
590
    {
591
        if (null === $this->testedEntityFqn) {
592
            $this->testedEntityFqn = \substr(static::class, 0, -4);
593
        }
594
595
        return $this->testedEntityFqn;
596
    }
597
598
    /**
599
     * Get the fully qualified name of the saver for the entity we are testing.
600
     *
601
     * @param EntityInterface $entity
602
     *
603
     * @return string
604
     * @throws \ReflectionException
605
     */
606
    protected function getSaverFqn(
607
        EntityInterface $entity
608
    ): string {
609
        $ref             = new \ReflectionClass($entity);
610
        $entityNamespace = $ref->getNamespaceName();
611
        $saverNamespace  = \str_replace(
612
            'Entities',
613
            'Entity\\Savers',
614
            $entityNamespace
615
        );
616
        $shortName       = $ref->getShortName();
617
618
        return $saverNamespace.'\\'.$shortName.'Saver';
619
    }
620
621
    /**
622
     * Get a \ReflectionClass for the currently tested Entity
623
     *
624
     * @return \ReflectionClass
625
     * @throws \ReflectionException
626
     */
627
    protected function getTestedEntityReflectionClass(): \ReflectionClass
628
    {
629
        if (null === $this->testedEntityReflectionClass) {
630
            $this->testedEntityReflectionClass = new \ReflectionClass(
631
                $this->getTestedEntityFqn()
632
            );
633
        }
634
635
        return $this->testedEntityReflectionClass;
636
    }
637
}
638