2020-08-07 23:42:38 +01:00

501 lines
16 KiB
PHP

<?php
/*
* Copyright 2016 Johannes M. Schmitt <schmittjoh@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace JMS\Serializer;
use Doctrine\Common\Annotations\CachedReader;
use Doctrine\Common\Cache\FilesystemCache;
use JMS\Serializer\Accessor\AccessorStrategyInterface;
use JMS\Serializer\Accessor\DefaultAccessorStrategy;
use JMS\Serializer\Accessor\ExpressionAccessorStrategy;
use JMS\Serializer\Builder\DefaultDriverFactory;
use JMS\Serializer\Builder\DriverFactoryInterface;
use JMS\Serializer\Handler\PhpCollectionHandler;
use JMS\Serializer\Handler\PropelCollectionHandler;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Handler\StdClassHandler;
use Metadata\Driver\DriverInterface;
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\DateHandler;
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 JMS\Serializer\ContextFactory\SerializationContextFactoryInterface;
use JMS\Serializer\ContextFactory\DeserializationContextFactoryInterface;
use JMS\Serializer\ContextFactory\CallableSerializationContextFactory;
use JMS\Serializer\ContextFactory\CallableDeserializationContextFactory;
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Annotations\AnnotationReader;
use Metadata\Cache\FileCache;
use JMS\Serializer\Naming\SerializedNameAnnotationStrategy;
use JMS\Serializer\Exception\InvalidArgumentException;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
/**
* Builder for serializer instances.
*
* This object makes serializer construction a breeze for projects that do not use
* any special dependency injection container.
*
* @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;
private $includeInterfaceMetadata = false;
private $driverFactory;
private $serializationContextFactory;
private $deserializationContextFactory;
/**
* @var ExpressionEvaluatorInterface
*/
private $expressionEvaluator;
/**
* @var AccessorStrategyInterface
*/
private $accessorStrategy;
public static function create()
{
return new static();
}
public function __construct()
{
$this->handlerRegistry = new HandlerRegistry();
$this->eventDispatcher = new EventDispatcher();
$this->driverFactory = new DefaultDriverFactory();
$this->serializationVisitors = new Map();
$this->deserializationVisitors = new Map();
}
public function setAccessorStrategy(AccessorStrategyInterface $accessorStrategy)
{
$this->accessorStrategy = $accessorStrategy;
}
protected function getAccessorStrategy()
{
if (!$this->accessorStrategy) {
$this->accessorStrategy = new DefaultAccessorStrategy();
if ($this->expressionEvaluator) {
$this->accessorStrategy = new ExpressionAccessorStrategy($this->expressionEvaluator, $this->accessorStrategy);
}
}
return $this->accessorStrategy;
}
public function setExpressionEvaluator(ExpressionEvaluatorInterface $expressionEvaluator)
{
$this->expressionEvaluator = $expressionEvaluator;
return $this;
}
public function setAnnotationReader(Reader $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 DateHandler());
$this->handlerRegistry->registerSubscribingHandler(new StdClassHandler());
$this->handlerRegistry->registerSubscribingHandler(new PhpCollectionHandler());
$this->handlerRegistry->registerSubscribingHandler(new ArrayCollectionHandler());
$this->handlerRegistry->registerSubscribingHandler(new PropelCollectionHandler());
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, $this->getAccessorStrategy()),
'yml' => new YamlSerializationVisitor($this->propertyNamingStrategy, $this->getAccessorStrategy()),
'json' => new JsonSerializationVisitor($this->propertyNamingStrategy, $this->getAccessorStrategy()),
));
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;
}
/**
* @param Boolean $include Whether to include the metadata from the interfaces
*
* @return SerializerBuilder
*/
public function includeInterfaceMetadata($include)
{
$this->includeInterfaceMetadata = (Boolean) $include;
return $this;
}
/**
* Sets a map of namespace prefixes to directories.
*
* This method overrides any previously defined directories.
*
* @param array<string,string> $namespacePrefixToDirMap
*
* @return SerializerBuilder
*
* @throws InvalidArgumentException When a directory does not exist
*/
public function setMetadataDirs(array $namespacePrefixToDirMap)
{
foreach ($namespacePrefixToDirMap as $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
*
* @throws InvalidArgumentException When a directory does not exist
* @throws InvalidArgumentException When a directory has already been registered
*/
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
*
* @throws InvalidArgumentException When a directory does not exist
* @throws InvalidArgumentException When no directory is configured for the ns prefix
*/
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 setMetadataDriverFactory(DriverFactoryInterface $driverFactory)
{
$this->driverFactory = $driverFactory;
return $this;
}
/**
* @param SerializationContextFactoryInterface|callable $serializationContextFactory
*
* @return self
*/
public function setSerializationContextFactory($serializationContextFactory)
{
if ($serializationContextFactory instanceof SerializationContextFactoryInterface) {
$this->serializationContextFactory = $serializationContextFactory;
} elseif (is_callable($serializationContextFactory)) {
$this->serializationContextFactory = new CallableSerializationContextFactory(
$serializationContextFactory
);
} else {
throw new InvalidArgumentException('expected SerializationContextFactoryInterface or callable.');
}
return $this;
}
/**
* @param DeserializationContextFactoryInterface|callable $deserializationContextFactory
*
* @return self
*/
public function setDeserializationContextFactory($deserializationContextFactory)
{
if ($deserializationContextFactory instanceof DeserializationContextFactoryInterface) {
$this->deserializationContextFactory = $deserializationContextFactory;
} elseif (is_callable($deserializationContextFactory)) {
$this->deserializationContextFactory = new CallableDeserializationContextFactory(
$deserializationContextFactory
);
} else {
throw new InvalidArgumentException('expected DeserializationContextFactoryInterface or callable.');
}
return $this;
}
public function build()
{
$annotationReader = $this->annotationReader;
if (null === $annotationReader) {
$annotationReader = new AnnotationReader();
if (null !== $this->cacheDir) {
$this->createDir($this->cacheDir.'/annotations');
$annotationsCache = new FilesystemCache($this->cacheDir.'/annotations');
$annotationReader = new CachedReader($annotationReader, $annotationsCache, $this->debug);
}
}
$metadataDriver = $this->driverFactory->createDriver($this->metadataDirs, $annotationReader);
$metadataFactory = new MetadataFactory($metadataDriver, null, $this->debug);
$metadataFactory->setIncludeInterfaces($this->includeInterfaceMetadata);
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();
}
$serializer = new Serializer(
$metadataFactory,
$this->handlerRegistry,
$this->objectConstructor ?: new UnserializeObjectConstructor(),
$this->serializationVisitors,
$this->deserializationVisitors,
$this->eventDispatcher,
null,
$this->expressionEvaluator
);
if (null !== $this->serializationContextFactory) {
$serializer->setSerializationContextFactory($this->serializationContextFactory);
}
if (null !== $this->deserializationContextFactory) {
$serializer->setDeserializationContextFactory($this->deserializationContextFactory);
}
return $serializer;
}
private function initializePropertyNamingStrategy()
{
if (null !== $this->propertyNamingStrategy) {
return;
}
$this->propertyNamingStrategy = new SerializedNameAnnotationStrategy(new CamelCaseNamingStrategy());
}
private function createDir($dir)
{
if (is_dir($dir)) {
return;
}
if (false === @mkdir($dir, 0777, true) && false === is_dir($dir)) {
throw new RuntimeException(sprintf('Could not create directory "%s".', $dir));
}
}
}