Skip to content

Commit 0050bbb

Browse files
committed
[Serializer] Introduce ObjectNormalizer
1 parent ff70902 commit 0050bbb

File tree

4 files changed

+664
-1
lines changed

4 files changed

+664
-1
lines changed

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ CHANGELOG
1515
`PropertyNormalizer::setCamelizedAttributes()` are replaced by
1616
`CamelCaseToSnakeCaseNameConverter`
1717
* [DEPRECATION] the `Exception` interface has been renamed to `ExceptionInterface`
18+
* added `ObjectNormalizer` leveraging the `PropertyAccess` component to normalize
19+
objects containing both properties and getters / setters / issers / hassers methods.
1820

1921
2.6.0
2022
-----
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Normalizer;
13+
14+
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
15+
use Symfony\Component\PropertyAccess\PropertyAccess;
16+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
17+
use Symfony\Component\Serializer\Exception\CircularReferenceException;
18+
use Symfony\Component\Serializer\Exception\LogicException;
19+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
20+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
21+
22+
/**
23+
* Converts between objects and arrays using the PropertyAccess component.
24+
*
25+
* @author Kévin Dunglas <[email protected]>
26+
*/
27+
class ObjectNormalizer extends AbstractNormalizer
28+
{
29+
/**
30+
* @var PropertyAccessorInterface
31+
*/
32+
protected $propertyAccessor;
33+
34+
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null)
35+
{
36+
parent::__construct($classMetadataFactory, $nameConverter);
37+
38+
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function supportsNormalization($data, $format = null)
45+
{
46+
return is_object($data);
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*
52+
* @throws CircularReferenceException
53+
*/
54+
public function normalize($object, $format = null, array $context = array())
55+
{
56+
if ($this->isCircularReference($object, $context)) {
57+
return $this->handleCircularReference($object);
58+
}
59+
60+
$data = array();
61+
$attributes = $this->getAllowedAttributes($object, $context, true);
62+
63+
// If not using groups, detect manually
64+
if (false === $attributes) {
65+
$attributes = array();
66+
67+
// methods
68+
$reflClass = new \ReflectionClass($object);
69+
foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) {
70+
if (
71+
!$reflMethod->isConstructor() &&
72+
!$reflMethod->isDestructor() &&
73+
0 === $reflMethod->getNumberOfRequiredParameters()
74+
) {
75+
$name = $reflMethod->getName();
76+
77+
if (strpos($name, 'get') === 0 || strpos($name, 'has') === 0) {
78+
// getters and hassers
79+
$attributes[lcfirst(substr($name, 3))] = true;
80+
} elseif (strpos($name, 'is') === 0) {
81+
// issers
82+
$attributes[lcfirst(substr($name, 2))] = true;
83+
}
84+
}
85+
}
86+
87+
// properties
88+
foreach ($reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $reflProperty) {
89+
$attributes[$reflProperty->getName()] = true;
90+
}
91+
92+
$attributes = array_keys($attributes);
93+
}
94+
95+
foreach ($attributes as $attribute) {
96+
if (in_array($attribute, $this->ignoredAttributes)) {
97+
continue;
98+
}
99+
100+
$attributeValue = $this->propertyAccessor->getValue($object, $attribute);
101+
102+
if (isset($this->callbacks[$attribute])) {
103+
$attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue);
104+
}
105+
106+
if (null !== $attributeValue && !is_scalar($attributeValue)) {
107+
if (!$this->serializer instanceof NormalizerInterface) {
108+
throw new LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $attribute));
109+
}
110+
111+
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
112+
}
113+
114+
if ($this->nameConverter) {
115+
$attribute = $this->nameConverter->normalize($attribute);
116+
}
117+
118+
$data[$attribute] = $attributeValue;
119+
}
120+
121+
return $data;
122+
}
123+
124+
/**
125+
* {@inheritdoc}
126+
*/
127+
public function supportsDenormalization($data, $type, $format = null)
128+
{
129+
return class_exists($type);
130+
}
131+
132+
/**
133+
* {@inheritdoc}
134+
*/
135+
public function denormalize($data, $class, $format = null, array $context = array())
136+
{
137+
$allowedAttributes = $this->getAllowedAttributes($class, $context, true);
138+
$normalizedData = $this->prepareForDenormalization($data);
139+
140+
$reflectionClass = new \ReflectionClass($class);
141+
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes);
142+
143+
foreach ($normalizedData as $attribute => $value) {
144+
$allowed = $allowedAttributes === false || in_array($attribute, $allowedAttributes);
145+
$ignored = in_array($attribute, $this->ignoredAttributes);
146+
147+
if ($allowed && !$ignored) {
148+
if ($this->nameConverter) {
149+
$attribute = $this->nameConverter->normalize($attribute);
150+
}
151+
152+
try {
153+
$this->propertyAccessor->setValue($object, $attribute, $value);
154+
} catch (NoSuchPropertyException $exception) {
155+
// Properties not found are ignored
156+
}
157+
}
158+
}
159+
160+
return $object;
161+
}
162+
}

0 commit comments

Comments
 (0)