Przeglądaj źródła

adds a builder object to make serializer construction a breeze :)

Johannes M. Schmitt 12 lat temu
rodzic
commit
e0d99824b7

+ 345 - 0
src/JMS/Serializer/SerializerBuilder.php

@@ -0,0 +1,345 @@
+<?php
+
+namespace JMS\Serializer;
+
+use Metadata\MetadataFactory;
+use JMS\Serializer\Metadata\Driver\AnnotationDriver;
+use JMS\Serializer\Handler\HandlerRegistry;
+use JMS\Serializer\Construction\UnserializeObjectConstructor;
+use PhpCollection\Map;
+use JMS\Serializer\EventDispatcher\EventDispatcher;
+use Metadata\Driver\DriverChain;
+use JMS\Serializer\Metadata\Driver\YamlDriver;
+use JMS\Serializer\Metadata\Driver\XmlDriver;
+use Metadata\Driver\FileLocator;
+use JMS\Serializer\Handler\DateTimeHandler;
+use JMS\Serializer\Handler\ArrayCollectionHandler;
+use JMS\Serializer\Construction\ObjectConstructorInterface;
+use JMS\Serializer\EventDispatcher\Subscriber\DoctrineProxySubscriber;
+use JMS\Serializer\Naming\CamelCaseNamingStrategy;
+use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
+use Doctrine\Common\Annotations\AnnotationReader;
+use Doctrine\Common\Annotations\FileCacheReader;
+use Metadata\Cache\FileCache;
+
+/**
+ * Builder for serializer instances.
+ *
+ * This makes it easier for you to wire all the different classes together.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class SerializerBuilder
+{
+    private $metadataDirs = array();
+    private $handlerRegistry;
+    private $handlersConfigured = false;
+    private $eventDispatcher;
+    private $listenersConfigured = false;
+    private $objectConstructor;
+    private $serializationVisitors;
+    private $deserializationVisitors;
+    private $visitorsAdded = false;
+    private $propertyNamingStrategy;
+    private $debug = false;
+    private $cacheDir;
+    private $annotationReader;
+
+    public static function create()
+    {
+        return new static();
+    }
+
+    public function __construct()
+    {
+        $this->handlerRegistry = new HandlerRegistry();
+        $this->eventDispatcher = new EventDispatcher();
+        $this->serializationVisitors = new Map();
+        $this->deserializationVisitors = new Map();
+    }
+
+    public function setAnnotationReader(AnnotationReader $reader)
+    {
+        $this->annotationReader = $reader;
+
+        return $this;
+    }
+
+    public function setDebug($bool)
+    {
+        $this->debug = (boolean) $bool;
+
+        return $this;
+    }
+
+    public function setCacheDir($dir)
+    {
+        if ( ! is_dir($dir)) {
+            $this->createDir($dir);
+        }
+        if ( ! is_writable($dir)) {
+            throw new \InvalidArgumentException(sprintf('The cache directory "%s" is not writable.', $dir));
+        }
+
+        $this->cacheDir = $dir;
+
+        return $this;
+    }
+
+    public function addDefaultHandlers()
+    {
+        $this->handlersConfigured = true;
+        $this->handlerRegistry->registerSubscribingHandler(new DateTimeHandler());
+        $this->handlerRegistry->registerSubscribingHandler(new ArrayCollectionHandler());
+
+        return $this;
+    }
+
+    public function configureHandlers(\Closure $closure)
+    {
+        $this->handlersConfigured = true;
+        $closure($this->handlerRegistry);
+
+        return $this;
+    }
+
+    public function addDefaultListeners()
+    {
+        $this->listenersConfigured = true;
+        $this->eventDispatcher->addSubscriber(new DoctrineProxySubscriber());
+
+        return $this;
+    }
+
+    public function configureListeners(\Closure $closure)
+    {
+        $this->listenersConfigured = true;
+        $closure($this->eventDispatcher);
+
+        return $this;
+    }
+
+    public function setObjectConstructor(ObjectConstructorInterface $constructor)
+    {
+        $this->objectConstructor = $constructor;
+
+        return $this;
+    }
+
+    public function setPropertyNamingStrategy(PropertyNamingStrategyInterface $propertyNamingStrategy)
+    {
+        $this->propertyNamingStrategy = $propertyNamingStrategy;
+
+        return $this;
+    }
+
+    public function setSerializationVisitor($format, VisitorInterface $visitor)
+    {
+        $this->visitorsAdded = true;
+        $this->serializationVisitors->set($format, $visitor);
+
+        return $this;
+    }
+
+    public function setDeserializationVisitor($format, VisitorInterface $visitor)
+    {
+        $this->visitorsAdded = true;
+        $this->deserializationVisitors->set($format, $visitor);
+
+        return $this;
+    }
+
+    public function addDefaultSerializationVisitors()
+    {
+        $this->initializePropertyNamingStrategy();
+
+        $this->visitorsAdded = true;
+        $this->serializationVisitors->setAll(array(
+            'xml' => new XmlSerializationVisitor($this->propertyNamingStrategy),
+            'yml' => new YamlSerializationVisitor($this->propertyNamingStrategy),
+            'json' => new JsonSerializationVisitor($this->propertyNamingStrategy),
+        ));
+
+        return $this;
+    }
+
+    public function addDefaultDeserializationVisitors()
+    {
+        $this->initializePropertyNamingStrategy();
+
+        $this->visitorsAdded = true;
+        $this->deserializationVisitors->setAll(array(
+            'xml' => new XmlDeserializationVisitor($this->propertyNamingStrategy),
+            'json' => new JsonDeserializationVisitor($this->propertyNamingStrategy),
+        ));
+
+        return $this;
+    }
+
+    /**
+     * Sets a map of namespace prefixes to directories.
+     *
+     * This method overrides any previously defined directories.
+     *
+     * @param array<string,string> $namespacePrefixToDirMap
+     *
+     * @return SerializerBuilder
+     */
+    public function setMetadataDirs(array $namespacePrefixToDirMap)
+    {
+        foreach ($namespacePrefixToDirMap as $prefix => $dir) {
+            if ( ! is_dir($dir)) {
+                throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
+            }
+        }
+
+        $this->metadataDirs = $namespacePrefixToDirMap;
+
+        return $this;
+    }
+
+    /**
+     * Adds a directory where the serializer will look for class metadata.
+     *
+     * The namespace prefix will make the names of the actual metadata files a bit shorter. For example, let's assume
+     * that you have a directory where you only store metadata files for the ``MyApplication\Entity`` namespace.
+     *
+     * If you use an empty prefix, your metadata files would need to look like:
+     *
+     * ``my-dir/MyApplication.Entity.SomeObject.yml``
+     * ``my-dir/MyApplication.Entity.OtherObject.xml``
+     *
+     * If you use ``MyApplication\Entity`` as prefix, your metadata files would need to look like:
+     *
+     * ``my-dir/SomeObject.yml``
+     * ``my-dir/OtherObject.yml``
+     *
+     * Please keep in mind that you currently may only have one directory per namespace prefix.
+     *
+     * @param string $dir The directory where metadata files are located.
+     * @param string $namespacePrefix An optional prefix if you only store metadata for specific namespaces in this directory.
+     *
+     * @return SerializerBuilder
+     */
+    public function addMetadataDir($dir, $namespacePrefix = '')
+    {
+        if ( ! is_dir($dir)) {
+            throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
+        }
+
+        if (isset($this->metadataDirs[$namespacePrefix])) {
+            throw new \InvalidArgumentException(sprintf('There is already a directory configured for the namespace prefix "%s". Please use replaceMetadataDir() to override directories.', $namespacePrefix));
+        }
+
+        $this->metadataDirs[$namespacePrefix] = $dir;
+
+        return $this;
+    }
+
+    /**
+     * Adds a map of namespace prefixes to directories.
+     *
+     * @param array<string,string> $namespacePrefixToDirMap
+     *
+     * @return SerializerBuilder
+     */
+    public function addMetadataDirs(array $namespacePrefixToDirMap)
+    {
+        foreach ($namespacePrefixToDirMap as $prefix => $dir) {
+            $this->addMetadataDir($dir, $prefix);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Similar to addMetadataDir(), but overrides an existing entry.
+     *
+     * @param string $dir
+     * @param string $namespacePrefix
+     *
+     * @return SerializerBuilder
+     */
+    public function replaceMetadataDir($dir, $namespacePrefix = '')
+    {
+        if ( ! is_dir($dir)) {
+            throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $dir));
+        }
+
+        if ( ! isset($this->metadataDirs[$namespacePrefix])) {
+            throw new \InvalidArgumentException(sprintf('There is no directory configured for namespace prefix "%s". Please use addMetadataDir() for adding new directories.', $namespacePrefix));
+        }
+
+        $this->metadataDirs[$namespacePrefix] = $dir;
+
+        return $this;
+    }
+
+    public function build()
+    {
+        $fileLocator = new FileLocator($this->metadataDirs);
+
+        $annotationReader = $this->annotationReader;
+        if (null === $annotationReader) {
+            $annotationReader = new AnnotationReader();
+
+            if (null !== $this->cacheDir) {
+                $this->createDir($this->cacheDir.'/annotations');
+                $annotationReader = new FileCacheReader($annotationReader, $this->cacheDir.'/annotations', $this->debug);
+            }
+        }
+
+        $metadataFactory = new MetadataFactory(new DriverChain(array(
+            new YamlDriver($fileLocator),
+            new XmlDriver($fileLocator),
+            new AnnotationDriver($annotationReader),
+        )), null, $this->debug);
+
+        if (null !== $this->cacheDir) {
+            $this->createDir($this->cacheDir.'/metadata');
+            $metadataFactory->setCache(new FileCache($this->cacheDir.'/metadata'));
+        }
+
+        if ( ! $this->handlersConfigured) {
+            $this->addDefaultHandlers();
+        }
+
+        if ( ! $this->listenersConfigured) {
+            $this->addDefaultListeners();
+        }
+
+        if ( ! $this->visitorsAdded) {
+            $this->addDefaultSerializationVisitors();
+            $this->addDefaultDeserializationVisitors();
+        }
+
+        return new Serializer(
+            $metadataFactory,
+            $this->handlerRegistry,
+            $this->objectConstructor ?: new UnserializeObjectConstructor(),
+            $this->serializationVisitors,
+            $this->deserializationVisitors,
+            $this->eventDispatcher
+        );
+    }
+
+    private function initializePropertyNamingStrategy()
+    {
+        if (null !== $this->propertyNamingStrategy) {
+            return;
+        }
+
+        $this->propertyNamingStrategy = new CamelCaseNamingStrategy();
+    }
+
+    private function createDir($dir)
+    {
+        if (is_dir($dir)) {
+            return;
+        }
+
+        if (false === @mkdir($dir, 0777, true)) {
+            throw new \RuntimeException(sprintf('Could not create directory "%s".', $dir));
+        }
+    }
+}

+ 99 - 0
tests/JMS/Serializer/Tests/SerializerBuilderTest.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace JMS\Serializer\Tests;
+
+use JMS\Serializer\SerializerBuilder;
+use Symfony\Component\Filesystem\Filesystem;
+use JMS\Serializer\Handler\HandlerRegistry;
+use JMS\Serializer\JsonSerializationVisitor;
+use JMS\Serializer\Naming\CamelCaseNamingStrategy;
+
+class SerializerBuilderTest extends \PHPUnit_Framework_TestCase
+{
+    /** @var SerializerBuilder */
+    private $builder;
+    private $fs;
+    private $tmpDir;
+
+    public function testBuildWithoutAnythingElse()
+    {
+        $serializer = $this->builder->build();
+
+        $this->assertEquals('"foo"', $serializer->serialize('foo', 'json'));
+        $this->assertEquals('<?xml version="1.0" encoding="UTF-8"?>
+<result><![CDATA[foo]]></result>
+', $serializer->serialize('foo', 'xml'));
+        $this->assertEquals('foo
+', $serializer->serialize('foo', 'yml'));
+
+        $this->assertEquals('foo', $serializer->deserialize('"foo"', 'string', 'json'));
+        $this->assertEquals('foo', $serializer->deserialize('<?xml version="1.0" encoding="UTF-8"?><result><![CDATA[foo]]></result>', 'string', 'xml'));
+    }
+
+    public function testWithCache()
+    {
+        $this->assertFileNotExists($this->tmpDir);
+
+        $this->assertSame($this->builder, $this->builder->setCacheDir($this->tmpDir));
+        $serializer = $this->builder->build();
+
+        $this->assertFileExists($this->tmpDir);
+        $this->assertFileExists($this->tmpDir.'/annotations');
+        $this->assertFileExists($this->tmpDir.'/metadata');
+
+        $factory = $this->getField($serializer, 'factory');
+        $this->assertAttributeSame(false, 'debug', $factory);
+        $this->assertAttributeNotSame(null, 'cache', $factory);
+    }
+
+    public function testDoesAddDefaultHandlers()
+    {
+        $serializer = $this->builder->build();
+
+        $this->assertEquals('"2020-04-16T00:00:00+0000"', $serializer->serialize(new \DateTime('2020-04-16'), 'json'));
+    }
+
+    public function testDoesNotAddDefaultHandlersWhenExplicitlyConfigured()
+    {
+        $this->assertSame($this->builder, $this->builder->configureHandlers(function(HandlerRegistry $registry) {
+        }));
+
+        $this->assertEquals('[]', $this->builder->build()->serialize(new \DateTime('2020-04-16'), 'json'));
+    }
+
+    /**
+     * @expectedException RuntimeException
+     */
+    public function testDoesNotAddOtherVisitorsWhenConfiguredExplicitly()
+    {
+        $this->assertSame(
+            $this->builder,
+            $this->builder->setSerializationVisitor('json', new JsonSerializationVisitor(new CamelCaseNamingStrategy()))
+        );
+
+        $this->builder->build()->serialize('foo', 'xml');
+    }
+
+    private function getField($obj, $name)
+    {
+        $ref = new \ReflectionProperty($obj, $name);
+        $ref->setAccessible(true);
+
+        return $ref->getValue($obj);
+    }
+
+    protected function setUp()
+    {
+        $this->builder = SerializerBuilder::create();
+        $this->fs = new Filesystem();
+
+        $this->tmpDir = sys_get_temp_dir().'/serializer';
+        $this->fs->remove($this->tmpDir);
+        clearstatcache();
+    }
+
+    protected function tearDown()
+    {
+        $this->fs->remove($this->tmpDir);
+    }
+}