├── .gitignore ├── .scrutinizer.yml ├── .styleci.yml ├── .travis.yml ├── Annotation ├── Accessor.php ├── Exclude.php ├── ExclusionPolicy.php ├── Expose.php ├── Groups.php ├── ReadOnly.php ├── SerializedName.php ├── SerializerAnnotation.php └── Type.php ├── DependencyInjection ├── CompilerPass │ └── MetadataPass.php ├── Configuration.php └── HappyrSerializerExtension.php ├── Exception ├── Exception.php └── RuntimeException.php ├── HappyrSerializerBundle.php ├── Metadata ├── AnnotationReader.php ├── Metadata.php ├── MetadataProvider.php └── MetadataReader.php ├── Normalizer ├── GroupValidationTrait.php ├── MetadataAwareDenormalizer.php └── MetadataAwareNormalizer.php ├── PropertyManager ├── AttributeExtractor.php ├── PropertyNameConverter.php └── ReflectionPropertyAccess.php ├── Readme.md ├── Resources └── config │ ├── services.yml │ └── twig.yml ├── Tests ├── App │ ├── AppKernel.php │ ├── autoload.php │ └── config │ │ └── config.yml ├── Fixtures │ ├── Accessor │ │ └── Car.php │ ├── Composition │ │ ├── Car.php │ │ └── Owner.php │ ├── Exclude │ │ └── Car.php │ ├── ExclusionPolicy │ │ ├── ExcludeAll.php │ │ ├── ExcludeDefault.php │ │ └── ExcludeNone.php │ ├── Expose │ │ └── Car.php │ ├── Groups │ │ └── Car.php │ ├── ReadOnly │ │ ├── Car.php │ │ └── ClassReadOnly.php │ └── SerializedName │ │ └── Car.php ├── Functional │ ├── AccessorTest.php │ ├── CompositionTest.php │ ├── ExcludeTest.php │ ├── ExclusionPolicyTest.php │ ├── ExposeTest.php │ ├── GroupsTest.php │ ├── ReadOnlyTest.php │ ├── SerializedNameTest.php │ └── SerializerTestCase.php └── Unit │ └── Metadata │ └── MetadataProviderTest.php ├── Twig └── SerializerExtension.php ├── composer.json └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | .puli/ 2 | build/ 3 | vendor/ 4 | composer.lock 5 | phpspec.yml 6 | phpunit.xml 7 | puli.json 8 | puli.phar 9 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [vendor/*, Tests/*] 3 | checks: 4 | php: 5 | code_rating: true 6 | duplication: true 7 | tools: 8 | external_code_coverage: 9 | timeout: 900 10 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: symfony 2 | 3 | finder: 4 | exclude: 5 | - "Resources" 6 | - "vendor" 7 | 8 | enabled: 9 | - short_array_syntax 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | cache: 6 | directories: 7 | - $HOME/.composer/cache/files 8 | 9 | php: 10 | - 5.5 11 | - 5.6 12 | - 7.0 13 | - hhvm 14 | 15 | env: 16 | global: 17 | - TEST_COMMAND="composer test" 18 | matrix: 19 | - SYMFONY_VERSION=3.0.* 20 | - SYMFONY_VERSION=2.8.* 21 | - SYMFONY_VERSION=2.7.* 22 | 23 | branches: 24 | except: 25 | - /^analysis-.*$/ 26 | 27 | matrix: 28 | fast_finish: true 29 | include: 30 | - php: 5.5 31 | env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" SYMFONY_VERSION=2.7.* 32 | 33 | before_install: 34 | - travis_retry composer self-update 35 | 36 | install: 37 | - composer require symfony/symfony:${SYMFONY_VERSION} --no-update 38 | - travis_retry composer update ${COMPOSER_FLAGS} --prefer-source --no-interaction 39 | 40 | script: 41 | - $TEST_COMMAND 42 | 43 | after_success: 44 | - if [[ "$COVERAGE" = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi 45 | - if [[ "$COVERAGE" = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi 46 | -------------------------------------------------------------------------------- /Annotation/Accessor.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class Accessor implements SerializerAnnotation 12 | { 13 | /** 14 | * @var string 15 | */ 16 | public $getter; 17 | /** 18 | * @var string 19 | */ 20 | public $setter; 21 | 22 | /** 23 | * @param array $values 24 | */ 25 | public function __construct(array $values) 26 | { 27 | $values = $values['value']; 28 | if (!empty($values['getter'])) { 29 | $this->getter = $values['getter']; 30 | } 31 | if (!empty($values['setter'])) { 32 | $this->setter = $values['setter']; 33 | } 34 | } 35 | 36 | public function getName() 37 | { 38 | return 'accessor'; 39 | } 40 | 41 | public function getValue() 42 | { 43 | return ['setter' => $this->setter, 'getter' => $this->getter]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Annotation/Exclude.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class Exclude implements SerializerAnnotation 12 | { 13 | public function getName() 14 | { 15 | return 'exclude'; 16 | } 17 | 18 | public function getValue() 19 | { 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Annotation/ExclusionPolicy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ExclusionPolicy implements SerializerAnnotation 14 | { 15 | const NONE = 'NONE'; 16 | const ALL = 'ALL'; 17 | 18 | public $value; 19 | 20 | public function __construct(array $values) 21 | { 22 | if (!is_string($values['value'])) { 23 | throw new RuntimeException('"value" must be a string.'); 24 | } 25 | 26 | $this->value = strtoupper($values['value']); 27 | 28 | if (self::NONE !== $this->value && self::ALL !== $this->value) { 29 | throw new RuntimeException('Exclusion policy must either be "ALL", or "NONE".'); 30 | } 31 | } 32 | 33 | public function getName() 34 | { 35 | return 'exclusion_policy'; 36 | } 37 | 38 | public function getValue() 39 | { 40 | return $this->value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Annotation/Expose.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class Expose implements SerializerAnnotation 12 | { 13 | public function getName() 14 | { 15 | return 'expose'; 16 | } 17 | 18 | public function getValue() 19 | { 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Annotation/Groups.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class Groups implements SerializerAnnotation 12 | { 13 | /** 14 | * @var array @Required 15 | */ 16 | public $groups; 17 | 18 | public function getName() 19 | { 20 | return 'groups'; 21 | } 22 | 23 | public function getValue() 24 | { 25 | return $this->groups; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Annotation/ReadOnly.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class ReadOnly implements SerializerAnnotation 12 | { 13 | /** 14 | * @var bool 15 | */ 16 | public $value = true; 17 | 18 | public function getName() 19 | { 20 | return 'read_only'; 21 | } 22 | 23 | public function getValue() 24 | { 25 | return $this->value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Annotation/SerializedName.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class SerializedName implements SerializerAnnotation 14 | { 15 | private $value; 16 | 17 | /** 18 | * @param $name 19 | */ 20 | public function __construct(array $values) 21 | { 22 | if (!is_string($values['value'])) { 23 | throw new RuntimeException('"value" must be a string.'); 24 | } 25 | 26 | $this->value = $values['value']; 27 | } 28 | 29 | public function getName() 30 | { 31 | return 'serialized_name'; 32 | } 33 | 34 | public function getValue() 35 | { 36 | return $this->value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Annotation/SerializerAnnotation.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface SerializerAnnotation 9 | { 10 | /** 11 | * The name or identifier for this metadata. 12 | * 13 | * @return string 14 | */ 15 | public function getName(); 16 | 17 | /** 18 | * The value of this annotation. 19 | * 20 | * @return mixed 21 | */ 22 | public function getValue(); 23 | } 24 | -------------------------------------------------------------------------------- /Annotation/Type.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class Type implements SerializerAnnotation 12 | { 13 | public $value; 14 | 15 | public function getName() 16 | { 17 | return 'type'; 18 | } 19 | 20 | public function getValue() 21 | { 22 | return $this->value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DependencyInjection/CompilerPass/MetadataPass.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class MetadataPass implements CompilerPassInterface 15 | { 16 | public function process(ContainerBuilder $container) 17 | { 18 | $provider = $container->get('happyr.serializer.metadata.metadata_provider'); 19 | $metadata = $provider->getMetadata(); 20 | 21 | $normalizerDef = $container->getDefinition('happyr.serializer.normalizer.metadata_aware'); 22 | $metadataString = Metadata::convertToStrings($metadata); 23 | $normalizerDef->replaceArgument(0, $metadataString); 24 | 25 | $normalizerDef = $container->getDefinition('happyr.serializer.denormalizer.metadata_aware'); 26 | $normalizerDef->replaceArgument(0, $metadataString); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Configuration implements ConfigurationInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function getConfigTreeBuilder() 19 | { 20 | $treeBuilder = new TreeBuilder(); 21 | $root = $treeBuilder->root('happyr_serializer'); 22 | 23 | $root->children() 24 | ->booleanNode('twig_extension')->defaultFalse()->end() 25 | ->arrayNode('source') 26 | ->prototype('scalar') 27 | ->validate() 28 | ->always(function ($v) { 29 | $v = str_replace(DIRECTORY_SEPARATOR, '/', $v); 30 | 31 | if (!is_dir($v)) { 32 | throw new \Exception(sprintf('The directory "%s" does not exist.', $v)); 33 | } 34 | 35 | return $v; 36 | }) 37 | ->end() 38 | ->end(); 39 | 40 | return $treeBuilder; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /DependencyInjection/HappyrSerializerExtension.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class HappyrSerializerExtension extends Extension 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function load(array $configs, ContainerBuilder $container) 19 | { 20 | $configuration = new Configuration(); 21 | $config = $this->processConfiguration($configuration, $configs); 22 | 23 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 24 | $loader->load('services.yml'); 25 | 26 | if ($config['twig_extension']) { 27 | $loader->load('twig.yml'); 28 | } 29 | 30 | if (empty($config['source'])) { 31 | // Try to help 32 | $path = $container->getParameter('kernel.root_dir').'/../src'; 33 | if (!is_dir($path)) { 34 | throw new \Exception('You must specify a path at happyr_serializer.source'); 35 | } 36 | $config['source'] = [$path]; 37 | } 38 | $container->getDefinition('happyr.serializer.metadata.annotation_reader') 39 | ->replaceArgument(0, $config['source']); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Exception/Exception.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface Exception 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class RuntimeException extends \RuntimeException implements Exception 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /HappyrSerializerBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new MetadataPass()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Metadata/AnnotationReader.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class AnnotationReader implements MetadataReader 17 | { 18 | /** 19 | * @var array 20 | */ 21 | private $paths; 22 | 23 | /** 24 | * @var Reader 25 | */ 26 | private $reader; 27 | 28 | /** 29 | * @var AttributeExtractor 30 | */ 31 | private $attributeExtractor; 32 | 33 | /** 34 | * @param array $paths 35 | * @param Reader $reader 36 | * @param AttributeExtractor $attributeExtractor 37 | */ 38 | public function __construct(array $paths, Reader $reader, AttributeExtractor $attributeExtractor) 39 | { 40 | $this->paths = $paths; 41 | $this->reader = $reader; 42 | $this->attributeExtractor = $attributeExtractor; 43 | } 44 | 45 | /** 46 | * @return Metadata[] 47 | */ 48 | public function getMetadata() 49 | { 50 | // Create a function to filter out our annotations 51 | $filterAnnotations = function ($annotation) { 52 | return $annotation instanceof SerializerAnnotation; 53 | }; 54 | 55 | $metadatas = []; 56 | foreach ($this->paths as $path) { 57 | $finder = new Finder(); 58 | $finder->in($path)->notName('*Test.php')->name('*.php'); 59 | /** @var SplFileInfo $file */ 60 | foreach ($finder as $file) { 61 | if (null === $fqcn = $this->getFullyQualifiedClassName($file->getPathname())) { 62 | continue; 63 | } 64 | $metadata = new Metadata($fqcn); 65 | 66 | $attributes = $this->attributeExtractor->getAttributes($fqcn); 67 | $reflectionClass = new \ReflectionClass($fqcn); 68 | $classAnnotations = array_filter($this->reader->getClassAnnotations($reflectionClass), $filterAnnotations); 69 | 70 | $propertyAnnotations = []; 71 | foreach ($attributes['property'] as $propertyName => $bool) { 72 | $propertyAnnotations[$propertyName] = array_filter($this->reader->getPropertyAnnotations($reflectionClass->getProperty($propertyName)), $filterAnnotations); 73 | } 74 | 75 | $methodAnnotations = []; 76 | foreach ($attributes['method'] as $methodName => $bool) { 77 | $methodAnnotations[$methodName] = array_filter($this->reader->getMethodAnnotations($reflectionClass->getMethod($methodName)), $filterAnnotations); 78 | } 79 | 80 | if ($this->buildMetadataFromAnnotation($metadata, $classAnnotations, $methodAnnotations, $propertyAnnotations)) { 81 | $metadatas[] = $metadata; 82 | } 83 | } 84 | } 85 | 86 | return $metadatas; 87 | } 88 | 89 | /** 90 | * Build a metadata object from annotations. 91 | * 92 | * @param Metadata $metadata 93 | * @param $classAnnotations 94 | * @param $methodAnnotations 95 | * @param $propertyAnnotations 96 | */ 97 | private function buildMetadataFromAnnotation(Metadata $metadata, $classAnnotations, $methodAnnotations, $propertyAnnotations) 98 | { 99 | $return = false; 100 | $a = [ 101 | 'setPropertyMetadata' => $propertyAnnotations, 102 | 'setMethodMetadata' => $methodAnnotations, 103 | ]; 104 | 105 | foreach ($a as $function => $typeAnnotations) { 106 | foreach ($typeAnnotations as $name => $annotations) { 107 | $data = []; 108 | /** @var SerializerAnnotation $annotation */ 109 | foreach ($annotations as $annotation) { 110 | $data[$annotation->getName()] = $annotation->getValue(); 111 | } 112 | 113 | $metadata->$function($name, $data); 114 | if (count($data) > 0) { 115 | $return = true; 116 | } 117 | } 118 | } 119 | 120 | // Class annotations 121 | /* @var SerializerAnnotation $annotation */ 122 | $data = []; 123 | foreach ($classAnnotations as $annotation) { 124 | $data[$annotation->getName()] = $annotation->getValue(); 125 | } 126 | $metadata->setClassMetadata($data); 127 | if (count($data) > 0) { 128 | $return = true; 129 | } 130 | 131 | return $return; 132 | } 133 | 134 | /** 135 | * @param string $path 136 | * 137 | * @return null|string 138 | */ 139 | private function getFullyQualifiedClassName($path) 140 | { 141 | $src = file_get_contents($path); 142 | $filename = basename($path); 143 | $classname = substr($filename, 0, -4); 144 | 145 | if (preg_match('|^namespace\s+(.+?);$|sm', $src, $matches)) { 146 | return $matches[1].'\\'.$classname; 147 | } 148 | 149 | return; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Metadata/Metadata.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Metadata 11 | { 12 | /** 13 | * @var string fqn of the class 14 | */ 15 | private $class; 16 | 17 | /** 18 | * @var array 19 | */ 20 | private $classMetadata = []; 21 | 22 | /** 23 | * @var array 24 | */ 25 | private $methodMetadata = []; 26 | 27 | /** 28 | * @var array 29 | */ 30 | private $propertyMetadata = []; 31 | 32 | /** 33 | * @param string $class 34 | */ 35 | public function __construct($class) 36 | { 37 | $this->class = $class; 38 | } 39 | 40 | /** 41 | * @param array $metadata 42 | */ 43 | public static function convertToStrings(array $metadata) 44 | { 45 | $data = []; 46 | 47 | /** @var Metadata $m */ 48 | foreach ($metadata as $m) { 49 | $data[$m->getClass()] = ['fqcn' => $m->getClass(), 'class' => $m->getClassMetadata(), 'property' => $m->getPropertyMetadata(), 'method' => $m->getMethodMetadata()]; 50 | } 51 | 52 | return $data; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getClass() 59 | { 60 | return $this->class; 61 | } 62 | 63 | /** 64 | * @param string $class 65 | * 66 | * @return Metadata 67 | */ 68 | public function setClass($class) 69 | { 70 | $this->class = $class; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * @return array 77 | */ 78 | public function getClassMetadata() 79 | { 80 | return $this->classMetadata; 81 | } 82 | 83 | /** 84 | * @param $classMetadata 85 | * 86 | * @return $this 87 | */ 88 | public function setClassMetadata($classMetadata) 89 | { 90 | $this->classMetadata = $classMetadata; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * @return array 97 | */ 98 | public function getMethodMetadata() 99 | { 100 | return $this->methodMetadata; 101 | } 102 | 103 | /** 104 | * @param $name 105 | * @param $methodMetadata 106 | * 107 | * @return $this 108 | */ 109 | public function setMethodMetadata($name, $methodMetadata) 110 | { 111 | $this->methodMetadata[$name] = $methodMetadata; 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * @return array 118 | */ 119 | public function getPropertyMetadata() 120 | { 121 | return $this->propertyMetadata; 122 | } 123 | 124 | /** 125 | * @param $name 126 | * @param $properyMetadata 127 | * 128 | * @return $this 129 | */ 130 | public function setPropertyMetadata($name, $properyMetadata) 131 | { 132 | $this->propertyMetadata[$name] = $properyMetadata; 133 | 134 | return $this; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Metadata/MetadataProvider.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class MetadataProvider 11 | { 12 | /** 13 | * @var MetadataReader[] 14 | */ 15 | private $readers; 16 | 17 | /** 18 | * @param MetadataReader[] $readers 19 | */ 20 | public function __construct(array $readers) 21 | { 22 | $this->readers = $readers; 23 | } 24 | 25 | /** 26 | * @return Metadata[] 27 | */ 28 | public function getMetadata() 29 | { 30 | $metadata = []; 31 | foreach ($this->readers as $reader) { 32 | $metadata = array_merge($metadata, $reader->getMetadata()); 33 | } 34 | 35 | return $metadata; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Metadata/MetadataReader.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface MetadataReader 11 | { 12 | public function getMetadata(); 13 | } 14 | -------------------------------------------------------------------------------- /Normalizer/GroupValidationTrait.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | trait GroupValidationTrait 9 | { 10 | /** 11 | * @param array $context 12 | * @param array $groups 13 | * 14 | * @return bool 15 | */ 16 | protected function includeBasedOnGroup(array $context, array $groups) 17 | { 18 | foreach ($context['groups'] as $group) { 19 | if (in_array($group, $groups)) { 20 | return true; 21 | } 22 | } 23 | 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Normalizer/MetadataAwareDenormalizer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class MetadataAwareDenormalizer extends SerializerAwareNormalizer implements DenormalizerInterface 18 | { 19 | use GroupValidationTrait; 20 | 21 | /** 22 | * @var array 23 | */ 24 | private $metadata; 25 | 26 | /** 27 | * @var PropertyNameConverter 28 | */ 29 | private $propertyNameConverter; 30 | 31 | /** 32 | * @param array $metadata 33 | * @param PropertyNameConverter $pnc 34 | */ 35 | public function __construct(array $metadata, PropertyNameConverter $pnc) 36 | { 37 | $this->metadata = $metadata; 38 | $this->propertyNameConverter = $pnc; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function denormalize($data, $class, $format = null, array $context = []) 45 | { 46 | $meta = $this->getMetadata($class); 47 | $normalizedData = (array) $data; 48 | $object = $this->getInstance($class, $context); 49 | 50 | foreach ($normalizedData as $attribute => $value) { 51 | if (null === $propertyName = $this->getPropertyName($meta, $attribute)) { 52 | // Name is invalid, skip this 53 | continue; 54 | } 55 | 56 | if (null !== $value && !is_scalar($value)) { 57 | if (!$this->serializer instanceof DenormalizerInterface) { 58 | throw new LogicException(sprintf('Cannot denormalize attribute "%s" because injected serializer is not a normalizer', $attribute)); 59 | } 60 | 61 | if (null !== $propertyType = $this->getPropertyType($meta, $propertyName)) { 62 | $value = $this->serializer->denormalize($value, $propertyType, $format, $context); 63 | } 64 | } 65 | 66 | $this->setPropertyValue($object, $meta, $propertyName, $value, $context); 67 | } 68 | 69 | return $object; 70 | } 71 | 72 | /** 73 | * Set the property value. 74 | * 75 | * @param $object 76 | * @param array $meta 77 | * @param $propertyName 78 | * @param $value 79 | * @param array $context 80 | */ 81 | private function setPropertyValue($object, array $meta, $propertyName, $value, array $context) 82 | { 83 | if (!isset($meta['property'][$propertyName])) { 84 | $meta['property'][$propertyName] = []; 85 | } 86 | 87 | // Default exclusion policy is NONE 88 | $exclusionPolicy = isset($meta['class']['exclusion_policy']) ? $meta['class']['exclusion_policy'] : ExclusionPolicy::NONE; 89 | 90 | // If this property should be in the output 91 | $included = $exclusionPolicy === ExclusionPolicy::NONE; 92 | 93 | // If read_only is defined and true 94 | $readOnly = false; 95 | if (isset($meta['class']['read_only']) ? $meta['class']['read_only'] : false) { 96 | $readOnly = true; 97 | } 98 | 99 | $groups = ['Default']; 100 | $accessor = null; 101 | foreach ($meta['property'][$propertyName] as $name => $metaValue) { 102 | switch ($name) { 103 | case 'exclude': 104 | // Skip this 105 | return; 106 | case 'read_only': 107 | if ($metaValue === true) { 108 | return; 109 | } 110 | // If readOnly = false we should include this 111 | $readOnly = false; 112 | break; 113 | case 'expose': 114 | $included = true; 115 | break; 116 | case 'accessor': 117 | if (isset($metaValue['setter'])) { 118 | $accessor = $metaValue['setter']; 119 | } 120 | break; 121 | case 'groups': 122 | $groups = $metaValue; 123 | break; 124 | } 125 | } 126 | 127 | // Validate context groups 128 | if (!empty($context['groups'])) { 129 | $included = $this->includeBasedOnGroup($context, $groups); 130 | } 131 | 132 | if (!$included || $readOnly) { 133 | return; 134 | } 135 | 136 | if ($accessor) { 137 | $object->$accessor($value); 138 | } else { 139 | ReflectionPropertyAccess::set($object, $propertyName, $value); 140 | } 141 | } 142 | 143 | /** 144 | * Get instance of the class. 145 | * 146 | * @param string $class 147 | * @param array $context 148 | * 149 | * @return object 150 | */ 151 | private function getInstance($class, array $context) 152 | { 153 | if ( 154 | isset($context[AbstractNormalizer::OBJECT_TO_POPULATE]) && 155 | is_object($context[AbstractNormalizer::OBJECT_TO_POPULATE]) && 156 | $context[AbstractNormalizer::OBJECT_TO_POPULATE] instanceof $class 157 | ) { 158 | $object = $context[AbstractNormalizer::OBJECT_TO_POPULATE]; 159 | unset($context[AbstractNormalizer::OBJECT_TO_POPULATE]); 160 | 161 | return $object; 162 | } 163 | 164 | $reflectionClass = new \ReflectionClass($class); 165 | $constructor = $reflectionClass->getConstructor(); 166 | if (!$constructor) { 167 | return new $class(); 168 | } 169 | 170 | return $reflectionClass->newInstanceWithoutConstructor(); 171 | } 172 | 173 | /** 174 | * Get the property name for this normalized key name. This will aslo verify if the name is correct. 175 | * 176 | * @param array $rootMeta 177 | * @param string $serializedName 178 | * 179 | * @return string|null 180 | */ 181 | private function getPropertyName($rootMeta, $serializedName) 182 | { 183 | $propertyName = $this->propertyNameConverter->getPropertyName($rootMeta, $serializedName); 184 | 185 | $meta = isset($rootMeta['property'][$propertyName]) ? $rootMeta['property'][$propertyName] : []; 186 | $verify = $this->propertyNameConverter->getSerializedName($meta, $propertyName); 187 | 188 | if ($serializedName === $verify) { 189 | return $propertyName; 190 | } 191 | 192 | // The $serializedName was fake 193 | return; 194 | } 195 | 196 | /** 197 | * Get the type of this property. 198 | * 199 | * @param array $meta 200 | * @param string $name 201 | * 202 | * @return null|string 203 | */ 204 | private function getPropertyType($meta, $name) 205 | { 206 | foreach ($meta['property'][$name] as $metaName => $value) { 207 | if ($metaName === 'type') { 208 | return $value; 209 | } 210 | } 211 | 212 | return; 213 | } 214 | 215 | /** 216 | * @param mixed $data 217 | * @param string $type 218 | * @param null $format 219 | * 220 | * @return bool 221 | */ 222 | public function supportsDenormalization($data, $type, $format = null) 223 | { 224 | return isset($this->metadata[$type]); 225 | } 226 | 227 | /** 228 | * @param string $class 229 | * 230 | * @return array 231 | */ 232 | private function getMetadata($class) 233 | { 234 | return $this->metadata[$class]; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /Normalizer/MetadataAwareNormalizer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class MetadataAwareNormalizer extends SerializerAwareNormalizer implements NormalizerInterface 18 | { 19 | use GroupValidationTrait; 20 | 21 | /** 22 | * @var array 23 | */ 24 | private $metadata; 25 | 26 | /** 27 | * @var AttributeExtractor 28 | */ 29 | private $attributeExtractor; 30 | 31 | /** 32 | * @var PropertyNameConverter 33 | */ 34 | private $propertyNameConverter; 35 | 36 | /** 37 | * @param array $metadata 38 | * @param AttributeExtractor $attributeExtractor 39 | * @param PropertyNameConverter $pnc 40 | */ 41 | public function __construct(array $metadata, AttributeExtractor $attributeExtractor, PropertyNameConverter $pnc) 42 | { 43 | $this->metadata = $metadata; 44 | $this->attributeExtractor = $attributeExtractor; 45 | $this->propertyNameConverter = $pnc; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function normalize($object, $format = null, array $context = []) 52 | { 53 | $meta = $this->getMetadata($object); 54 | $attributes = $this->attributeExtractor->getAttributes($object); 55 | 56 | $normalizedData = []; 57 | foreach ($attributes['property'] as $propertyName => $bool) { 58 | $this->normalizeProperty($normalizedData, $meta, $object, $propertyName, $context); 59 | } 60 | foreach ($attributes['method'] as $propertyName => $bool) { 61 | $this->normalizeMethod($normalizedData, $meta, $object, $propertyName, $context); 62 | } 63 | 64 | foreach ($normalizedData as $name => $value) { 65 | if (null !== $value && !is_scalar($value)) { 66 | if (!$this->serializer instanceof NormalizerInterface) { 67 | throw new LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $name)); 68 | } 69 | 70 | $normalizedData[$name] = $this->serializer->normalize($value, $format, $context); 71 | } 72 | } 73 | 74 | return $normalizedData; 75 | } 76 | 77 | /** 78 | * Normalize a property. 79 | * 80 | * @param array $normalizedData 81 | * @param array $meta 82 | * @param $object 83 | * @param $propertyName 84 | * @param array $context 85 | */ 86 | protected function normalizeProperty(array &$normalizedData, array $meta, $object, $propertyName, array $context) 87 | { 88 | if (!isset($meta['property'][$propertyName])) { 89 | $meta['property'][$propertyName] = []; 90 | } 91 | 92 | // Default exclusion policy is NONE 93 | $exclusionPolicy = isset($meta['class']['exclusion_policy']) ? $meta['class']['exclusion_policy'] : ExclusionPolicy::NONE; 94 | 95 | // If this property should be in the output 96 | $included = $exclusionPolicy === ExclusionPolicy::NONE; 97 | 98 | $groups = ['Default']; 99 | $value = ReflectionPropertyAccess::get($object, $propertyName); 100 | foreach ($meta['property'][$propertyName] as $name => $metaValue) { 101 | switch ($name) { 102 | case 'exclude': 103 | // Skip this 104 | return; 105 | case 'expose': 106 | $included = true; 107 | break; 108 | case 'accessor': 109 | if (!empty($metaValue['getter'])) { 110 | $accessor = $metaValue['getter']; 111 | $value = $object->$accessor(); 112 | } 113 | break; 114 | case 'groups': 115 | $groups = $metaValue; 116 | break; 117 | } 118 | } 119 | 120 | // Validate context groups 121 | if (!empty($context['groups'])) { 122 | $included = $this->includeBasedOnGroup($context, $groups); 123 | } 124 | 125 | if (!$included) { 126 | return; 127 | } 128 | 129 | $serializedName = $this->propertyNameConverter->getSerializedName($meta['property'][$propertyName], $propertyName); 130 | $normalizedData[$serializedName] = $value; 131 | } 132 | 133 | /** 134 | * Normalize a method. 135 | * 136 | * @param array $normalizedData 137 | * @param array $meta 138 | * @param $object 139 | * @param $methodName 140 | * @param array $context 141 | */ 142 | protected function normalizeMethod(array &$normalizedData, array $meta, $object, $methodName, array $context) 143 | { 144 | if (!isset($meta['method'][$methodName])) { 145 | $meta['method'][$methodName] = []; 146 | } 147 | 148 | // Methods are never serialized by default 149 | $included = false; 150 | 151 | $groups = ['Default']; 152 | foreach ($meta['method'][$methodName] as $name => $metaValue) { 153 | switch ($name) { 154 | case 'expose': 155 | $included = true; 156 | break; 157 | case 'groups': 158 | $groups = $metaValue; 159 | break; 160 | } 161 | } 162 | 163 | // Validate context groups 164 | if (!empty($context['groups'])) { 165 | $included = $this->includeBasedOnGroup($context, $groups); 166 | } 167 | 168 | if (!$included) { 169 | return; 170 | } 171 | 172 | $serializedName = $this->propertyNameConverter->getSerializedName($meta['method'][$methodName], $methodName); 173 | $normalizedData[$serializedName] = $object->$methodName(); 174 | } 175 | 176 | /** 177 | * @param mixed $data 178 | * @param null $format 179 | * 180 | * @return bool 181 | */ 182 | public function supportsNormalization($data, $format = null) 183 | { 184 | if (!is_object($data)) { 185 | return false; 186 | } 187 | 188 | $class = get_class($data); 189 | 190 | return isset($this->metadata[$class]); 191 | } 192 | 193 | /** 194 | * @param object $object 195 | * 196 | * @return array 197 | */ 198 | private function getMetadata($object) 199 | { 200 | $class = get_class($object); 201 | 202 | return $this->metadata[$class]; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /PropertyManager/AttributeExtractor.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class AttributeExtractor 11 | { 12 | /** 13 | * @var array 14 | */ 15 | private $attributesCache = []; 16 | 17 | /** 18 | * Gets and caches attributes for this class and context. 19 | * 20 | * @param object|string $object 21 | * 22 | * @return string[] 23 | */ 24 | public function getAttributes($object) 25 | { 26 | if (is_string($object)) { 27 | $class = $object; 28 | } else { 29 | $class = get_class($object); 30 | } 31 | 32 | // get cache key 33 | if (false !== $key = $this->getCacheKey($class)) { 34 | if (isset($this->attributesCache[$key])) { 35 | return $this->attributesCache[$key]; 36 | } 37 | } 38 | 39 | return $this->attributesCache[$key] = $this->extractAttributes($class); 40 | } 41 | 42 | /** 43 | * Extracts attributes for this class and context. 44 | * 45 | * @param string $class 46 | * 47 | * @return string[] 48 | */ 49 | private function extractAttributes($class) 50 | { 51 | // If not using groups, detect manually 52 | $attributes = ['property' => [], 'method' => []]; 53 | 54 | // methods 55 | $reflClass = new \ReflectionClass($class); 56 | foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) { 57 | if ( 58 | $reflMethod->getNumberOfRequiredParameters() !== 0 || 59 | $reflMethod->isStatic() || 60 | $reflMethod->isConstructor() || 61 | $reflMethod->isDestructor() 62 | ) { 63 | continue; 64 | } 65 | 66 | $name = $reflMethod->name; 67 | 68 | $attributes['method'][$name] = true; 69 | } 70 | 71 | // properties 72 | foreach ($reflClass->getProperties() as $reflProperty) { 73 | if ($reflProperty->isStatic()) { 74 | continue; 75 | } 76 | 77 | $attributes['property'][$reflProperty->name] = true; 78 | } 79 | 80 | return $attributes; 81 | } 82 | 83 | /** 84 | * Get a good cache key for this class. 85 | * 86 | * @param $class 87 | * 88 | * @return bool|string 89 | */ 90 | private function getCacheKey($class) 91 | { 92 | try { 93 | return md5($class); 94 | } catch (\Exception $e) { 95 | // The context cannot be serialized, skip the cache 96 | return false; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /PropertyManager/PropertyNameConverter.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PropertyNameConverter 14 | { 15 | /** 16 | * @var NameConverterInterface 17 | */ 18 | private $nameConverter; 19 | 20 | /** 21 | * @param NameConverterInterface $nameConverter 22 | */ 23 | public function __construct(NameConverterInterface $nameConverter = null) 24 | { 25 | $this->nameConverter = $nameConverter; 26 | if ($this->nameConverter === null) { 27 | $this->nameConverter = new CamelCaseToSnakeCaseNameConverter(); 28 | } 29 | } 30 | 31 | /** 32 | * Get the serialized name. 33 | * 34 | * @param array $propertyMeta 35 | * @param string $propertyName 36 | * 37 | * @return string 38 | */ 39 | public function getSerializedName(array $propertyMeta, $propertyName) 40 | { 41 | foreach ($propertyMeta as $metaName => $metaValue) { 42 | if ($metaName === 'serialized_name') { 43 | return $metaValue; 44 | } 45 | } 46 | 47 | return $this->nameConverter->normalize($propertyName); 48 | } 49 | 50 | /** 51 | * Get the property name form a serialized name and meta. 52 | * 53 | * @param array $meta root 54 | * @param string $serializedName 55 | * 56 | * @return string 57 | */ 58 | public function getPropertyName($meta, $serializedName) 59 | { 60 | // Try find the name in the meta values 61 | foreach ($meta['property'] as $prop => $propertyMeta) { 62 | foreach ($propertyMeta as $metaName => $metaValue) { 63 | if ($metaName === 'serialized_name' && $metaValue === $serializedName) { 64 | return $prop; 65 | } 66 | } 67 | } 68 | 69 | // Fallback on the name converter 70 | return $this->nameConverter->denormalize($serializedName); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /PropertyManager/ReflectionPropertyAccess.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ReflectionPropertyAccess 11 | { 12 | /** 13 | * @param $object 14 | * @param $propertyName 15 | * @param $value 16 | */ 17 | public static function set($object, $propertyName, $value) 18 | { 19 | $reflectionProperty = self::getReflectionProperty($object, $propertyName); 20 | $reflectionProperty->setValue($object, $value); 21 | } 22 | 23 | /** 24 | * @param $object 25 | * @param $propertyName 26 | * 27 | * @return mixed 28 | */ 29 | public static function get($object, $propertyName) 30 | { 31 | $reflectionProperty = self::getReflectionProperty($object, $propertyName); 32 | 33 | return $reflectionProperty->getValue($object); 34 | } 35 | 36 | /** 37 | * @param $object 38 | * @param $propertyName 39 | * 40 | * @return \ReflectionProperty 41 | */ 42 | private static function getReflectionProperty($object, $propertyName) 43 | { 44 | $reflectionClass = new \ReflectionClass($object); 45 | 46 | $reflectionProperty = $reflectionClass->getProperty($propertyName); 47 | $reflectionProperty->setAccessible(true); 48 | 49 | return $reflectionProperty; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | [![Latest Version](https://img.shields.io/github/release/Happyr/SerializerBundle.svg?style=flat-square)](https://github.com/Happyr/SerializerBundle/releases) 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 4 | [![Build Status](https://img.shields.io/travis/Happyr/SerializerBundle.svg?style=flat-square)](https://travis-ci.org/Happyr/SerializerBundle) 5 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/Happyr/SerializerBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/Happyr/SerializerBundle) 6 | [![Quality Score](https://img.shields.io/scrutinizer/g/Happyr/SerializerBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/Happyr/SerializerBundle) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/happyr/serializer-bundle.svg?style=flat-square)](https://packagist.org/packages/happyr/serializer-bundle) 8 | 9 | **Make the Symfony's serializer easy to use.** 10 | 11 | 12 | ## Install 13 | 14 | Via Composer 15 | 16 | ``` bash 17 | $ composer require happyr/serializer-bundle 18 | ``` 19 | 20 | Enable the bundle in your kernel: 21 | 22 | ``` php 23 | model; 62 | } 63 | } 64 | 65 | class Owner 66 | { 67 | private $name; 68 | 69 | /** 70 | * @Serializer\Type("Car") 71 | */ 72 | private $car; 73 | 74 | /** 75 | * @Serializer\ReadOnly 76 | */ 77 | private $birthday; 78 | 79 | public function __construct() 80 | { 81 | $this->name = 'Tobias'; 82 | $this->car = new Car(true); 83 | $this->birthday = new \DateTime('1989-04-30'); 84 | } 85 | } 86 | 87 | $json = $this->container->get('serializer')->serialize(new Owner(), 'json'); 88 | var_dump($json); 89 | ``` 90 | 91 | ```json 92 | { 93 | "name":"Tobias", 94 | "car":{ 95 | "size":"Small", 96 | "model":"This is model: Volvo" 97 | }, 98 | "birthday":"1989-04-30T00:00:00+02:00" 99 | } 100 | ``` 101 | 102 | ## Under the hood 103 | 104 | This bundle provides a custom normalizer to [Symfony's serializer component](http://symfony.com/doc/current/components/serializer.html). This makes 105 | this serializer very flexible. If you want to serialize an object in a very custom way, 106 | add your own serializer as discibed in the [Symfony documentation](http://symfony.com/doc/current/cookbook/serializer.html). 107 | 108 | ## Configuration 109 | 110 | You need to provide one or more paths to where your source code is. 111 | 112 | ```yaml 113 | // app/config/config.yml 114 | happyr_serializer: 115 | source: ['%kernel.root_dir%/../src'] # default 116 | twig_extension: false # default 117 | ``` 118 | 119 | ## Adding metadata 120 | 121 | Currently you may only configure the normalizer with Annotations. These annotations 122 | are very similar to JmsSerializer. 123 | 124 | #### @ExclusionPolicy 125 | 126 | This annotation can be defined on a class to indicate the exclusion strategy 127 | that should be used for the class. 128 | 129 | 130 | | Policy | Description | 131 | | -------- | ----------- | 132 | | all | all properties are excluded by default; only properties marked with @Expose will be serialized/unserialized 133 | | none | no properties are excluded by default; all properties except those marked with @Exclude will be serialized/unserialized 134 | 135 | 136 | #### @Exclude 137 | 138 | This annotation can be defined on a property to indicate that the property should 139 | not be serialized/unserialized. Works only in combination with ExclusionPolicy = "NONE". 140 | 141 | #### @Expose 142 | This annotation can be defined on a property to indicate that the property should 143 | be serialized/unserialized. Works only in combination with ExclusionPolicy = "ALL". 144 | 145 | #### @SerializedName 146 | This annotation can be defined on a property to define the serialized name for a 147 | property. If this is not defined, the property will be translated from camel-case 148 | to a lower-cased underscored name, e.g. camelCase -> camel_case. 149 | 150 | 151 | #### @Groups 152 | This annotation can be defined on a property to specifiy to if the property 153 | should be serialized when only serializing specific groups. If this is excluded the 154 | property/method will get group "Default". 155 | 156 | 157 | #### @Accessor 158 | This annotation can be defined on a property to specify which public method should 159 | be called to retrieve, or set the value of the given property. By default we access 160 | properties by reflection. 161 | 162 | ```php 163 | 164 | name); 180 | } 181 | 182 | public function setName($name) 183 | { 184 | $this->name = $name; 185 | } 186 | } 187 | ``` 188 | 189 | #### @ReadOnly 190 | 191 | This annotation can be defined on a property to indicate that the data of the property 192 | is read only and cannot be set during deserialization. 193 | 194 | A property can be marked as non read only with `@ReadOnly(false)` annotation (useful when a class is marked as read only). 195 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | 4 | happyr.serializer.metadata.annotation_reader: 5 | class: Happyr\SerializerBundle\Metadata\AnnotationReader 6 | public: false 7 | arguments: 8 | - [] 9 | - '@annotation_reader' 10 | - "@happyr.serializer.property_access.attribute_extractor" 11 | 12 | happyr.serializer.metadata.metadata_provider: 13 | class: Happyr\SerializerBundle\Metadata\MetadataProvider 14 | public: false 15 | arguments: 16 | - ['@happyr.serializer.metadata.annotation_reader'] 17 | 18 | happyr.serializer.property_access.attribute_extractor: 19 | class: Happyr\SerializerBundle\PropertyManager\AttributeExtractor 20 | public: false 21 | 22 | happyr.serializer.normalizer.metadata_aware: 23 | class: Happyr\SerializerBundle\Normalizer\MetadataAwareNormalizer 24 | arguments: 25 | - [] 26 | - "@happyr.serializer.property_access.attribute_extractor" 27 | - "@happyr.serializer.normalizer.property_manager.name_converter" 28 | public: false 29 | tags: 30 | - { name: "serializer.normalizer", priority: -999 } 31 | 32 | happyr.serializer.denormalizer.metadata_aware: 33 | class: Happyr\SerializerBundle\Normalizer\MetadataAwareDenormalizer 34 | arguments: [[], "@happyr.serializer.normalizer.property_manager.name_converter"] 35 | public: false 36 | tags: 37 | - { name: "serializer.normalizer", priority: -999 } 38 | 39 | happyr.serializer.normalizer.property_manager.name_converter: 40 | class: Happyr\SerializerBundle\PropertyManager\PropertyNameConverter 41 | arguments: [~] 42 | public: false -------------------------------------------------------------------------------- /Resources/config/twig.yml: -------------------------------------------------------------------------------- 1 | services: 2 | happyr.serializer.twig.serializer: 3 | class: Happyr\SerializerBundle\Twig\SerializerExtension 4 | arguments: ["@serializer"] 5 | public: false 6 | tags: 7 | - { name: 'twig.extension' } -------------------------------------------------------------------------------- /Tests/App/AppKernel.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/config/config.yml'); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getCacheDir() 33 | { 34 | return sys_get_temp_dir().'/serializer-bundle/cache'; 35 | } 36 | 37 | /** 38 | * Clear the cache before boot. 39 | */ 40 | public function boot() 41 | { 42 | $this->removeDirectory($this->getCacheDir()); 43 | 44 | return parent::boot(); 45 | } 46 | 47 | /** 48 | * Removes a directory and all contents. 49 | * 50 | * @param string $dir 51 | * 52 | * @return bool 53 | */ 54 | private function removeDirectory($dir) 55 | { 56 | if (!is_dir($dir)) { 57 | return; 58 | } 59 | 60 | $files = array_diff(scandir($dir), ['.', '..']); 61 | foreach ($files as $file) { 62 | (is_dir("$dir/$file")) ? $this->removeDirectory("$dir/$file") : unlink("$dir/$file"); 63 | } 64 | 65 | return rmdir($dir); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function getLogDir() 72 | { 73 | return sys_get_temp_dir().'/serializer-bundle/logs'; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/App/autoload.php: -------------------------------------------------------------------------------- 1 | model = $model.'_setter'; 30 | 31 | return $this; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/Fixtures/Composition/Car.php: -------------------------------------------------------------------------------- 1 | model = 'val_model'; 22 | $this->carSize = 'val_size'; 23 | $this->color = 'val_color'; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/Fixtures/Composition/Owner.php: -------------------------------------------------------------------------------- 1 | name = 'Foobar'; 22 | $this->car = new Car(true); 23 | $this->birthday = new \DateTime('-21years'); 24 | } 25 | } 26 | 27 | /** 28 | * @return Car 29 | */ 30 | public function getCar() 31 | { 32 | return $this->car; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/Fixtures/Exclude/Car.php: -------------------------------------------------------------------------------- 1 | model = 'val_model'; 20 | $this->size = 'val_size'; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/Fixtures/ExclusionPolicy/ExcludeAll.php: -------------------------------------------------------------------------------- 1 | model = 'val_model'; 28 | $this->size = 'val_size'; 29 | $this->color = 'val_color'; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/Fixtures/ExclusionPolicy/ExcludeDefault.php: -------------------------------------------------------------------------------- 1 | model = 'val_model'; 25 | $this->size = 'val_size'; 26 | $this->color = 'val_color'; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/Fixtures/ExclusionPolicy/ExcludeNone.php: -------------------------------------------------------------------------------- 1 | model = 'val_model'; 28 | $this->size = 'val_size'; 29 | $this->color = 'val_color'; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/Fixtures/Expose/Car.php: -------------------------------------------------------------------------------- 1 | model = 'val_model'; 23 | $this->size = 'val_size'; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/Fixtures/Groups/Car.php: -------------------------------------------------------------------------------- 1 | model = 'val_model'; 25 | $this->size = 'val_size'; 26 | $this->color = 'val_color'; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/Fixtures/ReadOnly/Car.php: -------------------------------------------------------------------------------- 1 | model = 'val_model'; 20 | $this->size = 'val_size'; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/Fixtures/ReadOnly/ClassReadOnly.php: -------------------------------------------------------------------------------- 1 | model = 'val_model'; 28 | $this->size = 'val_size'; 29 | $this->color = 'val_color'; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/Fixtures/SerializedName/Car.php: -------------------------------------------------------------------------------- 1 | model = 'val_model'; 22 | $this->carSize = 'val_size'; 23 | $this->color = 'val_color'; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/Functional/AccessorTest.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class AccessorTest extends SerializerTestCase 11 | { 12 | public function testSerialize() 13 | { 14 | $data = $this->serialize(new Car()); 15 | 16 | $this->assertTrue(isset($data['model'])); 17 | $this->assertEquals('getModel', $data['model']); 18 | } 19 | 20 | public function testDeserialize() 21 | { 22 | $data = ['model' => 'model_val']; 23 | $obj = $this->deserialize($data, Car::class); 24 | 25 | $this->assertPropertyValue($obj, 'model', 'model_val_setter'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/Functional/CompositionTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class CompositionTest extends SerializerTestCase 14 | { 15 | public function testSerialize() 16 | { 17 | $data = $this->serialize(new Owner(true)); 18 | 19 | $this->assertTrue(isset($data['car'])); 20 | $this->assertTrue(isset($data['car']['color'])); 21 | $this->assertTrue(isset($data['name'])); 22 | $this->assertTrue(isset($data['birthday'])); 23 | } 24 | 25 | public function testDeserialize() 26 | { 27 | $data = json_decode('{"name":"Foobar","car":{"super_model":"val_model","car_size":"val_size","color":"val_color"},"birthday":"1995-07-14T20:07:41+02:00"}', true); 28 | $obj = $this->deserialize($data, Owner::class); 29 | 30 | $this->assertPropertyValue($obj, 'name', 'Foobar'); 31 | $car = $obj->getCar(); 32 | $this->assertInstanceOf(Car::class, $car); 33 | $this->assertPropertyValue($car, 'color', 'val_color'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/Functional/ExcludeTest.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ExcludeTest extends SerializerTestCase 11 | { 12 | public function testSerialize() 13 | { 14 | $data = $this->serialize(new Car(true)); 15 | 16 | $this->assertFalse(isset($data['model'])); 17 | $this->assertTrue(isset($data['size'])); 18 | } 19 | 20 | public function testDeserialize() 21 | { 22 | $data = ['model' => 'model_value', 'size' => 'size_value']; 23 | $obj = $this->deserialize($data, Car::class); 24 | 25 | $this->assertPropertyValue($obj, 'model', null); 26 | $this->assertPropertyValue($obj, 'size', 'size_value'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/Functional/ExclusionPolicyTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ExclusionPolicyTest extends SerializerTestCase 13 | { 14 | public function testSerializeDefault() 15 | { 16 | $data = $this->serialize(new ExcludeDefault(true)); 17 | 18 | $this->assertFalse(isset($data['model'])); 19 | $this->assertTrue(isset($data['size'])); 20 | $this->assertTrue(isset($data['color'])); 21 | } 22 | 23 | public function testSerializeNone() 24 | { 25 | $data = $this->serialize(new ExcludeNone(true)); 26 | 27 | $this->assertFalse(isset($data['model'])); 28 | $this->assertTrue(isset($data['size'])); 29 | $this->assertTrue(isset($data['color'])); 30 | } 31 | 32 | public function testSerializeAll() 33 | { 34 | $data = $this->serialize(new ExcludeAll(true)); 35 | 36 | $this->assertFalse(isset($data['model'])); 37 | $this->assertTrue(isset($data['size'])); 38 | $this->assertFalse(isset($data['color'])); 39 | } 40 | 41 | public function testDeserializeDefault() 42 | { 43 | $data = ['model' => 'model_value', 'size' => 'size_value', 'color' => 'color_value']; 44 | $obj = $this->deserialize($data, ExcludeDefault::class); 45 | 46 | $this->assertPropertyValue($obj, 'model', null); 47 | $this->assertPropertyValue($obj, 'size', 'size_value'); 48 | $this->assertPropertyValue($obj, 'color', 'color_value'); 49 | } 50 | 51 | public function testDeserializeNone() 52 | { 53 | $data = ['model' => 'model_value', 'size' => 'size_value', 'color' => 'color_value']; 54 | $obj = $this->deserialize($data, ExcludeNone::class); 55 | 56 | $this->assertPropertyValue($obj, 'model', null); 57 | $this->assertPropertyValue($obj, 'size', 'size_value'); 58 | $this->assertPropertyValue($obj, 'color', 'color_value'); 59 | } 60 | 61 | public function testDeserializeAll() 62 | { 63 | $data = ['model' => 'model_value', 'size' => 'size_value', 'color' => 'color_value']; 64 | $obj = $this->deserialize($data, ExcludeAll::class); 65 | 66 | $this->assertPropertyValue($obj, 'model', null); 67 | $this->assertPropertyValue($obj, 'size', 'size_value'); 68 | $this->assertPropertyValue($obj, 'color', null); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/Functional/ExposeTest.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ExposeTest extends SerializerTestCase 11 | { 12 | public function testSerialize() 13 | { 14 | $data = $this->serialize(new Car(true)); 15 | 16 | $this->assertTrue(isset($data['model'])); 17 | $this->assertFalse(isset($data['size'])); 18 | } 19 | 20 | public function testDeserialize() 21 | { 22 | $data = ['model' => 'model_value', 'size' => 'size_value']; 23 | $obj = $this->deserialize($data, Car::class); 24 | 25 | $this->assertPropertyValue($obj, 'model', 'model_value'); 26 | $this->assertPropertyValue($obj, 'size', null); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/Functional/GroupsTest.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class GroupsTest extends SerializerTestCase 11 | { 12 | public function testSerialize() 13 | { 14 | $data = $this->serialize(new Car(true), ['groups' => ['First']]); 15 | $this->assertTrue(isset($data['model'])); 16 | $this->assertTrue(isset($data['size'])); 17 | $this->assertFalse(isset($data['color'])); 18 | 19 | $data = $this->serialize(new Car(true), ['groups' => ['Second']]); 20 | $this->assertTrue(isset($data['model'])); 21 | $this->assertFalse(isset($data['size'])); 22 | $this->assertFalse(isset($data['color'])); 23 | 24 | $data = $this->serialize(new Car(true), ['groups' => ['First', 'Second']]); 25 | $this->assertTrue(isset($data['model'])); 26 | $this->assertTrue(isset($data['size'])); 27 | $this->assertFalse(isset($data['color'])); 28 | 29 | $data = $this->serialize(new Car(true), ['groups' => ['Default', 'Second']]); 30 | $this->assertTrue(isset($data['model'])); 31 | $this->assertFalse(isset($data['size'])); 32 | $this->assertTrue(isset($data['color'])); 33 | 34 | $data = $this->serialize(new Car(true), ['groups' => []]); 35 | $this->assertTrue(isset($data['model'])); 36 | $this->assertTrue(isset($data['size'])); 37 | $this->assertTrue(isset($data['color'])); 38 | } 39 | 40 | public function testDeserialize() 41 | { 42 | $data = ['model' => 'model_value', 'size' => 'size_value', 'color' => 'color_value']; 43 | 44 | $obj = $this->deserialize($data, Car::class, ['groups' => ['First']]); 45 | $this->assertPropertyValue($obj, 'model', 'model_value'); 46 | $this->assertPropertyValue($obj, 'size', 'size_value'); 47 | $this->assertPropertyValue($obj, 'color', null); 48 | 49 | $obj = $this->deserialize($data, Car::class, ['groups' => ['Second']]); 50 | $this->assertPropertyValue($obj, 'model', 'model_value'); 51 | $this->assertPropertyValue($obj, 'size', null); 52 | $this->assertPropertyValue($obj, 'color', null); 53 | 54 | $obj = $this->deserialize($data, Car::class, ['groups' => ['First', 'Second']]); 55 | $this->assertPropertyValue($obj, 'model', 'model_value'); 56 | $this->assertPropertyValue($obj, 'size', 'size_value'); 57 | $this->assertPropertyValue($obj, 'color', null); 58 | 59 | $obj = $this->deserialize($data, Car::class, ['groups' => ['Default', 'Second']]); 60 | $this->assertPropertyValue($obj, 'model', 'model_value'); 61 | $this->assertPropertyValue($obj, 'size', null); 62 | $this->assertPropertyValue($obj, 'color', 'color_value'); 63 | 64 | $obj = $this->deserialize($data, Car::class, ['groups' => []]); 65 | $this->assertPropertyValue($obj, 'model', 'model_value'); 66 | $this->assertPropertyValue($obj, 'size', 'size_value'); 67 | $this->assertPropertyValue($obj, 'color', 'color_value'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/Functional/ReadOnlyTest.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class ReadOnlyTest extends SerializerTestCase 12 | { 13 | public function testSerialize() 14 | { 15 | $data = $this->serialize(new Car(true)); 16 | 17 | $this->assertTrue(isset($data['model'])); 18 | $this->assertTrue(isset($data['size'])); 19 | } 20 | 21 | public function testDeserialize() 22 | { 23 | $data = ['model' => 'model_value', 'size' => 'size_value']; 24 | $obj = $this->deserialize($data, Car::class); 25 | 26 | $this->assertPropertyValue($obj, 'model', null); 27 | $this->assertPropertyValue($obj, 'size', 'size_value'); 28 | } 29 | 30 | public function testSerializeReadOnlyClass() 31 | { 32 | $data = $this->serialize(new ClassReadOnly(true)); 33 | 34 | $this->assertTrue(isset($data['model'])); 35 | $this->assertTrue(isset($data['size'])); 36 | $this->assertTrue(isset($data['color'])); 37 | } 38 | 39 | public function testDeserializeReadOnlyClass() 40 | { 41 | $data = ['model' => 'model_value', 'size' => 'size_value', 'color' => 'color_value']; 42 | $obj = $this->deserialize($data, ClassReadOnly::class); 43 | 44 | $this->assertPropertyValue($obj, 'model', 'model_value'); 45 | $this->assertPropertyValue($obj, 'size', null); 46 | $this->assertPropertyValue($obj, 'color', null); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/Functional/SerializedNameTest.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class SerializedNameTest extends SerializerTestCase 11 | { 12 | public function testSerialize() 13 | { 14 | $data = $this->serialize(new Car(true)); 15 | 16 | $this->assertTrue(isset($data['super_model'])); 17 | $this->assertTrue(isset($data['car_size'])); 18 | $this->assertTrue(isset($data['color'])); 19 | } 20 | 21 | public function testDeserialize() 22 | { 23 | $data = ['super_model' => 'model_val', 'car_size' => 'size_val', 'color' => 'color_val']; 24 | $obj = $this->deserialize($data, Car::class); 25 | 26 | $this->assertPropertyValue($obj, 'model', 'model_val'); 27 | $this->assertPropertyValue($obj, 'carSize', 'size_val'); 28 | $this->assertPropertyValue($obj, 'color', 'color_val'); 29 | } 30 | 31 | /** 32 | * Test when the json data is named as the properties. They should be ignored. 33 | */ 34 | public function testDeserializeWhenIgnoringSerializedName() 35 | { 36 | $data = ['model' => 'model_val', 'carSize' => 'size_val', 'color' => 'color_val']; 37 | $obj = $this->deserialize($data, Car::class); 38 | 39 | $this->assertPropertyValue($obj, 'model', null); 40 | $this->assertPropertyValue($obj, 'carSize', null); 41 | $this->assertPropertyValue($obj, 'color', 'color_val'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/Functional/SerializerTestCase.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class SerializerTestCase extends WebTestCase 13 | { 14 | /** 15 | * @var SerializerInterface 16 | */ 17 | private $serializer; 18 | 19 | public function setUp() 20 | { 21 | static::bootKernel(); 22 | } 23 | 24 | /** 25 | * @return SerializerInterface 26 | */ 27 | protected function getSerializer() 28 | { 29 | if ($this->serializer) { 30 | return $this->serializer; 31 | } 32 | 33 | $container = static::$kernel->getContainer(); 34 | 35 | return $this->serializer = $container->get('serializer'); 36 | } 37 | 38 | /** 39 | * @param $obj 40 | * 41 | * @return array assoc 42 | */ 43 | protected function serialize($obj, array $context = []) 44 | { 45 | $json = $this->getSerializer()->serialize($obj, 'json', $context); 46 | 47 | return json_decode($json, true); 48 | } 49 | 50 | /** 51 | * @param array $data 52 | * @param string $type 53 | * @param array $context 54 | * 55 | * @return object 56 | */ 57 | protected function deserialize(array $data, $type, array $context = []) 58 | { 59 | return $this->getSerializer()->deserialize(json_encode($data), $type, 'json', $context); 60 | } 61 | 62 | /** 63 | * Assert $obj->name is $value. 64 | * 65 | * @param object $obj 66 | * @param string $name 67 | * @param mixed $value 68 | */ 69 | protected function assertPropertyValue($obj, $name, $value) 70 | { 71 | $this->assertEquals($value, ReflectionPropertyAccess::get($obj, $name)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/Unit/Metadata/MetadataProviderTest.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class MetadataProviderTest extends \PHPUnit_Framework_TestCase 12 | { 13 | public function testGetMetadata() 14 | { 15 | $reader0 = $this->getMock(MetadataReader::class); 16 | $reader0->expects($this->once()) 17 | ->method('getMetadata') 18 | ->willReturn(['m0', 'm1']); 19 | $reader1 = $this->getMock(MetadataReader::class); 20 | $reader1->expects($this->once()) 21 | ->method('getMetadata') 22 | ->willReturn(['m2', 'm3']); 23 | 24 | $metadataReader = new MetadataProvider([$reader0, $reader1]); 25 | $result = $metadataReader->getMetadata(); 26 | 27 | $this->assertTrue(in_array('m0', $result)); 28 | $this->assertTrue(in_array('m1', $result)); 29 | $this->assertTrue(in_array('m2', $result)); 30 | $this->assertTrue(in_array('m3', $result)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Twig/SerializerExtension.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class SerializerExtension extends \Twig_Extension 11 | { 12 | /** 13 | * @var SerializerInterface 14 | */ 15 | protected $serializer; 16 | 17 | /** 18 | * @param SerializerInterface $serializer 19 | */ 20 | public function __construct(SerializerInterface $serializer) 21 | { 22 | $this->serializer = $serializer; 23 | } 24 | 25 | public function getFilters() 26 | { 27 | return [ 28 | new \Twig_SimpleFilter('serialize', [$this, 'serialize']), 29 | ]; 30 | } 31 | 32 | /** 33 | * @param mixed $object 34 | * @param string $type 35 | * @param array $context 36 | */ 37 | public function serialize($object, $type = 'json', array $context = []) 38 | { 39 | return $this->serializer->serialize($object, $type, $context); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getName() 46 | { 47 | return 'happyr_serializer'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happyr/serializer-bundle", 3 | "description": "A bundle on top of Symfony's serializer. This supports annotations", 4 | "type": "symfony-bundle", 5 | "keywords": ["serializer", "annotation"], 6 | "homepage": "http://developer.happyr.io", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Tobias Nyholm", 11 | "email": "tobias.nyholm@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^5.5|^7.0", 16 | "symfony/serializer": "^2.7.10|^3.0", 17 | "symfony/finder": "^2.7|^3.0", 18 | "doctrine/annotations": "^1.2", 19 | "twig/twig": "^1.18|^2.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^4.8", 23 | "symfony/symfony": "^2.7|^3.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Happyr\\SerializerBundle\\": "" 28 | } 29 | }, 30 | "scripts": { 31 | "test": "vendor/bin/phpunit", 32 | "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | ./Tests/Unit 12 | 13 | 14 | ./Tests/Functional 15 | 16 | 17 | 18 | 19 | 20 | ./ 21 | 22 | ./Resources 23 | ./Tests 24 | ./vendor 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | --------------------------------------------------------------------------------