Completed
Push — master ( 56c59d...5628a9 )
by Philip
07:18
created

AbstractRestResourceController   D

Complexity

Total Complexity 49

Size/Duplication

Total Lines 538
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 19

Test Coverage

Coverage 92.18%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 19
dl 0
loc 538
ccs 165
cts 179
cp 0.9218
rs 4.6787
c 0
b 0
f 0

42 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 1
B listAction() 0 25 2
A postAction() 0 18 2
A getAction() 0 9 1
A putAction() 0 18 2
A deleteAction() 0 8 1
B listSubresourceAction() 0 26 2
A postSubresourceAction() 0 22 2
A putSubresourceAction() 0 8 1
A deleteSubresourceAction() 0 8 1
A postProcessPostedEntity() 0 4 1
A postProcessPuttedEntity() 0 4 1
A postProcessSubResourcePostedEntity() 0 4 1
A getEntityClass() 0 4 1
A getSubResourceEntityClass() 0 7 1
A getCurrentRequest() 0 4 1
A assertMethodGranted() 0 7 3
A assertSubResourceMethodGranted() 0 10 2
A getClassMetadata() 0 8 1
A resolveSubject() 0 9 2
A assertRightGranted() 0 10 3
A parseIncludes() 0 16 3
A denyAccessUnlessGranted() 0 11 3
A parseConstraintViolations() 0 14 2
A addPaginationHeaders() 0 11 1
A getNormalizer() 0 4 1
A getValidator() 0 4 1
A getRequestParser() 0 4 1
A getRequestStack() 0 4 1
A getMetadataFactory() 0 4 1
A getPropertyAccessor() 0 4 1
A getAuthorizationChecker() 0 4 1
A setAuthorizationChecker() 0 4 1
listEntities() 0 1 ?
fetchEntity() 0 1 ?
createEntity() 0 1 ?
updateEntity() 0 1 ?
removeEntity() 0 1 ?
listSubresource() 0 1 ?
createAssociation() 0 1 ?
addAssociation() 0 1 ?
removeAssociation() 0 1 ?

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Dontdrinkandroot\RestBundle\Controller;
4
5
use Doctrine\ORM\Tools\Pagination\Paginator;
6
use Dontdrinkandroot\RestBundle\Metadata\Annotation\Method;
7
use Dontdrinkandroot\RestBundle\Metadata\Annotation\Right;
8
use Dontdrinkandroot\RestBundle\Metadata\ClassMetadata;
9
use Dontdrinkandroot\RestBundle\Metadata\PropertyMetadata;
10
use Dontdrinkandroot\RestBundle\Service\Normalizer;
11
use Dontdrinkandroot\RestBundle\Service\RestRequestParserInterface;
12
use Metadata\MetadataFactoryInterface;
13
use Symfony\Component\HttpFoundation\JsonResponse;
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\HttpFoundation\RequestStack;
16
use Symfony\Component\HttpFoundation\Response;
17
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
18
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
19
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
20
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
21
use Symfony\Component\Validator\ConstraintViolationInterface;
22
use Symfony\Component\Validator\ConstraintViolationListInterface;
23
use Symfony\Component\Validator\Validator\ValidatorInterface;
24
25
/**
26
 * @author Philip Washington Sorst <[email protected]>
27
 */
28
abstract class AbstractRestResourceController implements RestResourceControllerInterface
29
{
30
    /**
31
     * @var Normalizer
32
     */
33
    private $normalizer;
34
35
    /**
36
     * @var ValidatorInterface
37
     */
38
    private $validator;
39
40
    /**
41
     * @var RestRequestParserInterface
42
     */
43
    private $requestParser;
44
45
    /**
46
     * @var RequestStack
47
     */
48
    private $requestStack;
49
50
    /**
51
     * @var MetadataFactoryInterface
52
     */
53
    private $metadataFactory;
54
55
    /**
56
     * @var PropertyAccessorInterface
57
     */
58
    private $propertyAccessor;
59
60
    /**
61
     * @var AuthorizationCheckerInterface
62
     */
63
    private $authorizationChecker;
64
65 80
    public function __construct(
66
        RestRequestParserInterface $requestParser,
67
        Normalizer $normalizer,
68
        ValidatorInterface $validator,
69
        RequestStack $requestStack,
70
        MetadataFactoryInterface $metadataFactory,
71
        PropertyAccessorInterface $propertyAccessor
72
    ) {
73 80
        $this->requestParser = $requestParser;
74 80
        $this->normalizer = $normalizer;
75 80
        $this->validator = $validator;
76 80
        $this->requestStack = $requestStack;
77 80
        $this->metadataFactory = $metadataFactory;
78 80
        $this->propertyAccessor = $propertyAccessor;
79 80
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84 10
    public function listAction(Request $request)
85
    {
86 10
        $page = $request->query->get('page', 1);
87 10
        $perPage = $request->query->get('perPage', 50);
88
89 10
        $this->assertMethodGranted(Method::LIST);
90
91 6
        $listResult = $this->listEntities($page, $perPage);
92
93 6
        $response = new JsonResponse();
94
95 6
        if ($listResult instanceof Paginator) {
96 6
            $entities = iterator_to_array($listResult->getIterator());
97 6
            $total = $listResult->count();
98 6
            $this->addPaginationHeaders($response, $page, $perPage, $total);
99
        } else {
100
            $entities = $listResult;
101
        }
102
103 6
        $content = $this->getNormalizer()->normalize($entities, $this->parseIncludes($request));
104
105 6
        $response->setData($content);
106
107 6
        return $response;
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 14
    public function postAction(Request $request)
114
    {
115 14
        $this->assertMethodGranted(Method::POST);
116
117 12
        $entity = $this->getRequestParser()->parseEntity($request, $this->getEntityClass());
118 12
        $entity = $this->postProcessPostedEntity($entity);
119
120 12
        $errors = $this->getValidator()->validate($entity);
121 12
        if ($errors->count() > 0) {
122 2
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
123
        }
124
125 10
        $entity = $this->createEntity($entity);
126
127 10
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
128
129 10
        return new JsonResponse($content, Response::HTTP_CREATED);
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 32
    public function getAction(Request $request, $id)
136
    {
137 32
        $entity = $this->fetchEntity($id);
138 30
        $this->assertMethodGranted(Method::GET, $entity);
139
140 28
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
141
142 28
        return new JsonResponse($content);
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148 12
    public function putAction(Request $request, $id)
149
    {
150 12
        $entity = $this->fetchEntity($id);
151 12
        $this->assertMethodGranted(Method::PUT, $entity);
152 10
        $entity = $this->getRequestParser()->parseEntity($request, $this->getEntityClass(), $entity);
153 10
        $entity = $this->postProcessPuttedEntity($entity);
154
155 10
        $errors = $this->getValidator()->validate($entity);
156 10
        if ($errors->count() > 0) {
157
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
158
        }
159
160 10
        $entity = $this->updateEntity($entity);
161
162 10
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
163
164 10
        return new JsonResponse($content);
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170 4
    public function deleteAction(Request $request, $id)
171
    {
172 4
        $entity = $this->fetchEntity($id);
173 4
        $this->assertMethodGranted(Method::DELETE, $entity);
174 2
        $this->removeEntity($entity);
175
176 2
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182 6
    public function listSubresourceAction(Request $request, $id, string $subresource)
183
    {
184 6
        $page = $request->query->get('page', 1);
185 6
        $perPage = $request->query->get('perPage', 50);
186
187 6
        $entity = $this->fetchEntity($id);
188 6
        $this->assertSubResourceMethodGranted(Method::LIST, $entity, $subresource);
189
190 6
        $listResult = $this->listSubresource($entity, $subresource, $page, $perPage);
191
192 6
        $response = new JsonResponse();
193
194 6
        if ($listResult instanceof Paginator) {
195 6
            $entities = iterator_to_array($listResult->getIterator());
196 6
            $total = $listResult->count();
197 6
            $this->addPaginationHeaders($response, $page, $perPage, $total);
198
        } else {
199
            $entities = $listResult;
200
        }
201
202 6
        $content = $this->getNormalizer()->normalize($entities, $this->parseIncludes($request));
203
204 6
        $response->setData($content);
205
206 6
        return $response;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212 4
    public function postSubresourceAction(Request $request, $id, string $subresource)
213
    {
214 4
        $parent = $this->fetchEntity($id);
215 4
        $this->assertSubResourceMethodGranted(Method::POST, $parent, $subresource);
216
217 2
        $restRequestParser = $this->getRequestParser();
218 2
        $entity = $restRequestParser->parseEntity($request, $this->getSubResourceEntityClass($subresource));
219
220 2
        $entity = $this->postProcessSubResourcePostedEntity($parent, $subresource, $entity);
221
222 2
        $errors = $this->getValidator()->validate($entity);
223
224 2
        if ($errors->count() > 0) {
225
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
226
        }
227
228 2
        $entity = $this->createAssociation($parent, $subresource, $entity);
229
230 2
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
231
232 2
        return new JsonResponse($content, Response::HTTP_CREATED);
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238 12
    public function putSubresourceAction(Request $request, $id, string $subresource, $subId)
239
    {
240 12
        $parent = $this->fetchEntity($id);
241 12
        $this->assertSubResourceMethodGranted(Method::PUT, $parent, $subresource);
242 12
        $this->addAssociation($parent, $subresource, $subId);
243
244 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250 12
    public function deleteSubresourceAction(Request $request, $id, string $subresource, $subId = null)
251
    {
252 12
        $parent = $this->fetchEntity($id);
253 12
        $this->assertSubResourceMethodGranted(Method::DELETE, $parent, $subresource);
254 12
        $this->removeAssociation($parent, $subresource, $subId);
255
256 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
257
    }
258
259
    /**
260
     * @param object $entity
261
     *
262
     * @return object
263
     */
264 12
    protected function postProcessPostedEntity($entity)
265
    {
266 12
        return $entity;
267
    }
268
269
    /**
270
     * @param object $entity
271
     *
272
     * @return object
273
     */
274 10
    protected function postProcessPuttedEntity($entity)
275
    {
276 10
        return $entity;
277
    }
278
279
    /**
280
     * @param object $parent
281
     * @param string $subresource
282
     * @param object $entity
283
     *
284
     * @return object
285
     */
286 2
    protected function postProcessSubResourcePostedEntity($parent, $subresource, $entity)
0 ignored issues
show
Unused Code introduced by
The parameter $parent is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $subresource is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
287
    {
288 2
        return $entity;
289
    }
290
291 80
    protected function getEntityClass()
292
    {
293 80
        return $this->getCurrentRequest()->attributes->get('_entityClass');
294
    }
295
296 2
    protected function getSubResourceEntityClass($subresource)
297
    {
298
        /** @var PropertyMetadata $propertyMetadata */
299 2
        $propertyMetadata = $this->getClassMetadata()->propertyMetadata[$subresource];
300
301 2
        return $propertyMetadata->getType();
302
    }
303
304 80
    protected function getCurrentRequest()
305
    {
306 80
        return $this->getRequestStack()->getCurrentRequest();
307
    }
308
309 70
    protected function assertMethodGranted(string $methodName, $entity = null)
310
    {
311 70
        $method = $this->getClassMetadata()->getMethod($methodName);
312 70
        if ($method !== null && null !== $right = $method->right) {
313 36
            $this->assertRightGranted($right, $entity);
314
        }
315 58
    }
316
317
    /**
318
     * @param string $methodName
319
     * @param object $entity
320
     * @param string $subresource
321
     */
322 30
    protected function assertSubResourceMethodGranted($methodName, $entity, string $subresource): void
323
    {
324 30
        $classMetadata = $this->getClassMetadata();
325
        /** @var PropertyMetadata $propertyMetadata */
326 30
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
327 30
        $method = $propertyMetadata->getMethod($methodName);
328 30
        if (null !== $right = $method->right) {
329 14
            $this->assertRightGranted($right, $entity);
330
        }
331 28
    }
332
333
    /**
334
     * @return ClassMetadata
335
     */
336 80
    protected function getClassMetadata()
337
    {
338 80
        $metaDataFactory = $this->getMetadataFactory();
339
        /** @var ClassMetadata $classMetaData */
340 80
        $classMetaData = $metaDataFactory->getMetadataForClass($this->getEntityClass());
341
342 80
        return $classMetaData;
343
    }
344
345
    protected function resolveSubject($entity, $propertyPath)
346
    {
347
        if ('this' === $propertyPath) {
348
            return $entity;
349
        }
350
        $propertyAccessor = $this->getPropertyAccessor();
351
352
        return $propertyAccessor->getValue($entity, $propertyPath);
353
    }
354
355
    /**
356
     * @param Right  $right
357
     * @param object $entity
358
     */
359 46
    protected function assertRightGranted(Right $right, $entity = null)
360
    {
361 46
        $propertyPath = $right->propertyPath;
362 46
        if (null === $propertyPath || null == $entity) {
363 46
            $this->denyAccessUnlessGranted($right->attributes);
364
        } else {
365
            $subject = $this->resolveSubject($entity, $propertyPath);
366
            $this->denyAccessUnlessGranted($right->attributes, $subject);
367
        }
368 32
    }
369
370 62
    protected function parseIncludes(Request $request)
371
    {
372 62
        $defaultIncludes = $request->attributes->get('_defaultincludes');
373 62
        if (null == $defaultIncludes) {
374 30
            $defaultIncludes = [];
375
        }
376
377 62
        $includeString = $request->query->get('include');
378 62
        if (empty($includeString)) {
379 48
            $includes = [];
380
        } else {
381 16
            $includes = explode(',', $includeString);
382
        }
383
384 62
        return array_merge($defaultIncludes, $includes);
385
    }
386
387 46
    protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.')
388
    {
389 46
        $authorizationChecker = $this->getAuthorizationChecker();
390 46
        if (null === $authorizationChecker) {
391
            throw new AccessDeniedException('No authorization checker configured');
392
        }
393
394 46
        if (!$authorizationChecker->isGranted($attributes, $object)) {
395 14
            throw new AccessDeniedException($message);
396
        }
397 32
    }
398
399 2
    protected function parseConstraintViolations(ConstraintViolationListInterface $errors)
400
    {
401 2
        $data = [];
402
        /** @var ConstraintViolationInterface $error */
403 2
        foreach ($errors as $error) {
404 2
            $data[] = [
405 2
                'propertyPath' => $error->getPropertyPath(),
406 2
                'message'      => $error->getMessage(),
407 2
                'value'        => $error->getInvalidValue()
408
            ];
409
        }
410
411 2
        return $data;
412
    }
413
414 12
    protected function addPaginationHeaders(Response $response, int $page, int $perPage, int $total)
415
    {
416 12
        $response->headers->add(
417
            [
418 12
                'x-pagination-current-page' => $page,
419 12
                'x-pagination-per-page'     => $perPage,
420 12
                'x-pagination-total'        => $total,
421 12
                'x-pagination-total-pages'  => (int)(($total - 1) / $perPage + 1)
422
            ]
423
        );
424 12
    }
425
426
    /**
427
     * {@inheritdoc}
428
     */
429 62
    protected function getNormalizer()
430
    {
431 62
        return $this->normalizer;
432
    }
433
434
    /**
435
     * {@inheritdoc}
436
     */
437 24
    protected function getValidator()
438
    {
439 24
        return $this->validator;
440
    }
441
442
    /**
443
     * {@inheritdoc}
444
     */
445 24
    protected function getRequestParser()
446
    {
447 24
        return $this->requestParser;
448
    }
449
450
    /**
451
     * {@inheritdoc}
452
     */
453 80
    protected function getRequestStack()
454
    {
455 80
        return $this->requestStack;
456
    }
457
458
    /**
459
     * {@inheritdoc}
460
     */
461 80
    protected function getMetadataFactory()
462
    {
463 80
        return $this->metadataFactory;
464
    }
465
466
    /**
467
     * {@inheritdoc}
468
     */
469
    protected function getPropertyAccessor()
470
    {
471
        return $this->propertyAccessor;
472
    }
473
474
    /**
475
     * {@inheritdoc}
476
     */
477 46
    protected function getAuthorizationChecker(): ?AuthorizationCheckerInterface
478
    {
479 46
        return $this->authorizationChecker;
480
    }
481
482
    /**
483
     * @param AuthorizationCheckerInterface $authorizationChecker
484
     */
485 76
    public function setAuthorizationChecker(AuthorizationCheckerInterface $authorizationChecker): void
486
    {
487 76
        $this->authorizationChecker = $authorizationChecker;
488 76
    }
489
490
    /**
491
     * @param int $page
492
     * @param int $perPage
493
     *
494
     * @return Paginator|array
495
     */
496
    abstract protected function listEntities(int $page = 1, int $perPage = 50);
497
498
    /**
499
     * @param int|string $id
500
     *
501
     * @return object
502
     *
503
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
504
     */
505
    abstract protected function fetchEntity($id);
506
507
    /**
508
     * @param object $entity
509
     *
510
     * @return object
511
     */
512
    abstract protected function createEntity($entity);
513
514
    /**
515
     * @param object $entity
516
     *
517
     * @return object
518
     *
519
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
520
     */
521
    abstract protected function updateEntity($entity);
522
523
    /**
524
     * @param $entity
525
     *
526
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
527
     */
528
    abstract protected function removeEntity($entity);
529
530
    /**
531
     * @param object $entity
532
     * @param string $subresource
533
     * @param int    $page
534
     * @param int    $perPage
535
     *
536
     * @return Paginator|array
537
     */
538
    abstract protected function listSubresource($entity, string $subresource, int $page = 1, int $perPage = 50);
539
540
    /**
541
     * @param object $parent
542
     * @param string $subresource
543
     *
544
     * @return object
545
     */
546
    abstract protected function createAssociation($parent, string $subresource, $entity);
547
548
    /**
549
     * @param object     $parent
550
     * @param string     $subresource
551
     * @param int|string $subId
552
     *
553
     * @return object
554
     */
555
    abstract protected function addAssociation($parent, string $subresource, $subId);
556
557
    /**
558
     * @param object          $parent
559
     * @param string          $subresource
560
     * @param int|string|null $subId
561
     *
562
     * @return mixed
563
     */
564
    abstract protected function removeAssociation($parent, string $subresource, $subId = null);
565
}
566