addPaginationHeaders()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 4
crap 1
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\Metadata\RestMetadataFactory;
11
use Dontdrinkandroot\RestBundle\Serializer\RestDenormalizer;
12
use Dontdrinkandroot\RestBundle\Serializer\RestNormalizer;
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\Serializer\SerializerInterface;
22
use Symfony\Component\Validator\ConstraintViolationInterface;
23
use Symfony\Component\Validator\ConstraintViolationListInterface;
24
use Symfony\Component\Validator\Validator\ValidatorInterface;
25
26
/**
27
 * @author Philip Washington Sorst <[email protected]>
28
 */
29
abstract class AbstractRestResourceController implements RestResourceControllerInterface
30
{
31
    /**
32
     * @var ValidatorInterface
33
     */
34
    private $validator;
35
36
    /**
37
     * @var RequestStack
38
     */
39
    private $requestStack;
40
41
    /**
42
     * @var RestMetadataFactory
43
     */
44
    private $metadataFactory;
45
46
    /**
47
     * @var PropertyAccessorInterface
48
     */
49
    private $propertyAccessor;
50
51
    /**
52
     * @var AuthorizationCheckerInterface
53
     */
54
    private $authorizationChecker;
55
56
    /**
57
     * @var SerializerInterface
58
     */
59
    private $serializer;
60
61 82
    public function __construct(
62
        ValidatorInterface $validator,
63
        RequestStack $requestStack,
64
        RestMetadataFactory $metadataFactory,
65
        PropertyAccessorInterface $propertyAccessor,
66
        SerializerInterface $serializer
67
    ) {
68 82
        $this->validator = $validator;
69 82
        $this->requestStack = $requestStack;
70 82
        $this->metadataFactory = $metadataFactory;
71 82
        $this->propertyAccessor = $propertyAccessor;
72 82
        $this->serializer = $serializer;
73 82
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78 10
    public function listAction(Request $request)
79
    {
80 10
        $page = $request->query->get('page', 1);
81 10
        $perPage = $request->query->get('perPage', 50);
82
83 10
        $this->assertMethodGranted(Method::LIST);
84
85 6
        $listResult = $this->listEntities($page, $perPage);
86
87 6
        $response = new JsonResponse();
88
89 6
        if ($listResult instanceof Paginator) {
90 6
            $entities = iterator_to_array($listResult->getIterator());
91 6
            $total = $listResult->count();
92 6
            $this->addPaginationHeaders($response, $page, $perPage, $total);
93
        } else {
94
            $entities = $listResult;
95
        }
96
97 6
        $json = $this->getSerializer()->serialize(
98 6
            $entities,
99 6
            'json',
100
            [
101 6
                RestNormalizer::DDR_REST_INCLUDES => $this->parseIncludes($request),
102 6
                RestNormalizer::DDR_REST_DEPTH    => 0,
103 6
                RestNormalizer::DDR_REST_PATH     => ''
104
            ]
105
        );
106 6
        $response->setJson($json);
107
108 6
        return $response;
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114 14
    public function postAction(Request $request)
115
    {
116 14
        $this->assertMethodGranted(Method::POST);
117
118 12
        $entity = $this->serializer->deserialize(
119 12
            $request->getContent(),
120 12
            $this->getEntityClass(),
121 12
            'json',
122 12
            [RestDenormalizer::DDR_REST_METHOD => Method::POST]
123
        );
124 12
        $entity = $this->postProcessPostedEntity($entity);
125
126 12
        $errors = $this->getValidator()->validate($entity);
127 12
        if ($errors->count() > 0) {
128 2
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
129
        }
130
131 10
        $entity = $this->createEntity($entity);
132
133 10
        $response = new JsonResponse(null, Response::HTTP_CREATED);
134
135 10
        $json = $this->getSerializer()->serialize(
136 10
            $entity,
137 10
            'json',
138
            [
139 10
                RestNormalizer::DDR_REST_INCLUDES => $this->parseIncludes($request),
140 10
                RestNormalizer::DDR_REST_DEPTH    => 0,
141 10
                RestNormalizer::DDR_REST_PATH     => ''
142
            ]
143
        );
144 10
        $response->setJson($json);
145
146 10
        return $response;
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152 32
    public function getAction(Request $request, $id)
153
    {
154 32
        $entity = $this->fetchEntity($id);
155 30
        $this->assertMethodGranted(Method::GET, $entity);
156
157 28
        $response = new JsonResponse();
158 28
        $json = $this->getSerializer()->serialize(
159 28
            $entity,
160 28
            'json',
161
            [
162 28
                RestNormalizer::DDR_REST_INCLUDES => $this->parseIncludes($request),
163 28
                RestNormalizer::DDR_REST_DEPTH    => 0,
164 28
                RestNormalizer::DDR_REST_PATH     => ''
165
            ]
166
        );
167 28
        $response->setJson($json);
168
169 28
        return $response;
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175 12
    public function putAction(Request $request, $id)
176
    {
177 12
        $entity = $this->fetchEntity($id);
178 12
        $this->assertMethodGranted(Method::PUT, $entity);
179
180 10
        $entity = $this->serializer->deserialize(
181 10
            $request->getContent(),
182 10
            $this->getEntityClass(),
183 10
            'json',
184 10
            [RestDenormalizer::DDR_REST_METHOD => Method::PUT, RestDenormalizer::DDR_REST_ENTITY => $entity]
185
        );
186 10
        $entity = $this->postProcessPuttedEntity($entity);
187
188 10
        $errors = $this->getValidator()->validate($entity);
189 10
        if ($errors->count() > 0) {
190
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
191
        }
192
193 10
        $entity = $this->updateEntity($entity);
194
195 10
        $response = new JsonResponse();
196
197 10
        $json = $this->getSerializer()->serialize(
198 10
            $entity,
199 10
            'json',
200
            [
201 10
                RestNormalizer::DDR_REST_INCLUDES => $this->parseIncludes($request),
202 10
                RestNormalizer::DDR_REST_DEPTH    => 0,
203 10
                RestNormalizer::DDR_REST_PATH     => ''
204
            ]
205
        );
206 10
        $response->setJson($json);
207
208 10
        return $response;
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 4
    public function deleteAction(Request $request, $id)
215
    {
216 4
        $entity = $this->fetchEntity($id);
217 4
        $this->assertMethodGranted(Method::DELETE, $entity);
218 2
        $this->removeEntity($entity);
219
220 2
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     */
226 6
    public function listSubresourceAction(Request $request, $id, string $subresource)
227
    {
228 6
        $page = $request->query->get('page', 1);
229 6
        $perPage = $request->query->get('perPage', 50);
230
231 6
        $entity = $this->fetchEntity($id);
232 6
        $this->assertSubResourceMethodGranted(Method::LIST, $entity, $subresource);
233
234 6
        $listResult = $this->listSubresource($entity, $subresource, $page, $perPage);
235
236 6
        $response = new JsonResponse();
237
238 6
        if ($listResult instanceof Paginator) {
239 6
            $entities = iterator_to_array($listResult->getIterator());
240 6
            $total = $listResult->count();
241 6
            $this->addPaginationHeaders($response, $page, $perPage, $total);
242
        } else {
243
            $entities = $listResult;
244
        }
245
246 6
        $json = $this->getSerializer()->serialize(
247 6
            $entities,
248 6
            'json',
249
            [
250 6
                RestNormalizer::DDR_REST_INCLUDES => $this->parseIncludes($request),
251 6
                RestNormalizer::DDR_REST_DEPTH    => 0,
252 6
                RestNormalizer::DDR_REST_PATH     => ''
253
            ]
254
        );
255 6
        $response->setJson($json);
256
257 6
        return $response;
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263 6
    public function postSubresourceAction(Request $request, $id, string $subresource)
264
    {
265 6
        $parent = $this->fetchEntity($id);
266 6
        $this->assertSubResourceMethodGranted(Method::POST, $parent, $subresource);
267
268 4
        $entity = $this->getSubresourcePostedEntity($request, $subresource);
269
270 4
        $entity = $this->buildAssociation($parent, $subresource, $entity);
271 4
        $entity = $this->postProcessSubResourcePostedEntity($parent, $subresource, $entity);
272
273 4
        $errors = $this->getValidator()->validate($entity);
274
275 4
        if ($errors->count() > 0) {
276
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
277
        }
278
279 4
        $entity = $this->createAssociation($entity);
280
281 4
        $response = new JsonResponse(null, Response::HTTP_CREATED);
282 4
        $json = $this->getSerializer()->serialize(
283 4
            $entity,
284 4
            'json',
285
            [
286 4
                RestNormalizer::DDR_REST_INCLUDES => $this->parseIncludes($request),
287 4
                RestNormalizer::DDR_REST_DEPTH    => 0,
288 4
                RestNormalizer::DDR_REST_PATH     => ''
289
            ]
290
        );
291 4
        $response->setJson($json);
292
293 4
        return $response;
294
    }
295
296
    /**
297
     * {@inheritdoc}
298
     */
299 12
    public function putSubresourceAction(Request $request, $id, string $subresource, $subId)
300
    {
301 12
        $parent = $this->fetchEntity($id);
302 12
        $this->assertSubResourceMethodGranted(Method::PUT, $parent, $subresource);
303 12
        $this->addAssociation($parent, $subresource, $subId);
304
305 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311 12
    public function deleteSubresourceAction(Request $request, $id, string $subresource, $subId = null)
312
    {
313 12
        $parent = $this->fetchEntity($id);
314 12
        $this->assertSubResourceMethodGranted(Method::DELETE, $parent, $subresource);
315 12
        $this->removeAssociation($parent, $subresource, $subId);
316
317 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
318
    }
319
320
    /**
321
     * @param object $entity
322
     *
323
     * @return object
324
     */
325 12
    protected function postProcessPostedEntity($entity)
326
    {
327 12
        return $entity;
328
    }
329
330
    /**
331
     * @param object $entity
332
     *
333
     * @return object
334
     */
335 10
    protected function postProcessPuttedEntity($entity)
336
    {
337 10
        return $entity;
338
    }
339
340
    /**
341
     * @param object $parent
342
     * @param string $subresource
343
     * @param object $entity
344
     *
345
     * @return object
346
     */
347 4
    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...
348
    {
349 4
        return $entity;
350
    }
351
352 82
    protected function getEntityClass()
353
    {
354 82
        return $this->getCurrentRequest()->attributes->get('_entityClass');
355
    }
356
357 4
    protected function getSubResourceEntityClass($subresource)
358
    {
359
        /** @var PropertyMetadata $propertyMetadata */
360 4
        $propertyMetadata = $this->getClassMetadata()->propertyMetadata[$subresource];
361
362 4
        return $propertyMetadata->getType();
363
    }
364
365 82
    protected function getCurrentRequest()
366
    {
367 82
        return $this->getRequestStack()->getCurrentRequest();
368
    }
369
370 70
    protected function assertMethodGranted(string $methodName, $entity = null)
371
    {
372 70
        $method = $this->getClassMetadata()->getMethod($methodName);
373 70
        if ($method !== null && null !== $right = $method->right) {
374 36
            $this->assertRightGranted($right, $entity);
375
        }
376 58
    }
377
378
    /**
379
     * @param string $methodName
380
     * @param object $entity
381
     * @param string $subresource
382
     */
383 32
    protected function assertSubResourceMethodGranted($methodName, $entity, string $subresource): void
384
    {
385 32
        $classMetadata = $this->getClassMetadata();
386
        /** @var PropertyMetadata $propertyMetadata */
387 32
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
388 32
        $method = $propertyMetadata->getMethod($methodName);
389 32
        if (null !== $right = $method->right) {
390 16
            $this->assertRightGranted($right, $entity);
391
        }
392 30
    }
393
394
    /**
395
     * @return ClassMetadata
396
     */
397 82
    protected function getClassMetadata()
398
    {
399 82
        $metaDataFactory = $this->getMetadataFactory();
400
        /** @var ClassMetadata $classMetaData */
401 82
        $classMetaData = $metaDataFactory->getMetadataForClass($this->getEntityClass());
402
403 82
        return $classMetaData;
404
    }
405
406
    protected function resolveSubject($entity, $propertyPath)
407
    {
408
        if ('this' === $propertyPath) {
409
            return $entity;
410
        }
411
        $propertyAccessor = $this->getPropertyAccessor();
412
413
        return $propertyAccessor->getValue($entity, $propertyPath);
414
    }
415
416
    /**
417
     * @param Right  $right
418
     * @param object $entity
419
     */
420 48
    protected function assertRightGranted(Right $right, $entity = null)
421
    {
422 48
        $propertyPath = $right->propertyPath;
423 48
        if (null === $propertyPath || null == $entity) {
424 48
            $this->denyAccessUnlessGranted($right->attributes);
425
        } else {
426
            $subject = $this->resolveSubject($entity, $propertyPath);
427
            $this->denyAccessUnlessGranted($right->attributes, $subject);
428
        }
429 34
    }
430
431 64
    protected function parseIncludes(Request $request)
432
    {
433 64
        $defaultIncludes = $request->attributes->get('_defaultincludes');
434 64
        if (null == $defaultIncludes) {
435 30
            $defaultIncludes = [];
436
        }
437
438 64
        $includeString = $request->query->get('include');
439 64
        if (empty($includeString)) {
440 50
            $includes = [];
441
        } else {
442 16
            $includes = explode(',', $includeString);
443
        }
444
445 64
        return array_merge($defaultIncludes, $includes);
446
    }
447
448 48
    protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.')
449
    {
450 48
        $authorizationChecker = $this->getAuthorizationChecker();
451 48
        if (null === $authorizationChecker) {
452
            throw new AccessDeniedException('No authorization checker configured');
453
        }
454
455 48
        if (!$authorizationChecker->isGranted($attributes, $object)) {
456 14
            throw new AccessDeniedException($message);
457
        }
458 34
    }
459
460 2
    protected function parseConstraintViolations(ConstraintViolationListInterface $errors)
461
    {
462 2
        $data = [];
463
        /** @var ConstraintViolationInterface $error */
464 2
        foreach ($errors as $error) {
465 2
            $data[] = [
466 2
                'propertyPath' => $error->getPropertyPath(),
467 2
                'message'      => $error->getMessage(),
468 2
                'value'        => $error->getInvalidValue()
469
            ];
470
        }
471
472 2
        return $data;
473
    }
474
475 12
    protected function addPaginationHeaders(Response $response, int $page, int $perPage, int $total)
476
    {
477 12
        $response->headers->add(
478
            [
479 12
                'x-pagination-current-page' => $page,
480 12
                'x-pagination-per-page'     => $perPage,
481 12
                'x-pagination-total'        => $total,
482 12
                'x-pagination-total-pages'  => (int)(($total - 1) / $perPage + 1)
483
            ]
484
        );
485 12
    }
486
487 26
    protected function getValidator()
488
    {
489 26
        return $this->validator;
490
    }
491
492 82
    protected function getRequestStack()
493
    {
494 82
        return $this->requestStack;
495
    }
496
497 82
    protected function getMetadataFactory()
498
    {
499 82
        return $this->metadataFactory;
500
    }
501
502
    protected function getPropertyAccessor()
503
    {
504
        return $this->propertyAccessor;
505
    }
506
507 48
    protected function getAuthorizationChecker(): ?AuthorizationCheckerInterface
508
    {
509 48
        return $this->authorizationChecker;
510
    }
511
512 64
    protected function getSerializer(): SerializerInterface
513
    {
514 64
        return $this->serializer;
515
    }
516
517
    /**
518
     * @param AuthorizationCheckerInterface $authorizationChecker
519
     */
520 78
    public function setAuthorizationChecker(AuthorizationCheckerInterface $authorizationChecker): void
521
    {
522 78
        $this->authorizationChecker = $authorizationChecker;
523 78
    }
524
525
    /**
526
     * @param int $page
527
     * @param int $perPage
528
     *
529
     * @return Paginator|array
530
     */
531
    abstract protected function listEntities(int $page = 1, int $perPage = 50);
532
533
    /**
534
     * @param int|string $id
535
     *
536
     * @return object
537
     *
538
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
539
     */
540
    abstract protected function fetchEntity($id);
541
542
    /**
543
     * @param object $entity
544
     *
545
     * @return object
546
     */
547
    abstract protected function createEntity($entity);
548
549
    /**
550
     * @param object $entity
551
     *
552
     * @return object
553
     *
554
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
555
     */
556
    abstract protected function updateEntity($entity);
557
558
    /**
559
     * @param $entity
560
     *
561
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
562
     */
563
    abstract protected function removeEntity($entity);
564
565
    /**
566
     * @param object $entity
567
     * @param string $subresource
568
     * @param int    $page
569
     * @param int    $perPage
570
     *
571
     * @return Paginator|array
572
     */
573
    abstract protected function listSubresource($entity, string $subresource, int $page = 1, int $perPage = 50);
574
575
    /**
576
     * @param object $parent
577
     * @param string $subresource
578
     *
579
     * @return object
580
     */
581
    abstract protected function buildAssociation($parent, string $subresource, $entity);
582
583
    /**
584
     * @param object $associatedEntity
585
     *
586
     * @return object
587
     */
588
    abstract protected function createAssociation($associatedEntity);
589
590
    /**
591
     * @param object     $parent
592
     * @param string     $subresource
593
     * @param int|string $subId
594
     *
595
     * @return object
596
     */
597
    abstract protected function addAssociation($parent, string $subresource, $subId);
598
599
    /**
600
     * @param object          $parent
601
     * @param string          $subresource
602
     * @param int|string|null $subId
603
     *
604
     * @return mixed
605
     */
606
    abstract protected function removeAssociation($parent, string $subresource, $subId = null);
607
608
    /**
609
     * @param Request $request
610
     * @param string  $subresource
611
     *
612
     * @return mixed
613
     */
614 4
    protected function getSubresourcePostedEntity(Request $request, string $subresource)
615
    {
616 4
        $content = null;
0 ignored issues
show
Unused Code introduced by
$content is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
617 4
        $requestParameters = $request->request->all();
618 4
        if (!empty($requestParameters)) {
619 2
            $content = json_encode($requestParameters);
620
        } else {
621 2
            $content = $request->getContent();
622
        }
623
624 4
        if (null === $content || '' === $content) {
625
            $content = '{}';
626
        }
627
628 4
        $entity = $this->serializer->deserialize(
629 4
            $content,
630 4
            $this->getSubResourceEntityClass($subresource),
631 4
            'json',
632 4
            [RestDenormalizer::DDR_REST_METHOD => Method::POST]
633
        );
634
635 4
        return $entity;
636
    }
637
}
638