Passed
Pull Request — master (#5)
by Alex
07:47
created

testSaveAssociationsWillThrowAPersistenceExceptionIfTheTargetEntityMethodDoesNotExist()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 78
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 47
dl 0
loc 78
rs 9.1563
c 0
b 0
f 0
cc 1
nc 1
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace ArpTest\DoctrineEntityRepository\Persistence;
6
7
use Arp\DoctrineEntityRepository\EntityRepositoryInterface;
8
use Arp\DoctrineEntityRepository\Exception\EntityRepositoryException;
9
use Arp\DoctrineEntityRepository\Persistence\CascadeSaveService;
10
use Arp\DoctrineEntityRepository\Persistence\Exception\PersistenceException;
11
use Arp\Entity\EntityInterface;
12
use Arp\Entity\EntityTrait;
13
use Doctrine\ORM\EntityManagerInterface;
14
use Doctrine\ORM\Mapping\ClassMetadata;
15
use PHPUnit\Framework\MockObject\MockObject;
16
use PHPUnit\Framework\TestCase;
17
use Psr\Log\LoggerInterface;
18
19
/**
20
 * @author  Alex Patterson <[email protected]>
21
 * @package ArpTest\DoctrineEntityRepository\Persistence
22
 */
23
final class CascadeSaveServiceTest extends TestCase
24
{
25
    /**
26
     * @var EntityManagerInterface|MockObject
27
     */
28
    private $entityManager;
29
30
    /**
31
     * @var LoggerInterface|MockObject
32
     */
33
    private $logger;
34
35
    /**
36
     * @var array
37
     */
38
    private array $options = [];
39
40
    /**
41
     * @var array
42
     */
43
    private array $collectionOptions = [];
44
45
    /**
46
     * Prepare the test case dependencies.
47
     */
48
    public function setUp(): void
49
    {
50
        $this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
51
52
        $this->entityManager = $this->getMockForAbstractClass(EntityManagerInterface::class);
53
    }
54
55
    /**
56
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::__construct
57
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociation
58
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::getTargetRepository
59
     *
60
     * @throws PersistenceException
61
     * @throws EntityRepositoryException
62
     */
63
    public function testSaveAssociationWillThrowAPersistenceExceptionIfTheTargetEntityRepositoryCannotBeLoaded(): void
64
    {
65
        $cascadeService = new CascadeSaveService($this->logger, $this->options, $this->collectionOptions);
66
67
        $entityName = EntityInterface::class;
68
69
        /** @var EntityInterface $entity */
70
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
71
72
        $exceptionMessage = 'This is a test exception message';
73
        $exception = new \Exception($exceptionMessage, 123);
74
75
        $this->entityManager->expects($this->once())
76
            ->method('getRepository')
77
            ->with($entityName)
78
            ->willThrowException($exception);
79
80
        $errorMessage = sprintf(
81
            'An error occurred while attempting to load the repository for entity class \'%s\' : %s',
82
            $entityName,
83
            $exceptionMessage
84
        );
85
86
        $this->logger->expects($this->once())
87
            ->method('error')
88
            ->with($errorMessage, compact('exception'));
89
90
        $this->expectException(PersistenceException::class);
91
        $this->expectExceptionMessage($errorMessage);
92
        $this->expectExceptionCode($exception->getCode());
93
94
        $cascadeService->saveAssociation($this->entityManager, $entityName, $entity);
95
    }
96
97
    /**
98
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::__construct
99
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociation
100
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::getTargetRepository
101
     *
102
     * @throws PersistenceException
103
     * @throws EntityRepositoryException
104
     */
105
    public function testSaveAssociationWillThrowAPersistenceExceptionIfTheTargetEntityRepositoryIsInvalid(): void
106
    {
107
        $cascadeService = new CascadeSaveService($this->logger, $this->options, $this->collectionOptions);
108
109
        $entityName = EntityInterface::class;
110
111
        /** @var EntityInterface $entity */
112
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
113
114
        $entityRepository = new \stdClass();
115
116
        $this->entityManager->expects($this->once())
117
            ->method('getRepository')
118
            ->with($entityName)
119
            ->willReturn($entityRepository);
120
121
        $errorMessage = sprintf(
122
            'The entity repository must be an object of type \'%s\'; \'%s\' returned in \'%s::%s\'',
123
            EntityRepositoryInterface::class,
124
            (is_object($entityRepository) ? get_class($entityRepository) : gettype($entityRepository)),
125
            CascadeSaveService::class,
126
            'getTargetRepository'
127
        );
128
129
        $this->logger->expects($this->once())->method('error')->with($errorMessage);
130
131
        $this->expectException(PersistenceException::class);
132
        $this->expectExceptionMessage($errorMessage);
133
134
        $cascadeService->saveAssociation($this->entityManager, $entityName, $entity);
135
    }
136
137
    /**
138
     * Assert that an PersistenceException is thrown if an invalid entity or collection value is
139
     * passed to the saveAssociation() method.
140
     *
141
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::__construct
142
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociation
143
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::getTargetRepository
144
     *
145
     * @throws PersistenceException
146
     * @throws EntityRepositoryException
147
     */
148
    public function testSaveAssociationWillThrowAPersistenceExceptionIfTheTargetEntityOrCollectionIsInvalid(): void
149
    {
150
        $cascadeService = new CascadeSaveService($this->logger, $this->options, $this->collectionOptions);
151
152
        $targetEntityName = EntityInterface::class;
153
154
        /** @var EntityInterface|\stdClass $entityOrCollection */
155
        $entityOrCollection = new \stdClass();
156
157
        $errorMessage = sprintf(
158
            'Unable to cascade save target entity \'%s\': The entity or collection is of an invalid type \'%s\'',
159
            $targetEntityName,
160
            \stdClass::class
161
        );
162
163
        $this->logger->expects($this->once())->method('error')->with($errorMessage);
164
165
        $this->expectException(PersistenceException::class);
166
        $this->expectExceptionMessage($errorMessage);
167
168
        $cascadeService->saveAssociation($this->entityManager, $targetEntityName, $entityOrCollection);
0 ignored issues
show
Bug introduced by
$entityOrCollection of type stdClass is incompatible with the type Arp\Entity\EntityInterfa...ityInterface[]|iterable expected by parameter $entityOrCollection of Arp\DoctrineEntityReposi...vice::saveAssociation(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

168
        $cascadeService->saveAssociation($this->entityManager, $targetEntityName, /** @scrutinizer ignore-type */ $entityOrCollection);
Loading history...
169
    }
170
171
    /**
172
     * @param array $options
173
     *
174
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::__construct
175
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociation
176
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::getTargetRepository
177
     *
178
     * @throws EntityRepositoryException
179
     * @throws PersistenceException
180
     */
181
    public function testSaveAssociationWillSaveEntity(array $options = []): void
182
    {
183
        $cascadeService = new CascadeSaveService($this->logger, $this->options, $this->collectionOptions);
184
185
        $targetEntityName = EntityInterface::class;
186
187
        /** @var EntityInterface|MockObject $entityOrCollection */
188
        $entityOrCollection = $this->getMockForAbstractClass(EntityInterface::class);
189
190
        /** @var EntityRepositoryInterface|MockObject $entityRepository */
191
        $entityRepository = $this->getMockForAbstractClass(EntityRepositoryInterface::class);
192
193
        $this->entityManager->expects($this->once())
194
            ->method('getRepository')
195
            ->with($targetEntityName)
196
            ->willReturn($entityRepository);
197
198
        $entityRepository->expects($this->once())
199
            ->method('save')
200
            ->with($entityOrCollection, $options);
201
202
        $cascadeService->saveAssociation($this->entityManager, $targetEntityName, $entityOrCollection, $options);
203
    }
204
205
    /**
206
     * @param array $options
207
     *
208
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::__construct
209
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociation
210
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::getTargetRepository
211
     *
212
     * @throws EntityRepositoryException
213
     * @throws PersistenceException
214
     */
215
    public function testSaveAssociationWillSaveEntityCollection(array $options = []): void
216
    {
217
        $cascadeService = new CascadeSaveService($this->logger, $this->options, $this->collectionOptions);
218
219
        $targetEntityName = EntityInterface::class;
220
221
        /** @var EntityInterface|MockObject $entityOrCollection */
222
        $entityOrCollection = [
223
            $this->getMockForAbstractClass(EntityInterface::class),
224
            $this->getMockForAbstractClass(EntityInterface::class),
225
            $this->getMockForAbstractClass(EntityInterface::class)
226
        ];
227
228
        /** @var EntityRepositoryInterface|MockObject $entityRepository */
229
        $entityRepository = $this->getMockForAbstractClass(EntityRepositoryInterface::class);
230
231
        $this->entityManager->expects($this->once())
232
            ->method('getRepository')
233
            ->with($targetEntityName)
234
            ->willReturn($entityRepository);
235
236
        $entityRepository->expects($this->once())
237
            ->method('saveCollection')
238
            ->with($entityOrCollection, $options);
239
240
        $cascadeService->saveAssociation($this->entityManager, $targetEntityName, $entityOrCollection, $options);
241
    }
242
243
    /**
244
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::__construct
245
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociations
246
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::getClassMetadata
247
     *
248
     * @throws EntityRepositoryException
249
     * @throws PersistenceException
250
     */
251
    public function testSaveAssociationsWillThrowAPersistenceExceptionIfTheEntityMetadataCannotBeLoaded(): void
252
    {
253
        $cascadeService = new CascadeSaveService($this->logger, $this->options, $this->collectionOptions);
254
255
        $entityName = EntityInterface::class;
256
257
        /** @var EntityInterface|MockObject $entity */
258
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
259
260
        $exceptionMessage = 'This is a test exception message';
261
        $exception = new \Exception($exceptionMessage, 123);
262
263
        $errorMessage = $errorMessage = sprintf(
0 ignored issues
show
Unused Code introduced by
The assignment to $errorMessage is dead and can be removed.
Loading history...
264
            'The entity metadata mapping for class \'%s\' could not be loaded: %s',
265
            $entityName,
266
            $exceptionMessage
267
        );
268
269
        $this->entityManager->expects($this->once())
270
            ->method('getClassMetadata')
271
            ->with($entityName)
272
            ->willThrowException($exception);
273
274
        $this->logger->expects($this->once())
275
            ->method('error')
276
            ->with($errorMessage);
277
278
        $this->expectException(PersistenceException::class);
279
        $this->expectExceptionMessage($errorMessage);
280
        $this->expectExceptionCode(123);
281
282
        $cascadeService->saveAssociations($this->entityManager, $entityName, $entity);
283
    }
284
285
    /**
286
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociations
287
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::resolveTargetEntityOrCollection
288
     *
289
     * @throws EntityRepositoryException
290
     * @throws PersistenceException
291
     */
292
    public function testSaveAssociationsWillThrowAPersistenceExceptionIfTheTargetEntityMethodDoesNotExist(): void
293
    {
294
        $cascadeService = new CascadeSaveService($this->logger, $this->options, $this->collectionOptions);
295
296
        $entityName = EntityInterface::class;
297
298
        /** @var EntityInterface|MockObject $entity */
299
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
300
301
        /**
302
         * @var ClassMetadata|MockObject $classMetadata
303
         * @var ClassMetadata|MockObject $targetMetadata
304
         */
305
        $classMetadata = $this->createMock(ClassMetadata::class);
306
        $targetMetadata = $this->createMock(ClassMetadata::class);
307
308
        $mapping = [
309
            'targetEntity' => EntityInterface::class,
310
            'fieldName' => 'test',
311
            'type' => 'string',
312
            'isCascadePersist' => true,
313
        ];
314
315
        $mappings = [$mapping];
316
317
        $this->entityManager->expects($this->exactly(2))
318
            ->method('getClassMetadata')
319
            ->withConsecutive(
320
                [$entityName],
321
                [$mapping['targetEntity']]
322
            )
323
            ->willReturnOnConsecutiveCalls(
324
                $classMetadata,
325
                $targetMetadata
326
            );
327
328
        $classMetadata->expects($this->once())
329
            ->method('getAssociationMappings')
330
            ->willReturn($mappings);
331
332
        $this->logger->expects($this->exactly(2))
333
            ->method('info')
334
            ->withConsecutive(
335
                [
336
                    sprintf('Processing cascade save operations for for entity class \'%s\'', $entityName)
337
                ],
338
                [
339
                    sprintf(
340
                        'The entity field \'%s::%s\' is configured for cascade operations for target entity \'%s\'',
341
                        $entityName,
342
                        $mapping['fieldName'],
343
                        $mapping['targetEntity']
344
                    )
345
                ]
346
            );
347
348
        $classMetadata->expects($this->once())->method('getName')->willReturn($entityName);
349
        $targetMetadata->expects($this->once())->method('getName')->willReturn($mapping['targetEntity']);
350
351
        $methodName = 'get' . ucfirst($mapping['fieldName']);
352
353
        $errorMessage = sprintf(
354
            'Failed to find required entity method \'%s::%s\'. The method is required for cascade operations '
355
            . 'of field \'%s\' of target entity \'%s\'',
356
            $entityName,
357
            $methodName,
358
            $mapping['fieldName'],
359
            $mapping['targetEntity']
360
        );
361
362
        $this->logger->expects($this->once())
363
            ->method('error')
364
            ->with($errorMessage);
365
366
        $this->expectException(PersistenceException::class);
367
        $this->expectExceptionMessage($errorMessage);
368
369
        $cascadeService->saveAssociations($this->entityManager, $entityName, $entity);
370
    }
371
372
    /**
373
     * Assert that calls to saveAssociations() will raise a PersistenceException if the provided entity method call
374
     * throws an exception
375
     *
376
     * @throws EntityRepositoryException
377
     * @throws PersistenceException
378
     */
379
    public function testSaveAssociationsWillThrowAPersistenceExceptionIfTheTargetEntityCannotBeLoaded(): void
380
    {
381
        $cascadeService = new CascadeSaveService($this->logger, $this->options, $this->collectionOptions);
382
383
        $entityName = EntityInterface::class;
384
385
        $exceptionMessage = 'This is a test exception message';
386
        $exception = new \Error($exceptionMessage);
387
388
        /** @var EntityInterface|MockObject $entity */
389
        $entity = new class($exception) implements EntityInterface {
390
            use EntityTrait;
391
            public \Throwable $exception;
392
393
            public function __construct(\Throwable $exception)
394
            {
395
                $this->exception = $exception;
396
            }
397
398
            public function getFoo(): string
399
            {
400
                throw $this->exception;
401
            }
402
        };
403
404
        /**
405
         * @var ClassMetadata|MockObject $classMetadata
406
         * @var ClassMetadata|MockObject $targetMetadata
407
         */
408
        $classMetadata = $this->createMock(ClassMetadata::class);
409
        $targetMetadata = $this->createMock(ClassMetadata::class);
410
411
        $mapping = [
412
            'targetEntity' => EntityInterface::class,
413
            'fieldName' => 'foo',
414
            'type' => 'string',
415
            'isCascadePersist' => true,
416
        ];
417
418
        $mappings = [$mapping];
419
420
        $this->entityManager->expects($this->exactly(2))
421
            ->method('getClassMetadata')
422
            ->withConsecutive(
423
                [$entityName],
424
                [$mapping['targetEntity']]
425
            )
426
            ->willReturnOnConsecutiveCalls(
427
                $classMetadata,
428
                $targetMetadata
429
            );
430
431
        $classMetadata->expects($this->once())
432
            ->method('getAssociationMappings')
433
            ->willReturn($mappings);
434
435
        $this->logger->expects($this->exactly(2))
436
            ->method('info')
437
            ->withConsecutive(
438
                [
439
                    sprintf(
440
                        'Processing cascade save operations for for entity class \'%s\'',
441
                        $entityName
442
                    )
443
                ],
444
                [
445
                    sprintf(
446
                        'The entity field \'%s::%s\' is configured for cascade operations for target entity \'%s\'',
447
                        $entityName,
448
                        $mapping['fieldName'],
449
                        $mapping['targetEntity']
450
                    )
451
                ]
452
            );
453
454
        $classMetadata->expects($this->once())->method('getName')->willReturn($entityName);
455
        $targetMetadata->expects($this->once())->method('getName')->willReturn($mapping['targetEntity']);
456
457
        $methodName = 'get' . ucfirst($mapping['fieldName']);
458
459
        $errorMessage = sprintf(
460
            'The call to resolve entity of type \'%s\' from method call \'%s::%s\' failed: %s',
461
            $mapping['targetEntity'],
462
            $entityName,
463
            $methodName,
464
            $exceptionMessage
465
        );
466
467
        $this->logger->expects($this->once())
468
            ->method('error')
469
            ->with($errorMessage);
470
471
        $this->expectException(PersistenceException::class);
472
        $this->expectExceptionMessage($errorMessage);
473
474
        $cascadeService->saveAssociations($this->entityManager, $entityName, $entity);
475
    }
476
477
    /**
478
     * Assert that calls to saveAssociations() when mapping data contains associations that are either incorrectly
479
     * configured (missing required keys) or are not cascade persist.
480
     *
481
     * @param array $mappingData The association mapping data for a single field to test
482
     *
483
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociations
484
     *
485
     * @dataProvider getSaveAssociationsWillSkipAssociationsWithNonCascadePersistMappingDataData
486
     */
487
    public function testSaveAssociationsWillSkipAssociationsWithNonCascadePersistMappingData(array $mappingData): void
488
    {
489
        /** @var CascadeSaveService|MockObject $cascadeService */
490
        $cascadeService = $this->getMockBuilder(CascadeSaveService::class)
491
            ->setConstructorArgs([$this->logger, $this->options, $this->collectionOptions])
492
            ->onlyMethods(['saveAssociation'])
493
            ->getMock();
494
495
        $entityName = EntityInterface::class;
496
497
        /** @var EntityInterface|MockObject $entity */
498
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
499
500
        /** @var ClassMetadata|MockObject $classMetadata */
501
        $classMetadata = $this->createMock(ClassMetadata::class);
502
503
        $this->entityManager->expects($this->once())
504
            ->method('getClassMetadata')
505
            ->with($entityName)
506
            ->willReturn($classMetadata);
507
508
        $classMetadata->expects($this->once())
509
            ->method('getAssociationMappings')
510
            ->willReturn([$mappingData]);
511
512
        $cascadeService->saveAssociations($this->entityManager, $entityName, $entity);
513
    }
514
515
    /**
516
     * @return array
517
     */
518
    public function getSaveAssociationsWillSkipAssociationsWithNonCascadePersistMappingDataData(): array
519
    {
520
        return [
521
            [
522
                [
523
524
                ],
525
            ],
526
527
            [
528
                [
529
                    'targetEntity' => EntityInterface::class,
530
                ],
531
            ],
532
533
            [
534
                [
535
                    'targetEntity' => EntityInterface::class,
536
                    'fieldName' => 'foo',
537
                ],
538
            ],
539
540
            [
541
                [
542
                    'targetEntity' => EntityInterface::class,
543
                    'fieldName' => 'foo',
544
                    'type' => 1
545
                ],
546
            ],
547
548
            [
549
                [
550
                    'targetEntity' => EntityInterface::class,
551
                    'fieldName' => 'foo',
552
                    'type' => 1,
553
                    'isCascadePersist' => false
554
                ],
555
            ]
556
        ];
557
    }
558
}
559