1<?php
2
3namespace JMS\SerializerBundle\Tests\DependencyInjection;
4
5use Doctrine\Common\Annotations\AnnotationReader;
6use JMS\Serializer\Handler\SubscribingHandlerInterface;
7use JMS\Serializer\SerializationContext;
8use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
9use JMS\SerializerBundle\JMSSerializerBundle;
10use JMS\SerializerBundle\Tests\DependencyInjection\Fixture\ObjectUsingExpressionLanguage;
11use JMS\SerializerBundle\Tests\DependencyInjection\Fixture\ObjectUsingExpressionProperties;
12use JMS\SerializerBundle\Tests\DependencyInjection\Fixture\SimpleObject;
13use JMS\SerializerBundle\Tests\DependencyInjection\Fixture\VersionedObject;
14use PHPUnit\Framework\TestCase;
15use Symfony\Component\DependencyInjection\ContainerBuilder;
16use Symfony\Component\DependencyInjection\Definition;
17
18class JMSSerializerExtensionTest extends TestCase
19{
20    protected function setUp()
21    {
22        $this->clearTempDir();
23    }
24
25    protected function tearDown()
26    {
27        $this->clearTempDir();
28    }
29
30    private function clearTempDir()
31    {
32        // clear temporary directory
33        $dir = sys_get_temp_dir() . '/serializer';
34        if (is_dir($dir)) {
35            foreach (new \RecursiveDirectoryIterator($dir) as $file) {
36                $filename = $file->getFileName();
37                if ('.' === $filename || '..' === $filename) {
38                    continue;
39                }
40
41                @unlink($file->getPathName());
42            }
43
44            @rmdir($dir);
45        }
46    }
47
48    public function testHasContextFactories()
49    {
50        $container = $this->getContainerForConfig(array(array()));
51
52        $factory = $container->get('jms_serializer.serialization_context_factory');
53        $this->assertInstanceOf('JMS\Serializer\ContextFactory\SerializationContextFactoryInterface', $factory);
54
55        $factory = $container->get('jms_serializer.deserialization_context_factory');
56        $this->assertInstanceOf('JMS\Serializer\ContextFactory\DeserializationContextFactoryInterface', $factory);
57    }
58
59    public function testSerializerContextFactoriesAreSet()
60    {
61        $container = $this->getContainerForConfig(array(array()));
62
63        $def = $container->getDefinition('jms_serializer');
64        $calls = $def->getMethodCalls();
65
66        $this->assertCount(2, $calls);
67
68        $serializationCall = $calls[0];
69        $this->assertEquals('setSerializationContextFactory', $serializationCall[0]);
70        $this->assertEquals('jms_serializer.serialization_context_factory', (string)$serializationCall[1][0]);
71
72        $serializationCall = $calls[1];
73        $this->assertEquals('setDeserializationContextFactory', $serializationCall[0]);
74        $this->assertEquals('jms_serializer.deserialization_context_factory', (string)$serializationCall[1][0]);
75    }
76
77    public function testSerializerContextFactoriesWithId()
78    {
79        $config = array(
80            'default_context' => array(
81                'serialization' => array(
82                    'id' => 'foo'
83                ),
84                'deserialization' => array(
85                    'id' => 'bar'
86                )
87            )
88        );
89
90        $foo = new Definition('stdClass');
91        $foo->setPublic(true);
92        $bar = new Definition('stdClass');
93        $bar->setPublic(true);
94
95        $container = $this->getContainerForConfig(array($config), function (ContainerBuilder $containerBuilder) use ($foo, $bar) {
96            $containerBuilder->setDefinition('foo', $foo);
97            $containerBuilder->setDefinition('bar', $bar);
98        });
99
100        $def = $container->getDefinition('jms_serializer');
101        $calls = $def->getMethodCalls();
102
103        $this->assertCount(2, $calls);
104
105        $serializationCall = $calls[0];
106        $this->assertEquals('setSerializationContextFactory', $serializationCall[0]);
107        $this->assertEquals('foo', (string)$serializationCall[1][0]);
108
109        $serializationCall = $calls[1];
110        $this->assertEquals('setDeserializationContextFactory', $serializationCall[0]);
111        $this->assertEquals('bar', (string)$serializationCall[1][0]);
112
113        $this->assertEquals('bar', (string)$container->getAlias('jms_serializer.deserialization_context_factory'));
114        $this->assertEquals('foo', (string)$container->getAlias('jms_serializer.serialization_context_factory'));
115    }
116
117    public function testLoadWithoutTranslator()
118    {
119        $container = $this->getContainerForConfig(array(array()), function (ContainerBuilder $containerBuilder) {
120            $containerBuilder->set('translator', null);
121            $containerBuilder->getDefinition('jms_serializer.form_error_handler')->setPublic(true);
122        });
123
124        $def = $container->getDefinition('jms_serializer.form_error_handler');
125        $this->assertSame(null, $def->getArgument(0));
126    }
127
128    public function testConfiguringContextFactories()
129    {
130        $container = $this->getContainerForConfig(array(array()));
131
132        $def = $container->getDefinition('jms_serializer.serialization_context_factory');
133        $this->assertCount(0, $def->getMethodCalls());
134
135        $def = $container->getDefinition('jms_serializer.deserialization_context_factory');
136        $this->assertCount(0, $def->getMethodCalls());
137    }
138
139    public function testConfiguringContextFactoriesWithParams()
140    {
141        $config = array(
142            'default_context' => array(
143                'serialization' => array(
144                    'version' => 1600,
145                    'serialize_null' => true,
146                    'attributes' => array('x' => 1720),
147                    'groups' => array('Default', 'Registration'),
148                    'enable_max_depth_checks' => true,
149                ),
150                'deserialization' => array(
151                    'version' => 1640,
152                    'serialize_null' => false,
153                    'attributes' => array('x' => 1740),
154                    'groups' => array('Default', 'Profile'),
155                    'enable_max_depth_checks' => true,
156                )
157            )
158        );
159
160        $container = $this->getContainerForConfig(array($config));
161        $services = [
162            'serialization' => 'jms_serializer.serialization_context_factory',
163            'deserialization' => 'jms_serializer.deserialization_context_factory',
164        ];
165        foreach ($services as $configKey => $serviceId) {
166            $def = $container->getDefinition($serviceId);
167            $values = $config['default_context'][$configKey];
168
169            $this->assertSame($values['version'], $this->getDefinitionMethodCall($def, 'setVersion')[0]);
170            $this->assertSame($values['serialize_null'], $this->getDefinitionMethodCall($def, 'setSerializeNulls')[0]);
171            $this->assertSame($values['attributes'], $this->getDefinitionMethodCall($def, 'setAttributes')[0]);
172            $this->assertSame($values['groups'], $this->getDefinitionMethodCall($def, 'setGroups')[0]);
173            $this->assertSame($values['groups'], $this->getDefinitionMethodCall($def, 'setGroups')[0]);
174            $this->assertSame(array(), $this->getDefinitionMethodCall($def, 'enableMaxDepthChecks'));
175        }
176    }
177
178    public function testConfiguringContextFactoriesWithNullDefaults()
179    {
180        $config = array(
181            'default_context' => array(
182                'serialization' => array(
183                    'version' => null,
184                    'serialize_null' => null,
185                    'attributes' => [],
186                    'groups' => null,
187                ),
188                'deserialization' => array(
189                    'version' => null,
190                    'serialize_null' => null,
191                    'attributes' => null,
192                    'groups' => null,
193                )
194            )
195        );
196
197        $container = $this->getContainerForConfig(array($config));
198        $services = [
199            'serialization' => 'jms_serializer.serialization_context_factory',
200            'deserialization' => 'jms_serializer.deserialization_context_factory',
201        ];
202        foreach ($services as $configKey => $serviceId) {
203            $def = $container->getDefinition($serviceId);
204            $this->assertCount(0, $def->getMethodCalls());
205        }
206    }
207
208    private function getDefinitionMethodCall(Definition $def, $method)
209    {
210        foreach ($def->getMethodCalls() as $call) {
211            if ($call[0] === $method) {
212                return $call[1];
213            }
214        }
215        return false;
216    }
217
218    public function testLoad()
219    {
220        $container = $this->getContainerForConfig(array(array()), function (ContainerBuilder $container) {
221            $container->getDefinition('jms_serializer.doctrine_object_constructor')->setPublic(true);
222            $container->getDefinition('jms_serializer.array_collection_handler')->setPublic(true);
223            $container->getDefinition('jms_serializer.doctrine_proxy_subscriber')->setPublic(true);
224            $container->getAlias('JMS\Serializer\SerializerInterface')->setPublic(true);
225            $container->getAlias('JMS\Serializer\ArrayTransformerInterface')->setPublic(true);
226        });
227
228        $simpleObject = new SimpleObject('foo', 'bar');
229        $versionedObject = new VersionedObject('foo', 'bar');
230        $serializer = $container->get('jms_serializer');
231
232        $this->assertTrue($container->has('JMS\Serializer\SerializerInterface'), 'Alias should be defined to allow autowiring');
233        $this->assertTrue($container->has('JMS\Serializer\ArrayTransformerInterface'), 'Alias should be defined to allow autowiring');
234
235        $this->assertFalse($container->getDefinition('jms_serializer.array_collection_handler')->getArgument(0));
236
237        // the logic is inverted because arg 0 on doctrine_proxy_subscriber is $skipVirtualTypeInit = false
238        $this->assertTrue($container->getDefinition('jms_serializer.doctrine_proxy_subscriber')->getArgument(0));
239        $this->assertFalse($container->getDefinition('jms_serializer.doctrine_proxy_subscriber')->getArgument(1));
240
241        $this->assertEquals("null", $container->getDefinition('jms_serializer.doctrine_object_constructor')->getArgument(2));
242
243        // test that all components have been wired correctly
244        $this->assertEquals(json_encode(array('name' => 'bar')), $serializer->serialize($versionedObject, 'json'));
245        $this->assertEquals($simpleObject, $serializer->deserialize($serializer->serialize($simpleObject, 'json'), get_class($simpleObject), 'json'));
246        $this->assertEquals($simpleObject, $serializer->deserialize($serializer->serialize($simpleObject, 'xml'), get_class($simpleObject), 'xml'));
247
248        $this->assertEquals(json_encode(array('name' => 'foo')), $serializer->serialize($versionedObject, 'json', SerializationContext::create()->setVersion('0.0.1')));
249
250        $this->assertEquals(json_encode(array('name' => 'bar')), $serializer->serialize($versionedObject, 'json', SerializationContext::create()->setVersion('1.1.1')));
251    }
252
253    public function testLoadWithOptions()
254    {
255        $container = $this->getContainerForConfig(array(array(
256            'subscribers' => [
257                'doctrine_proxy' => [
258                    'initialize_virtual_types' => true,
259                    'initialize_excluded' => true,
260                ],
261            ],
262            'object_constructors' => [
263                'doctrine' => [
264                    'fallback_strategy' => "exception",
265                ],
266            ],
267            'handlers' => [
268                'array_collection' => [
269                    'initialize_excluded' => true,
270                ],
271            ],
272        )), function ($container) {
273            $container->getDefinition('jms_serializer.doctrine_object_constructor')->setPublic(true);
274            $container->getDefinition('jms_serializer.array_collection_handler')->setPublic(true);
275            $container->getDefinition('jms_serializer.doctrine_proxy_subscriber')->setPublic(true);
276        });
277
278        $this->assertTrue($container->getDefinition('jms_serializer.array_collection_handler')->getArgument(0));
279
280        // the logic is inverted because arg 0 on doctrine_proxy_subscriber is $skipVirtualTypeInit = false
281        $this->assertFalse($container->getDefinition('jms_serializer.doctrine_proxy_subscriber')->getArgument(0));
282        $this->assertTrue($container->getDefinition('jms_serializer.doctrine_proxy_subscriber')->getArgument(1));
283
284        $this->assertEquals("exception", $container->getDefinition('jms_serializer.doctrine_object_constructor')->getArgument(2));
285    }
286
287    public function testLoadExistentMetadataDir()
288    {
289        $container = $this->getContainerForConfig(array(array(
290            'metadata' => [
291                'directories' => [
292                    'foo' => [
293                        'namespace_prefix' => 'foo_ns',
294                        'path' => __DIR__,
295                    ]
296                ]
297            ]
298        )), function ($container) {
299            $container->getDefinition('jms_serializer.metadata.file_locator')->setPublic(true);
300        });
301
302        $fileLocatorDef = $container->getDefinition('jms_serializer.metadata.file_locator');
303        $directories = $fileLocatorDef->getArgument(0);
304        $this->assertEquals(['foo_ns' => __DIR__], $directories);
305    }
306
307    public function testWarmUpWithDirs()
308    {
309        $container = $this->getContainerForConfig([[
310            'metadata' => [
311                'warmup' => [
312                    'paths' => [
313                        'included' => ['a'],
314                        'excluded' => ['b']
315                    ]
316                ]
317            ]
318        ]], function ($container){
319            $container->getDefinition('jms_serializer.cache.cache_warmer')->setPublic(true);
320        });
321
322        $this->assertTrue($container->hasDefinition('jms_serializer.cache.cache_warmer'));
323
324        $def = $container->getDefinition('jms_serializer.cache.cache_warmer');
325
326        $this->assertEquals(['a'], $def->getArgument(0));
327        $this->assertEquals(['b'], $def->getArgument(2));
328    }
329
330    public function testWarmUpWithDirsWithNoPaths()
331    {
332        $this->getContainerForConfig([[]], function ($container) {
333            $this->assertFalse($container->hasDefinition('jms_serializer.cache.cache_warmer'));
334        });
335    }
336
337    /**
338     * @expectedException \JMS\Serializer\Exception\RuntimeException
339     * @expectedExceptionMessage  The metadata directory "foo_dir" does not exist for the namespace "foo_ns"
340     */
341    public function testLoadNotExistentMetadataDir()
342    {
343        $this->getContainerForConfig(array(array(
344            'metadata' => [
345                'directories' => [
346                    'foo' => [
347                        'namespace_prefix' => 'foo_ns',
348                        'path' => 'foo_dir',
349                    ]
350                ]
351            ]
352        )));
353    }
354
355    /**
356     * @dataProvider getJsonVisitorConfigs
357     */
358    public function testJsonVisitorOptions($expectedOptions, $config)
359    {
360        $container = $this->getContainerForConfig(array($config), function ($container) {
361            $container->getDefinition('jms_serializer.json_serialization_visitor')->setPublic(true);
362        });
363        $this->assertSame($expectedOptions, $container->get('jms_serializer.json_serialization_visitor')->getOptions());
364    }
365
366    public function getJsonVisitorConfigs()
367    {
368        $configs = array();
369
370        if (version_compare(PHP_VERSION, '5.4', '>=')) {
371            $configs[] = array(JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT, array(
372                'visitors' => array(
373                    'json' => array(
374                        'options' => array('JSON_UNESCAPED_UNICODE', 'JSON_PRETTY_PRINT')
375                    )
376                )
377            ));
378
379            $configs[] = array(JSON_UNESCAPED_UNICODE, array(
380                'visitors' => array(
381                    'json' => array(
382                        'options' => 'JSON_UNESCAPED_UNICODE'
383                    )
384                )
385            ));
386        }
387
388        $configs[] = array(128, array(
389            'visitors' => array(
390                'json' => array(
391                    'options' => 128
392                )
393            )
394        ));
395
396        $configs[] = array(0, array());
397
398        return $configs;
399    }
400
401    public function testExpressionLanguage()
402    {
403        if (!interface_exists('Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface')) {
404            $this->markTestSkipped("The Symfony Expression Language is not available");
405        }
406        $container = $this->getContainerForConfig(array(array()));
407        $serializer = $container->get('jms_serializer');
408        // test that all components have been wired correctly
409        $object = new ObjectUsingExpressionLanguage('foo', true);
410        $this->assertEquals('{"name":"foo"}', $serializer->serialize($object, 'json'));
411        $object = new ObjectUsingExpressionLanguage('foo', false);
412        $this->assertEquals('{}', $serializer->serialize($object, 'json'));
413    }
414
415    public function testExpressionLanguageVirtualProperties()
416    {
417        if (!interface_exists('Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface')) {
418            $this->markTestSkipped("The Symfony Expression Language is not available");
419        }
420        $container = $this->getContainerForConfig(array(array()));
421        $serializer = $container->get('jms_serializer');
422        // test that all components have been wired correctly
423        $object = new ObjectUsingExpressionProperties('foo');
424        $this->assertEquals('{"v_prop_name":"foo"}', $serializer->serialize($object, 'json'));
425    }
426
427    /**
428     * @expectedException \JMS\Serializer\Exception\ExpressionLanguageRequiredException
429     */
430    public function testExpressionLanguageDisabledVirtualProperties()
431    {
432        if (!interface_exists('Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface')) {
433            $this->markTestSkipped("The Symfony Expression Language is not available");
434        }
435        $container = $this->getContainerForConfig(array(array('expression_evaluator' => array('id' => null))));
436        $serializer = $container->get('jms_serializer');
437        // test that all components have been wired correctly
438        $object = new ObjectUsingExpressionProperties('foo');
439        $serializer->serialize($object, 'json');
440    }
441
442    /**
443     * @expectedException \JMS\Serializer\Exception\ExpressionLanguageRequiredException
444     * @expectedExceptionMessage  To use conditional exclude/expose in JMS\SerializerBundle\Tests\DependencyInjection\Fixture\ObjectUsingExpressionLanguage you must configure the expression language.
445     */
446    public function testExpressionLanguageNotLoaded()
447    {
448        $container = $this->getContainerForConfig(array(array('expression_evaluator' => array('id' => null))));
449        $serializer = $container->get('jms_serializer');
450        // test that all components have been wired correctly
451        $object = new ObjectUsingExpressionLanguage('foo', true);
452        $serializer->serialize($object, 'json');
453    }
454
455    /**
456     * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
457     * @expectedExceptionMessage Invalid configuration for path "jms_serializer.expression_evaluator.id": You need at least symfony/expression language v2.6 or v3.0 to use the expression evaluator features
458     */
459    public function testExpressionInvalidEvaluator()
460    {
461        if (interface_exists('Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface')) {
462            $this->markTestSkipped('To pass this test the "symfony/expression-language" component should be available');
463        }
464        $this->getContainerForConfig(array(array('expression_evaluator' => array('id' => 'foo'))));
465    }
466
467    /**
468     * @dataProvider getXmlVisitorWhitelists
469     */
470    public function testXmlVisitorOptions($expectedOptions, $config)
471    {
472        $container = $this->getContainerForConfig(array($config), function ($container) {
473            $container->getDefinition('jms_serializer.xml_deserialization_visitor')->setPublic(true);
474        });
475        $this->assertSame($expectedOptions, $container->get('jms_serializer.xml_deserialization_visitor')->getDoctypeWhitelist());
476    }
477
478    public function getXmlVisitorWhitelists()
479    {
480        $configs = array();
481
482        $configs[] = array(array('good document', 'other good document'), array(
483            'visitors' => array(
484                'xml' => array(
485                    'doctype_whitelist' => array('good document', 'other good document'),
486                )
487            )
488        ));
489
490        $configs[] = array(array(), array());
491
492        return $configs;
493    }
494
495    public function testXmlVisitorFormatOutput()
496    {
497        $config = array(
498            'visitors' => array(
499                'xml' => array(
500                    'format_output' => false,
501                )
502            )
503        );
504        $container = $this->getContainerForConfig(array($config), function ($container) {
505            $container->getDefinition('jms_serializer.xml_serialization_visitor')->setPublic(true);
506        });
507
508        $this->assertFalse($container->get('jms_serializer.xml_serialization_visitor')->isFormatOutput());
509    }
510
511    public function testXmlVisitorDefaultValueToFormatOutput()
512    {
513        $container = $this->getContainerForConfig(array(), function ($container) {
514            $container->getDefinition('jms_serializer.xml_serialization_visitor')->setPublic(true);
515        });
516        $this->assertTrue($container->get('jms_serializer.xml_serialization_visitor')->isFormatOutput());
517    }
518
519    public function testAutoconfigureSubscribers()
520    {
521        $container = $this->getContainerForConfig(array());
522
523        if (!method_exists($container, 'registerForAutoconfiguration')) {
524            $this->markTestSkipped(
525                'registerForAutoconfiguration method is not available in the container'
526            );
527        }
528
529        $autoconfigureInstance = $container->getAutoconfiguredInstanceof();
530
531        $this->assertTrue(array_key_exists(EventSubscriberInterface::class, $autoconfigureInstance));
532        $this->assertTrue($autoconfigureInstance[EventSubscriberInterface::class]->hasTag('jms_serializer.event_subscriber'));
533    }
534
535    public function testAutoconfigureHandlers()
536    {
537        $container = $this->getContainerForConfig(array());
538
539        if (!method_exists($container, 'registerForAutoconfiguration')) {
540            $this->markTestSkipped(
541                'registerForAutoconfiguration method is not available in the container'
542            );
543        }
544
545        $autoconfigureInstance = $container->getAutoconfiguredInstanceof();
546
547        $this->assertTrue(array_key_exists(SubscribingHandlerInterface::class, $autoconfigureInstance));
548        $this->assertTrue($autoconfigureInstance[SubscribingHandlerInterface::class]->hasTag('jms_serializer.subscribing_handler'));
549    }
550
551    private function getContainerForConfig(array $configs, callable $configurator = null)
552    {
553        $bundle = new JMSSerializerBundle();
554        $extension = $bundle->getContainerExtension();
555
556        $container = new ContainerBuilder();
557        $container->setParameter('kernel.debug', true);
558        $container->setParameter('kernel.cache_dir', sys_get_temp_dir() . '/serializer');
559        $container->setParameter('kernel.bundles', array());
560        $container->set('annotation_reader', new AnnotationReader());
561        $container->setDefinition('doctrine', new Definition(Registry::class));
562        $container->set('translator', $this->getMockBuilder('Symfony\\Component\\Translation\\TranslatorInterface')->getMock());
563        $container->set('debug.stopwatch', $this->getMockBuilder('Symfony\\Component\\Stopwatch\\Stopwatch')->getMock());
564        $container->registerExtension($extension);
565        $extension->load($configs, $container);
566
567        $bundle->build($container);
568
569        if ($configurator) {
570            call_user_func($configurator, $container);
571        }
572
573        $container->compile();
574
575        return $container;
576    }
577}
578