RestDenormalizer   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 237
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 86.46%

Importance

Changes 0
Metric Value
wmc 45
lcom 1
cbo 11
dl 0
loc 237
ccs 83
cts 96
cp 0.8646
rs 8.8
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A denormalize() 0 17 4
A supportsDenormalization() 0 8 2
A hasCacheableSupportsMethod() 0 4 1
A updateObject() 0 14 4
A updateProperty() 0 12 3
A updateByReference() 0 16 3
A updatePropertyObject() 0 11 2
A isUpdateable() 0 12 6
A isGranted() 0 20 4
A resolveSubject() 0 8 2
A convert() 0 15 5
B isUpdateableByReference() 0 18 7
A setAuthorizationChecker() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like RestDenormalizer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RestDenormalizer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Dontdrinkandroot\RestBundle\Serializer;
4
5
use Doctrine\Common\Util\ClassUtils;
6
use Doctrine\DBAL\Types\Type;
7
use Doctrine\ORM\EntityManagerInterface;
8
use Dontdrinkandroot\RestBundle\Metadata\Annotation\Method;
9
use Dontdrinkandroot\RestBundle\Metadata\Annotation\Right;
10
use Dontdrinkandroot\RestBundle\Metadata\PropertyMetadata;
11
use Dontdrinkandroot\RestBundle\Metadata\RestMetadataFactory;
12
use Symfony\Component\HttpFoundation\Request;
13
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
14
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
15
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
16
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
17
18
/**
19
 * @author Philip Washington Sorst <[email protected]>
20
 */
21
class RestDenormalizer implements DenormalizerInterface, CacheableSupportsMethodInterface
22
{
23
    const DDR_REST_METHOD = 'ddrRestMethod';
24
    const DDR_REST_ENTITY = 'ddrRestEntity';
25
26
    /**
27
     * @var RestMetadataFactory
28
     */
29
    private $metadataFactory;
30
31
    /**
32
     * @var PropertyAccessorInterface
33
     */
34
    private $propertyAccessor;
35
36
    /**
37
     * @var AuthorizationCheckerInterface|null
38
     */
39
    private $authorizationChecker;
40
41
    /**
42
     * @var EntityManagerInterface
43
     */
44
    private $entityManager;
45
46 82
    public function __construct(
47
        RestMetadataFactory $metadataFactory,
48
        PropertyAccessorInterface $propertyAccessor,
49
        EntityManagerInterface $entityManager
50
    ) {
51 82
        $this->metadataFactory = $metadataFactory;
52 82
        $this->propertyAccessor = $propertyAccessor;
53 82
        $this->entityManager = $entityManager;
54 82
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59 26
    public function denormalize($data, $class, $format = null, array $context = [])
60
    {
61 26
        if (!array_key_exists(self::DDR_REST_METHOD, $context)) {
62
            throw new \BadMethodCallException('No REST Method specified');
63
        }
64
65 26
        $method = $context[self::DDR_REST_METHOD];
66 26
        $entity = array_key_exists(self::DDR_REST_ENTITY, $context) ? $context[self::DDR_REST_ENTITY] : null;
67
68 26
        if (null === $entity) {
69 16
            $entity = new $class;
70
        }
71
72 26
        $this->updateObject($entity, $method, $data);
73
74 26
        return $entity;
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 26
    public function supportsDenormalization($data, $type, $format = null)
81
    {
82 26
        if ('json' === $format) {
83 26
            return true;
84
        }
85
86
        return false;
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92 26
    public function hasCacheableSupportsMethod(): bool
93
    {
94 26
        return true;
95
    }
96
97
    /**
98
     * @param object $object Access by reference.
99
     * @param string $method
100
     * @param array  $data
101
     */
102 26
    protected function updateObject(&$object, $method, $data)
103
    {
104 26
        $classMetadata = $this->metadataFactory->getMetadataForClass(ClassUtils::getClass($object));
105
106 26
        foreach ($data as $key => $value) {
107 26
            if (array_key_exists($key, $classMetadata->propertyMetadata)) {
108
                /** @var PropertyMetadata $propertyMetadata */
109 26
                $propertyMetadata = $classMetadata->propertyMetadata[$key];
110 26
                if ($this->isUpdateable($object, $method, $propertyMetadata)) {
111 26
                    $this->updateProperty($object, $method, $propertyMetadata, $value);
112
                }
113
            }
114
        }
115 26
    }
116
117
    /**
118
     * @param object           $object Access by reference.
119
     * @param string           $method
120
     * @param PropertyMetadata $propertyMetadata
121
     * @param mixed            $value
122
     */
123 26
    protected function updateProperty(&$object, string $method, PropertyMetadata $propertyMetadata, $value)
124
    {
125 26
        $byReference = $this->isUpdateableByReference($propertyMetadata, $method);
126 26
        if ($byReference) {
127 4
            $this->updateByReference($object, $propertyMetadata, $value);
128 22
        } elseif (array_key_exists($propertyMetadata->getType(), Type::getTypesMap())) {
129 22
            $convertedValue = $this->convert($propertyMetadata->getType(), $value);
130 22
            $this->propertyAccessor->setValue($object, $propertyMetadata->name, $convertedValue);
131
        } else {
132 2
            $this->updatePropertyObject($object, $method, $propertyMetadata, $value);
133
        }
134 26
    }
135
136 4
    private function updateByReference(&$object, PropertyMetadata $propertyMetadata, $value)
137
    {
138 4
        if (null === $value) {
139
            $this->propertyAccessor->setValue($object, $propertyMetadata->name, null);
140
        } else {
141 4
            $type = $propertyMetadata->getType();
142 4
            $classMetadata = $this->entityManager->getClassMetadata($type);
143 4
            $identifiers = $classMetadata->getIdentifier();
144 4
            $id = [];
145 4
            foreach ($identifiers as $idName) {
146 4
                $id[$idName] = $value[$idName];
147
            }
148 4
            $reference = $this->entityManager->getReference($type, $id);
149 4
            $this->propertyAccessor->setValue($object, $propertyMetadata->name, $reference);
150
        }
151 4
    }
152
153 2
    protected function updatePropertyObject(&$object, string $method, PropertyMetadata $propertyMetadata, $value)
154
    {
155 2
        $propertyObject = $this->propertyAccessor->getValue($object, $propertyMetadata->name);
156 2
        if (null === $propertyObject) {
157
            $type = $propertyMetadata->getType();
158
            $propertyObject = new $type;
159
        }
160
161 2
        $this->updateObject($propertyObject, $method, $value);
162 2
        $this->propertyAccessor->setValue($object, $propertyMetadata->name, $propertyObject);
163 2
    }
164
165
    /**
166
     * @param string           $method
167
     * @param object           $object
168
     * @param PropertyMetadata $propertyMetadata
169
     *
170
     * @return bool
171
     */
172 26
    protected function isUpdateable($object, string $method, PropertyMetadata $propertyMetadata): bool
173
    {
174 26
        if ((Request::METHOD_PUT === $method || Request::METHOD_PATCH === $method) && $propertyMetadata->isPuttable()) {
175 10
            return $this->isGranted($object, $propertyMetadata->getPuttable()->right);
176
        }
177
178 22
        if (Request:: METHOD_POST === $method && $propertyMetadata->isPostable()) {
179 16
            return $this->isGranted($object, $propertyMetadata->getPostable()->right);
180
        }
181
182 12
        return false;
183
    }
184
185 26
    private function isGranted($object, ?Right $right): bool
186
    {
187 26
        if (null === $right) {
188 26
            return true;
189
        }
190
191
        /* If no Security is enabled always deny access */
192 12
        if (null === $this->authorizationChecker) {
193
            return false;
194
        }
195
196 12
        $propertyPath = $right->propertyPath;
197 12
        if (null === $propertyPath) {
198 12
            return $this->authorizationChecker->isGranted($right->attributes);
199
        } else {
200
            $subject = $this->resolveSubject($object, $propertyPath);
201
202
            return $this->authorizationChecker->isGranted($right->attributes, $subject);
203
        }
204
    }
205
206
    private function resolveSubject($entity, $propertyPath)
207
    {
208
        if ('this' === $propertyPath) {
209
            return $entity;
210
        }
211
212
        return $this->propertyAccessor->getValue($entity, $propertyPath);
213
    }
214
215 22
    private function convert(?string $type, $value)
216
    {
217 22
        if (null === $value) {
218
            return $value;
219
        }
220
221
        switch ($type) {
222 22
            case Type::DATETIME:
223 22
            case Type::DATE:
224 22
            case TYPE::TIME:
225 2
                return new \DateTime($value);
226
            default:
227 22
                return $value;
228
        }
229
    }
230
231 26
    private function isUpdateableByReference(PropertyMetadata $propertyMetadata, string $method)
232
    {
233
        if (
234 26
            Method::PUT === $method
235 26
            && null !== $propertyMetadata->getPuttable() && true === $propertyMetadata->getPuttable()->byReference
236
        ) {
237 2
            return true;
238
        }
239
240
        if (
241 24
            Method::POST === $method
242 24
            && null !== $propertyMetadata->getPostable() && true === $propertyMetadata->getPostable()->byReference
243
        ) {
244 2
            return true;
245
        }
246
247 22
        return false;
248
    }
249
250
    /**
251
     * @param AuthorizationCheckerInterface $authorizationChecker
252
     */
253 78
    public function setAuthorizationChecker(AuthorizationCheckerInterface $authorizationChecker)
254
    {
255 78
        $this->authorizationChecker = $authorizationChecker;
256 78
    }
257
}
258