1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Serializer\Tests\Normalizer;
13
14use Doctrine\Common\Annotations\AnnotationReader;
15use PHPUnit\Framework\TestCase;
16use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
17use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
18use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
19use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
20use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
21use Symfony\Component\Serializer\Serializer;
22use Symfony\Component\Serializer\SerializerInterface;
23use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
24use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
25use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
26use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
27
28class GetSetMethodNormalizerTest extends TestCase
29{
30    /**
31     * @var GetSetMethodNormalizer
32     */
33    private $normalizer;
34    /**
35     * @var SerializerInterface
36     */
37    private $serializer;
38
39    protected function setUp()
40    {
41        $this->serializer = $this->getMockBuilder(SerializerNormalizer::class)->getMock();
42        $this->normalizer = new GetSetMethodNormalizer();
43        $this->normalizer->setSerializer($this->serializer);
44    }
45
46    public function testInterface()
47    {
48        $this->assertInstanceOf('Symfony\Component\Serializer\Normalizer\NormalizerInterface', $this->normalizer);
49        $this->assertInstanceOf('Symfony\Component\Serializer\Normalizer\DenormalizerInterface', $this->normalizer);
50    }
51
52    public function testNormalize()
53    {
54        $obj = new GetSetDummy();
55        $object = new \stdClass();
56        $obj->setFoo('foo');
57        $obj->setBar('bar');
58        $obj->setBaz(true);
59        $obj->setCamelCase('camelcase');
60        $obj->setObject($object);
61
62        $this->serializer
63            ->expects($this->once())
64            ->method('normalize')
65            ->with($object, 'any')
66            ->willReturn('string_object')
67        ;
68
69        $this->assertEquals(
70            [
71                'foo' => 'foo',
72                'bar' => 'bar',
73                'baz' => true,
74                'fooBar' => 'foobar',
75                'camelCase' => 'camelcase',
76                'object' => 'string_object',
77            ],
78            $this->normalizer->normalize($obj, 'any')
79        );
80    }
81
82    public function testDenormalize()
83    {
84        $obj = $this->normalizer->denormalize(
85            ['foo' => 'foo', 'bar' => 'bar', 'baz' => true, 'fooBar' => 'foobar'],
86            GetSetDummy::class,
87            'any'
88        );
89        $this->assertEquals('foo', $obj->getFoo());
90        $this->assertEquals('bar', $obj->getBar());
91        $this->assertTrue($obj->isBaz());
92    }
93
94    public function testIgnoredAttributesInContext()
95    {
96        $ignoredAttributes = ['foo', 'bar', 'baz', 'object'];
97        $this->normalizer->setIgnoredAttributes($ignoredAttributes);
98        $obj = new GetSetDummy();
99        $obj->setFoo('foo');
100        $obj->setBar('bar');
101        $obj->setCamelCase(true);
102        $this->assertEquals(
103            [
104                'fooBar' => 'foobar',
105                'camelCase' => true,
106            ],
107            $this->normalizer->normalize($obj, 'any')
108        );
109    }
110
111    public function testDenormalizeWithObject()
112    {
113        $data = new \stdClass();
114        $data->foo = 'foo';
115        $data->bar = 'bar';
116        $data->fooBar = 'foobar';
117        $obj = $this->normalizer->denormalize($data, GetSetDummy::class, 'any');
118        $this->assertEquals('foo', $obj->getFoo());
119        $this->assertEquals('bar', $obj->getBar());
120    }
121
122    public function testDenormalizeNull()
123    {
124        $this->assertEquals(new GetSetDummy(), $this->normalizer->denormalize(null, GetSetDummy::class));
125    }
126
127    public function testConstructorDenormalize()
128    {
129        $obj = $this->normalizer->denormalize(
130            ['foo' => 'foo', 'bar' => 'bar', 'baz' => true, 'fooBar' => 'foobar'],
131            GetConstructorDummy::class, 'any');
132        $this->assertEquals('foo', $obj->getFoo());
133        $this->assertEquals('bar', $obj->getBar());
134        $this->assertTrue($obj->isBaz());
135    }
136
137    public function testConstructorDenormalizeWithNullArgument()
138    {
139        $obj = $this->normalizer->denormalize(
140            ['foo' => 'foo', 'bar' => null, 'baz' => true],
141            GetConstructorDummy::class, 'any');
142        $this->assertEquals('foo', $obj->getFoo());
143        $this->assertNull($obj->getBar());
144        $this->assertTrue($obj->isBaz());
145    }
146
147    public function testConstructorDenormalizeWithMissingOptionalArgument()
148    {
149        $obj = $this->normalizer->denormalize(
150            ['foo' => 'test', 'baz' => [1, 2, 3]],
151            GetConstructorOptionalArgsDummy::class, 'any');
152        $this->assertEquals('test', $obj->getFoo());
153        $this->assertEquals([], $obj->getBar());
154        $this->assertEquals([1, 2, 3], $obj->getBaz());
155    }
156
157    public function testConstructorDenormalizeWithOptionalDefaultArgument()
158    {
159        $obj = $this->normalizer->denormalize(
160            ['bar' => 'test'],
161            GetConstructorArgsWithDefaultValueDummy::class, 'any');
162        $this->assertEquals([], $obj->getFoo());
163        $this->assertEquals('test', $obj->getBar());
164    }
165
166    /**
167     * @requires PHP 5.6
168     */
169    public function testConstructorDenormalizeWithVariadicArgument()
170    {
171        $obj = $this->normalizer->denormalize(
172            ['foo' => [1, 2, 3]],
173            'Symfony\Component\Serializer\Tests\Fixtures\VariadicConstructorArgsDummy', 'any');
174        $this->assertEquals([1, 2, 3], $obj->getFoo());
175    }
176
177    /**
178     * @requires PHP 5.6
179     */
180    public function testConstructorDenormalizeWithMissingVariadicArgument()
181    {
182        $obj = $this->normalizer->denormalize(
183            [],
184            'Symfony\Component\Serializer\Tests\Fixtures\VariadicConstructorArgsDummy', 'any');
185        $this->assertEquals([], $obj->getFoo());
186    }
187
188    public function testConstructorWithObjectDenormalize()
189    {
190        $data = new \stdClass();
191        $data->foo = 'foo';
192        $data->bar = 'bar';
193        $data->baz = true;
194        $data->fooBar = 'foobar';
195        $obj = $this->normalizer->denormalize($data, GetConstructorDummy::class, 'any');
196        $this->assertEquals('foo', $obj->getFoo());
197        $this->assertEquals('bar', $obj->getBar());
198    }
199
200    public function testConstructorWArgWithPrivateMutator()
201    {
202        $obj = $this->normalizer->denormalize(['foo' => 'bar'], ObjectConstructorArgsWithPrivateMutatorDummy::class, 'any');
203        $this->assertEquals('bar', $obj->getFoo());
204    }
205
206    public function testGroupsNormalize()
207    {
208        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
209        $this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
210        $this->normalizer->setSerializer($this->serializer);
211
212        $obj = new GroupDummy();
213        $obj->setFoo('foo');
214        $obj->setBar('bar');
215        $obj->setFooBar('fooBar');
216        $obj->setSymfony('symfony');
217        $obj->setKevin('kevin');
218        $obj->setCoopTilleuls('coopTilleuls');
219
220        $this->assertEquals([
221            'bar' => 'bar',
222        ], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['c']]));
223
224        $this->assertEquals([
225            'symfony' => 'symfony',
226            'foo' => 'foo',
227            'fooBar' => 'fooBar',
228            'bar' => 'bar',
229            'kevin' => 'kevin',
230            'coopTilleuls' => 'coopTilleuls',
231        ], $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['a', 'c']]));
232    }
233
234    public function testGroupsDenormalize()
235    {
236        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
237        $this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
238        $this->normalizer->setSerializer($this->serializer);
239
240        $obj = new GroupDummy();
241        $obj->setFoo('foo');
242
243        $toNormalize = ['foo' => 'foo', 'bar' => 'bar'];
244
245        $normalized = $this->normalizer->denormalize(
246            $toNormalize,
247            'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
248            null,
249            [GetSetMethodNormalizer::GROUPS => ['a']]
250        );
251        $this->assertEquals($obj, $normalized);
252
253        $obj->setBar('bar');
254
255        $normalized = $this->normalizer->denormalize(
256            $toNormalize,
257            'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy',
258            null,
259            [GetSetMethodNormalizer::GROUPS => ['a', 'b']]
260        );
261        $this->assertEquals($obj, $normalized);
262    }
263
264    public function testGroupsNormalizeWithNameConverter()
265    {
266        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
267        $this->normalizer = new GetSetMethodNormalizer($classMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
268        $this->normalizer->setSerializer($this->serializer);
269
270        $obj = new GroupDummy();
271        $obj->setFooBar('@dunglas');
272        $obj->setSymfony('@coopTilleuls');
273        $obj->setCoopTilleuls('les-tilleuls.coop');
274
275        $this->assertEquals(
276            [
277                'bar' => null,
278                'foo_bar' => '@dunglas',
279                'symfony' => '@coopTilleuls',
280            ],
281            $this->normalizer->normalize($obj, null, [GetSetMethodNormalizer::GROUPS => ['name_converter']])
282        );
283    }
284
285    public function testGroupsDenormalizeWithNameConverter()
286    {
287        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
288        $this->normalizer = new GetSetMethodNormalizer($classMetadataFactory, new CamelCaseToSnakeCaseNameConverter());
289        $this->normalizer->setSerializer($this->serializer);
290
291        $obj = new GroupDummy();
292        $obj->setFooBar('@dunglas');
293        $obj->setSymfony('@coopTilleuls');
294
295        $this->assertEquals(
296            $obj,
297            $this->normalizer->denormalize([
298                'bar' => null,
299                'foo_bar' => '@dunglas',
300                'symfony' => '@coopTilleuls',
301                'coop_tilleuls' => 'les-tilleuls.coop',
302            ], 'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy', null, [GetSetMethodNormalizer::GROUPS => ['name_converter']])
303        );
304    }
305
306    /**
307     * @dataProvider provideCallbacks
308     */
309    public function testCallbacks($callbacks, $value, $result, $message)
310    {
311        $this->normalizer->setCallbacks($callbacks);
312
313        $obj = new GetConstructorDummy('', $value, true);
314
315        $this->assertEquals(
316            $result,
317            $this->normalizer->normalize($obj, 'any'),
318            $message
319        );
320    }
321
322    public function testUncallableCallbacks()
323    {
324        $this->expectException('InvalidArgumentException');
325        $this->normalizer->setCallbacks(['bar' => null]);
326
327        $obj = new GetConstructorDummy('baz', 'quux', true);
328
329        $this->normalizer->normalize($obj, 'any');
330    }
331
332    public function testIgnoredAttributes()
333    {
334        $this->normalizer->setIgnoredAttributes(['foo', 'bar', 'baz', 'camelCase', 'object']);
335
336        $obj = new GetSetDummy();
337        $obj->setFoo('foo');
338        $obj->setBar('bar');
339        $obj->setBaz(true);
340
341        $this->assertEquals(
342            ['fooBar' => 'foobar'],
343            $this->normalizer->normalize($obj, 'any')
344        );
345    }
346
347    public function provideCallbacks()
348    {
349        return [
350            [
351                [
352                    'bar' => function ($bar) {
353                        return 'baz';
354                    },
355                ],
356                'baz',
357                ['foo' => '', 'bar' => 'baz', 'baz' => true],
358                'Change a string',
359            ],
360            [
361                [
362                    'bar' => function ($bar) {
363                    },
364                ],
365                'baz',
366                ['foo' => '', 'bar' => null, 'baz' => true],
367                'Null an item',
368            ],
369            [
370                [
371                    'bar' => function ($bar) {
372                        return $bar->format('d-m-Y H:i:s');
373                    },
374                ],
375                new \DateTime('2011-09-10 06:30:00'),
376                ['foo' => '', 'bar' => '10-09-2011 06:30:00', 'baz' => true],
377                'Format a date',
378            ],
379            [
380                [
381                    'bar' => function ($bars) {
382                        $foos = '';
383                        foreach ($bars as $bar) {
384                            $foos .= $bar->getFoo();
385                        }
386
387                        return $foos;
388                    },
389                ],
390                [new GetConstructorDummy('baz', '', false), new GetConstructorDummy('quux', '', false)],
391                ['foo' => '', 'bar' => 'bazquux', 'baz' => true],
392                'Collect a property',
393            ],
394            [
395                [
396                    'bar' => function ($bars) {
397                        return \count($bars);
398                    },
399                ],
400                [new GetConstructorDummy('baz', '', false), new GetConstructorDummy('quux', '', false)],
401                ['foo' => '', 'bar' => 2, 'baz' => true],
402                'Count a property',
403            ],
404        ];
405    }
406
407    public function testUnableToNormalizeObjectAttribute()
408    {
409        $this->expectException('Symfony\Component\Serializer\Exception\LogicException');
410        $this->expectExceptionMessage('Cannot normalize attribute "object" because the injected serializer is not a normalizer');
411        $serializer = $this->getMockBuilder('Symfony\Component\Serializer\SerializerInterface')->getMock();
412        $this->normalizer->setSerializer($serializer);
413
414        $obj = new GetSetDummy();
415        $object = new \stdClass();
416        $obj->setObject($object);
417
418        $this->normalizer->normalize($obj, 'any');
419    }
420
421    public function testUnableToNormalizeCircularReference()
422    {
423        $this->expectException('Symfony\Component\Serializer\Exception\CircularReferenceException');
424        $serializer = new Serializer([$this->normalizer]);
425        $this->normalizer->setSerializer($serializer);
426        $this->normalizer->setCircularReferenceLimit(2);
427
428        $obj = new CircularReferenceDummy();
429
430        $this->normalizer->normalize($obj);
431    }
432
433    public function testSiblingReference()
434    {
435        $serializer = new Serializer([$this->normalizer]);
436        $this->normalizer->setSerializer($serializer);
437
438        $siblingHolder = new SiblingHolder();
439
440        $expected = [
441            'sibling0' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
442            'sibling1' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
443            'sibling2' => ['coopTilleuls' => 'Les-Tilleuls.coop'],
444        ];
445        $this->assertEquals($expected, $this->normalizer->normalize($siblingHolder));
446    }
447
448    public function testCircularReferenceHandler()
449    {
450        $serializer = new Serializer([$this->normalizer]);
451        $this->normalizer->setSerializer($serializer);
452        $this->normalizer->setCircularReferenceHandler(function ($obj) {
453            return \get_class($obj);
454        });
455
456        $obj = new CircularReferenceDummy();
457
458        $expected = ['me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy'];
459        $this->assertEquals($expected, $this->normalizer->normalize($obj));
460    }
461
462    public function testObjectToPopulate()
463    {
464        $dummy = new GetSetDummy();
465        $dummy->setFoo('foo');
466
467        $obj = $this->normalizer->denormalize(
468            ['bar' => 'bar'],
469            GetSetDummy::class,
470            null,
471            [GetSetMethodNormalizer::OBJECT_TO_POPULATE => $dummy]
472        );
473
474        $this->assertEquals($dummy, $obj);
475        $this->assertEquals('foo', $obj->getFoo());
476        $this->assertEquals('bar', $obj->getBar());
477    }
478
479    public function testDenormalizeNonExistingAttribute()
480    {
481        $this->assertEquals(
482            new GetSetDummy(),
483            $this->normalizer->denormalize(['non_existing' => true], GetSetDummy::class)
484        );
485    }
486
487    public function testDenormalizeShouldNotSetStaticAttribute()
488    {
489        $obj = $this->normalizer->denormalize(['staticObject' => true], GetSetDummy::class);
490
491        $this->assertEquals(new GetSetDummy(), $obj);
492        $this->assertNull(GetSetDummy::getStaticObject());
493    }
494
495    public function testNoTraversableSupport()
496    {
497        $this->assertFalse($this->normalizer->supportsNormalization(new \ArrayObject()));
498    }
499
500    public function testNoStaticGetSetSupport()
501    {
502        $this->assertFalse($this->normalizer->supportsNormalization(new ObjectWithJustStaticSetterDummy()));
503    }
504
505    public function testPrivateSetter()
506    {
507        $obj = $this->normalizer->denormalize(['foo' => 'foobar'], ObjectWithPrivateSetterDummy::class);
508        $this->assertEquals('bar', $obj->getFoo());
509    }
510
511    public function testHasGetterDenormalize()
512    {
513        $obj = $this->normalizer->denormalize(['foo' => true], ObjectWithHasGetterDummy::class);
514        $this->assertTrue($obj->hasFoo());
515    }
516
517    public function testHasGetterNormalize()
518    {
519        $obj = new ObjectWithHasGetterDummy();
520        $obj->setFoo(true);
521
522        $this->assertEquals(
523            ['foo' => true],
524            $this->normalizer->normalize($obj, 'any')
525        );
526    }
527
528    public function testMaxDepth()
529    {
530        $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
531        $this->normalizer = new GetSetMethodNormalizer($classMetadataFactory);
532        $serializer = new Serializer([$this->normalizer]);
533        $this->normalizer->setSerializer($serializer);
534
535        $level1 = new MaxDepthDummy();
536        $level1->bar = 'level1';
537
538        $level2 = new MaxDepthDummy();
539        $level2->bar = 'level2';
540        $level1->child = $level2;
541
542        $level3 = new MaxDepthDummy();
543        $level3->bar = 'level3';
544        $level2->child = $level3;
545
546        $level4 = new MaxDepthDummy();
547        $level4->bar = 'level4';
548        $level3->child = $level4;
549
550        $result = $serializer->normalize($level1, null, [GetSetMethodNormalizer::ENABLE_MAX_DEPTH => true]);
551
552        $expected = [
553            'bar' => 'level1',
554            'child' => [
555                    'bar' => 'level2',
556                    'child' => [
557                            'bar' => 'level3',
558                            'child' => [
559                                    'child' => null,
560                                ],
561                        ],
562                ],
563            ];
564
565        $this->assertEquals($expected, $result);
566    }
567}
568
569class GetSetDummy
570{
571    protected $foo;
572    private $bar;
573    private $baz;
574    protected $camelCase;
575    protected $object;
576    private static $staticObject;
577
578    public function getFoo()
579    {
580        return $this->foo;
581    }
582
583    public function setFoo($foo)
584    {
585        $this->foo = $foo;
586    }
587
588    public function getBar()
589    {
590        return $this->bar;
591    }
592
593    public function setBar($bar)
594    {
595        $this->bar = $bar;
596    }
597
598    public function isBaz()
599    {
600        return $this->baz;
601    }
602
603    public function setBaz($baz)
604    {
605        $this->baz = $baz;
606    }
607
608    public function getFooBar()
609    {
610        return $this->foo.$this->bar;
611    }
612
613    public function getCamelCase()
614    {
615        return $this->camelCase;
616    }
617
618    public function setCamelCase($camelCase)
619    {
620        $this->camelCase = $camelCase;
621    }
622
623    public function otherMethod()
624    {
625        throw new \RuntimeException('Dummy::otherMethod() should not be called');
626    }
627
628    public function setObject($object)
629    {
630        $this->object = $object;
631    }
632
633    public function getObject()
634    {
635        return $this->object;
636    }
637
638    public static function getStaticObject()
639    {
640        return self::$staticObject;
641    }
642
643    public static function setStaticObject($object)
644    {
645        self::$staticObject = $object;
646    }
647
648    protected function getPrivate()
649    {
650        throw new \RuntimeException('Dummy::getPrivate() should not be called');
651    }
652}
653
654class GetConstructorDummy
655{
656    protected $foo;
657    private $bar;
658    private $baz;
659
660    public function __construct($foo, $bar, $baz)
661    {
662        $this->foo = $foo;
663        $this->bar = $bar;
664        $this->baz = $baz;
665    }
666
667    public function getFoo()
668    {
669        return $this->foo;
670    }
671
672    public function getBar()
673    {
674        return $this->bar;
675    }
676
677    public function isBaz()
678    {
679        return $this->baz;
680    }
681
682    public function otherMethod()
683    {
684        throw new \RuntimeException('Dummy::otherMethod() should not be called');
685    }
686}
687
688abstract class SerializerNormalizer implements SerializerInterface, NormalizerInterface
689{
690}
691
692class GetConstructorOptionalArgsDummy
693{
694    protected $foo;
695    private $bar;
696    private $baz;
697
698    public function __construct($foo, $bar = [], $baz = [])
699    {
700        $this->foo = $foo;
701        $this->bar = $bar;
702        $this->baz = $baz;
703    }
704
705    public function getFoo()
706    {
707        return $this->foo;
708    }
709
710    public function getBar()
711    {
712        return $this->bar;
713    }
714
715    public function getBaz()
716    {
717        return $this->baz;
718    }
719
720    public function otherMethod()
721    {
722        throw new \RuntimeException('Dummy::otherMethod() should not be called');
723    }
724}
725
726class GetConstructorArgsWithDefaultValueDummy
727{
728    protected $foo;
729    protected $bar;
730
731    public function __construct($foo = [], $bar = null)
732    {
733        $this->foo = $foo;
734        $this->bar = $bar;
735    }
736
737    public function getFoo()
738    {
739        return $this->foo;
740    }
741
742    public function getBar()
743    {
744        return $this->bar;
745    }
746
747    public function otherMethod()
748    {
749        throw new \RuntimeException('Dummy::otherMethod() should not be called');
750    }
751}
752
753class ObjectConstructorArgsWithPrivateMutatorDummy
754{
755    private $foo;
756
757    public function __construct($foo)
758    {
759        $this->setFoo($foo);
760    }
761
762    public function getFoo()
763    {
764        return $this->foo;
765    }
766
767    private function setFoo($foo)
768    {
769        $this->foo = $foo;
770    }
771}
772
773class ObjectWithPrivateSetterDummy
774{
775    private $foo = 'bar';
776
777    public function getFoo()
778    {
779        return $this->foo;
780    }
781
782    private function setFoo($foo)
783    {
784    }
785}
786
787class ObjectWithJustStaticSetterDummy
788{
789    private static $foo = 'bar';
790
791    public static function getFoo()
792    {
793        return self::$foo;
794    }
795
796    public static function setFoo($foo)
797    {
798        self::$foo = $foo;
799    }
800}
801
802class ObjectWithHasGetterDummy
803{
804    private $foo;
805
806    public function setFoo($foo)
807    {
808        $this->foo = $foo;
809    }
810
811    public function hasFoo()
812    {
813        return $this->foo;
814    }
815}
816