Completed
Push — master ( 791922...f9d483 )
by Alex
16s queued 15s
created

php$1 ➔ testSaveAssociationsThrowPersistenceExceptionIfTheAssocValueIsInvalid()   B

Complexity

Conditions 1

Size

Total Lines 81

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 81
rs 8.4145
cc 1

2 Methods

Rating   Name   Duplication   Size   Complexity  
A CascadeSaveServiceTest.php$1 ➔ __construct() 0 3 1
A CascadeSaveServiceTest.php$1 ➔ getFoo() 0 3 1

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
392
            public \Throwable $exception;
393
394
            public function __construct(\Throwable $exception)
395
            {
396
                $this->exception = $exception;
397
            }
398
399
            public function getFoo(): string
400
            {
401
                throw $this->exception;
402
            }
403
        };
404
405
        /**
406
         * @var ClassMetadata|MockObject $classMetadata
407
         * @var ClassMetadata|MockObject $targetMetadata
408
         */
409
        $classMetadata = $this->createMock(ClassMetadata::class);
410
        $targetMetadata = $this->createMock(ClassMetadata::class);
411
412
        $mapping = [
413
            'targetEntity' => EntityInterface::class,
414
            'fieldName' => 'foo',
415
            'type' => 'string',
416
            'isCascadePersist' => true,
417
        ];
418
419
        $mappings = [$mapping];
420
421
        $this->entityManager->expects($this->exactly(2))
422
            ->method('getClassMetadata')
423
            ->withConsecutive(
424
                [$entityName],
425
                [$mapping['targetEntity']]
426
            )
427
            ->willReturnOnConsecutiveCalls(
428
                $classMetadata,
429
                $targetMetadata
430
            );
431
432
        $classMetadata->expects($this->once())
433
            ->method('getAssociationMappings')
434
            ->willReturn($mappings);
435
436
        $this->logger->expects($this->exactly(2))
437
            ->method('info')
438
            ->withConsecutive(
439
                [
440
                    sprintf(
441
                        'Processing cascade save operations for for entity class \'%s\'',
442
                        $entityName
443
                    )
444
                ],
445
                [
446
                    sprintf(
447
                        'The entity field \'%s::%s\' is configured for cascade operations for target entity \'%s\'',
448
                        $entityName,
449
                        $mapping['fieldName'],
450
                        $mapping['targetEntity']
451
                    )
452
                ]
453
            );
454
455
        $classMetadata->expects($this->once())->method('getName')->willReturn($entityName);
456
        $targetMetadata->expects($this->once())->method('getName')->willReturn($mapping['targetEntity']);
457
458
        $methodName = 'get' . ucfirst($mapping['fieldName']);
459
460
        $errorMessage = sprintf(
461
            'The call to resolve entity of type \'%s\' from method call \'%s::%s\' failed: %s',
462
            $mapping['targetEntity'],
463
            $entityName,
464
            $methodName,
465
            $exceptionMessage
466
        );
467
468
        $this->logger->expects($this->once())
469
            ->method('error')
470
            ->with($errorMessage);
471
472
        $this->expectException(PersistenceException::class);
473
        $this->expectExceptionMessage($errorMessage);
474
475
        $cascadeService->saveAssociations($this->entityManager, $entityName, $entity);
476
    }
477
478
    /**
479
     * Assert that calls to saveAssociations() when mapping data contains associations that are either incorrectly
480
     * configured (missing required keys) or are not cascade persist.
481
     *
482
     * @param array $mappingData The association mapping data for a single field to test
483
     *
484
     * @covers       \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociations
485
     *
486
     * @dataProvider getSaveAssociationsWillSkipAssociationsWithNonCascadePersistMappingDataData
487
     *
488
     * @throws EntityRepositoryException
489
     * @throws PersistenceException
490
     */
491
    public function testSaveAssociationsWillSkipAssociationsWithNonCascadePersistMappingData(array $mappingData): void
492
    {
493
        /** @var CascadeSaveService|MockObject $cascadeService */
494
        $cascadeService = $this->getMockBuilder(CascadeSaveService::class)
495
            ->setConstructorArgs([$this->logger, $this->options, $this->collectionOptions])
496
            ->onlyMethods(['saveAssociation'])
497
            ->getMock();
498
499
        $entityName = EntityInterface::class;
500
501
        /** @var EntityInterface|MockObject $entity */
502
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
503
504
        /** @var ClassMetadata|MockObject $classMetadata */
505
        $classMetadata = $this->createMock(ClassMetadata::class);
506
507
        $this->entityManager->expects($this->once())
508
            ->method('getClassMetadata')
509
            ->with($entityName)
510
            ->willReturn($classMetadata);
511
512
        $classMetadata->expects($this->once())
513
            ->method('getAssociationMappings')
514
            ->willReturn([$mappingData]);
515
516
        $cascadeService->saveAssociations($this->entityManager, $entityName, $entity);
517
    }
518
519
    /**
520
     * @return array
521
     */
522
    public function getSaveAssociationsWillSkipAssociationsWithNonCascadePersistMappingDataData(): array
523
    {
524
        return [
525
            [
526
                [
527
528
                ],
529
            ],
530
531
            [
532
                [
533
                    'targetEntity' => EntityInterface::class,
534
                ],
535
            ],
536
537
            [
538
                [
539
                    'targetEntity' => EntityInterface::class,
540
                    'fieldName' => 'foo',
541
                ],
542
            ],
543
544
            [
545
                [
546
                    'targetEntity' => EntityInterface::class,
547
                    'fieldName' => 'foo',
548
                    'type' => 1
549
                ],
550
            ],
551
552
            [
553
                [
554
                    'targetEntity'     => EntityInterface::class,
555
                    'fieldName'        => 'foo',
556
                    'type'             => 1,
557
                    'isCascadePersist' => false,
558
                ],
559
            ]
560
        ];
561
    }
562
563
    /**
564
     * @param array $mappingData
565
     * @param mixed $returnValue
566
     *
567
     * @dataProvider getSaveAssociationsThrowPersistenceExceptionIfTheAssocValueIsInvalidData
568
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociations
569
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::isValidAssociation
570
     *
571
     * @throws EntityRepositoryException
572
     * @throws PersistenceException
573
     */
574
    public function testSaveAssociationsThrowPersistenceExceptionIfTheAssocValueIsInvalid(
575
        array $mappingData,
576
        $returnValue
577
    ): void {
578
        $cascadeSaveService = new CascadeSaveService($this->logger, $this->options, $this->collectionOptions);
579
580
        $entityName = EntityInterface::class;
581
        $fieldName = 'foo';
582
        $targetEntityName = $mappingData['targetEntity'] = $mappingData['targetEntity'] ?? EntityInterface::class;
583
        $mappingData['fieldName'] = 'foo';
584
585
        /** @var EntityInterface|MockObject $entity */
586
        $entity = new class ($returnValue) implements EntityInterface {
587
            use EntityTrait;
588
589
            /**
590
             * @var mixed
591
             */
592
            private $returnValue;
593
594
            public function __construct($returnValue)
595
            {
596
                $this->returnValue = $returnValue;
597
            }
598
599
            public function getFoo()
600
            {
601
                return $this->returnValue;
602
            }
603
        };
604
605
        /** @var ClassMetadata|MockObject $metadata */
606
        $metadata = $this->createMock(ClassMetadata::class);
607
608
        /** @var ClassMetadata|MockObject $targetMetadata */
609
        $targetMetadata = $this->createMock(ClassMetadata::class);
610
611
        $this->entityManager->expects($this->exactly(2))
612
            ->method('getClassMetadata')
613
            ->withConsecutive(
614
                [$entityName],
615
                [$targetEntityName]
616
            )->willReturnOnConsecutiveCalls(
617
                $metadata,
618
                $targetMetadata
619
            );
620
621
        $mappings = [
622
            $mappingData
623
        ];
624
625
        $metadata->expects($this->once())
626
            ->method('getAssociationMappings')
627
            ->willReturn($mappings);
628
629
        $this->logger->expects($this->exactly(2))
630
            ->method('info')
631
            ->withConsecutive(
632
                [
633
                    sprintf('Processing cascade save operations for for entity class \'%s\'', $entityName)
634
                ],
635
                [
636
                    sprintf(
637
                        'The entity field \'%s::%s\' is configured for cascade operations for target entity \'%s\'',
638
                        $entityName,
639
                        $fieldName,
640
                        $targetEntityName
641
                    )
642
                ]
643
            );
644
645
        $errorMessage = sprintf('The entity field \'%s::%s\' value could not be resolved', $entityName, $fieldName);
646
647
        $this->logger->expects($this->once())
648
            ->method('error')
649
            ->with($errorMessage);
650
651
        $this->expectException(PersistenceException::class);
652
        $this->expectExceptionMessage($errorMessage);
653
654
        $cascadeSaveService->saveAssociations($this->entityManager, $entityName, $entity);
655
    }
656
657
    /**
658
     * @return array|\array[][]
659
     */
660
    public function getSaveAssociationsThrowPersistenceExceptionIfTheAssocValueIsInvalidData(): array
661
    {
662
        return [
663
            // The return value of the assoc is NULL but the field mapping does not allow NULL should raise an error.
664
            [
665
                [
666
                    'type'             => ClassMetadata::MANY_TO_ONE,
667
                    'isCascadePersist' => true,
668
                    'joinColumns'      => [
669
                        [
670
                            'nullable' => false,
671
                        ],
672
                    ],
673
                ],
674
                null
675
            ],
676
677
            // The return value of the assoc is not a EntityInterface or iterable collection
678
            [
679
                [
680
                    'type'             => ClassMetadata::MANY_TO_ONE,
681
                    'isCascadePersist' => true,
682
                    'joinColumns'      => [
683
                        [
684
                            'nullable' => false,
685
                        ],
686
                    ],
687
                ],
688
                new \stdClass(),
689
            ],
690
691
        ];
692
    }
693
694
    /**
695
     * @param array                               $mappingData
696
     * @param iterable|EntityInterface|MockObject $returnValue
697
     *
698
     * @dataProvider getSaveAssociationsData
699
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::saveAssociations
700
     * @covers \Arp\DoctrineEntityRepository\Persistence\CascadeSaveService::resolveTargetEntityOrCollection
701
     *
702
     * @throws EntityRepositoryException
703
     * @throws PersistenceException
704
     */
705
    public function testSaveAssociations(array $mappingData, $returnValue): void
706
    {
707
        $this->options = [
708
            'foo' => 1,
709
        ];
710
711
        $this->collectionOptions = [
712
            'bar' => 2,
713
        ];
714
715
        if ($returnValue instanceof EntityInterface) {
716
            $saveOptions = $this->options;
717
        } else {
718
            $saveOptions = $this->collectionOptions;
719
        }
720
721
        /** @var CascadeSaveService|MockObject $cascadeSaveService */
722
        $cascadeSaveService = $this->getMockBuilder(CascadeSaveService::class)
723
            ->setConstructorArgs([$this->logger, $this->options, $this->collectionOptions])
724
            ->onlyMethods(['saveAssociation'])
725
            ->getMock();
726
727
        $entityName = EntityInterface::class;
728
        $fieldName = 'foo';
729
        $targetEntityName = $mappingData['targetEntity'] = $mappingData['targetEntity'] ?? EntityInterface::class;
730
        $mappingData['fieldName'] = 'foo';
731
732
        /** @var EntityInterface|MockObject $entity */
733
        $entity = new class ($returnValue) implements EntityInterface {
734
            use EntityTrait;
735
736
            /**
737
             * @var mixed
738
             */
739
            private $returnValue;
740
741
            public function __construct($returnValue)
742
            {
743
                $this->returnValue = $returnValue;
744
            }
745
746
            public function getFoo()
747
            {
748
                return $this->returnValue;
749
            }
750
        };
751
752
        /** @var ClassMetadata|MockObject $metadata */
753
        $metadata = $this->createMock(ClassMetadata::class);
754
755
        /** @var ClassMetadata|MockObject $targetMetadata */
756
        $targetMetadata = $this->createMock(ClassMetadata::class);
757
758
        $this->entityManager->expects($this->exactly(2))
759
            ->method('getClassMetadata')
760
            ->withConsecutive(
761
                [$entityName],
762
                [$targetEntityName]
763
            )->willReturnOnConsecutiveCalls(
764
                $metadata,
765
                $targetMetadata
766
            );
767
768
        $mappings = [
769
            $mappingData,
770
        ];
771
772
        $metadata->expects($this->once())
773
            ->method('getAssociationMappings')
774
            ->willReturn($mappings);
775
776
        $this->logger->expects($this->exactly(3))
777
            ->method('info')
778
            ->withConsecutive(
779
                [
780
                    sprintf('Processing cascade save operations for for entity class \'%s\'', $entityName),
781
                ],
782
                [
783
                    sprintf(
784
                        'The entity field \'%s::%s\' is configured for cascade operations for target entity \'%s\'',
785
                        $entityName,
786
                        $fieldName,
787
                        $targetEntityName
788
                    ),
789
                ],
790
                [
791
                    sprintf('Performing cascading save operations for field \'%s::%s\'', $entityName, $fieldName)
792
                ]
793
            );
794
795
        $cascadeSaveService->expects($this->once())
796
            ->method('saveAssociation')
797
            ->with($this->entityManager, $targetEntityName, $returnValue, $saveOptions);
798
799
        $cascadeSaveService->saveAssociations($this->entityManager, $entityName, $entity);
800
    }
801
802
    /**
803
     * @return array|\array[][]
804
     */
805
    public function getSaveAssociationsData(): array
806
    {
807
        /** @var EntityInterface[]|MockObject[] $collection */
808
        $collection = [
809
            $this->getMockForAbstractClass(EntityInterface::class),
810
            $this->getMockForAbstractClass(EntityInterface::class),
811
            $this->getMockForAbstractClass(EntityInterface::class),
812
        ];
813
        $iteratorCollection = new \ArrayIterator($collection);
814
815
        return [
816
            // Save a single entity association
817
            [
818
                [
819
                    'type'             => ClassMetadata::MANY_TO_ONE,
820
                    'isCascadePersist' => true,
821
                    'joinColumns'      => [
822
                        [
823
                            'nullable' => false,
824
                        ],
825
                    ],
826
                ],
827
                $this->getMockForAbstractClass(EntityInterface::class)
828
            ],
829
830
            // Save a collection association
831
            [
832
                [
833
                    'type'             => ClassMetadata::ONE_TO_MANY,
834
                    'isCascadePersist' => true,
835
                ],
836
                $collection
837
            ],
838
839
            // Save an iterable collection association
840
            [
841
                [
842
                    'type'             => ClassMetadata::MANY_TO_MANY,
843
                    'isCascadePersist' => true,
844
                ],
845
                $iteratorCollection
846
            ],
847
        ];
848
    }
849
}
850