Completed
Push — master ( e9ba41...791922 )
by Alex
14s queued 12s
created

php$0 ➔ getSaveAssociationsWillSkipAssociationsWithNonCascadePersistMappingDataData()   A

Complexity

Conditions 1

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 9.344
c 0
b 0
f 0
cc 1
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 {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after class keyword; 0 found
Loading history...
390
            use EntityTrait;
0 ignored issues
show
Coding Style introduced by
There must be a blank line following the last trait import statement
Loading history...
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
     * @throws EntityRepositoryException
488
     * @throws PersistenceException
489
     */
490
    public function testSaveAssociationsWillSkipAssociationsWithNonCascadePersistMappingData(array $mappingData): void
491
    {
492
        /** @var CascadeSaveService|MockObject $cascadeService */
493
        $cascadeService = $this->getMockBuilder(CascadeSaveService::class)
494
            ->setConstructorArgs([$this->logger, $this->options, $this->collectionOptions])
495
            ->onlyMethods(['saveAssociation'])
496
            ->getMock();
497
498
        $entityName = EntityInterface::class;
499
500
        /** @var EntityInterface|MockObject $entity */
501
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
502
503
        /** @var ClassMetadata|MockObject $classMetadata */
504
        $classMetadata = $this->createMock(ClassMetadata::class);
505
506
        $this->entityManager->expects($this->once())
507
            ->method('getClassMetadata')
508
            ->with($entityName)
509
            ->willReturn($classMetadata);
510
511
        $classMetadata->expects($this->once())
512
            ->method('getAssociationMappings')
513
            ->willReturn([$mappingData]);
514
515
        $cascadeService->saveAssociations($this->entityManager, $entityName, $entity);
516
    }
517
518
    /**
519
     * @return array
520
     */
521
    public function getSaveAssociationsWillSkipAssociationsWithNonCascadePersistMappingDataData(): array
522
    {
523
        return [
524
            [
525
                [
526
527
                ],
528
            ],
529
530
            [
531
                [
532
                    'targetEntity' => EntityInterface::class,
533
                ],
534
            ],
535
536
            [
537
                [
538
                    'targetEntity' => EntityInterface::class,
539
                    'fieldName' => 'foo',
540
                ],
541
            ],
542
543
            [
544
                [
545
                    'targetEntity' => EntityInterface::class,
546
                    'fieldName' => 'foo',
547
                    'type' => 1
548
                ],
549
            ],
550
551
            [
552
                [
553
                    'targetEntity' => EntityInterface::class,
554
                    'fieldName' => 'foo',
555
                    'type' => 1,
556
                    'isCascadePersist' => false
557
                ],
558
            ]
559
        ];
560
    }
561
562
563
564
}
565