Completed
Push — master ( 43e8f3...ac8b3b )
by Philip
04:11
created

RestRequestParser   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 86.96%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 41
c 5
b 0
f 0
lcom 1
cbo 13
dl 0
loc 241
ccs 80
cts 92
cp 0.8696
rs 8.2769

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A parseEntity() 0 22 3
A getRequestContent() 0 9 2
A updateObject() 0 17 4
A updateProperty() 0 16 3
A updateByReference() 0 16 3
A updatePropertyObject() 0 15 2
B isUpdateable() 0 15 6
A isGranted() 0 15 3
A resolveSubject() 0 8 2
B convert() 0 15 5
B isUpdateableByReference() 0 18 7

How to fix   Complexity   

Complex Class

Complex classes like RestRequestParser 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 RestRequestParser, and based on these observations, apply Extract Interface, too.

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