Completed
Push — master ( ab2585...334712 )
by Philip
10:52
created

denyAccessUnlessGranted()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

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