Completed
Push — master ( 658cec...20b116 )
by Philip
06:38
created

getSubResourceEntityClass()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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 80
    public function __construct(
62
        ValidatorInterface $validator,
63
        RequestStack $requestStack,
64
        RestMetadataFactory $metadataFactory,
65
        PropertyAccessorInterface $propertyAccessor,
66
        SerializerInterface $serializer
67
    ) {
68 80
        $this->validator = $validator;
69 80
        $this->requestStack = $requestStack;
70 80
        $this->metadataFactory = $metadataFactory;
71 80
        $this->propertyAccessor = $propertyAccessor;
72 80
        $this->serializer = $serializer;
73 80
    }
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 4
    public function postSubresourceAction(Request $request, $id, string $subresource)
264
    {
265 4
        $parent = $this->fetchEntity($id);
266 4
        $this->assertSubResourceMethodGranted(Method::POST, $parent, $subresource);
267
268 2
        $entity = $this->serializer->deserialize(
269 2
            $request->getContent(),
270 2
            $this->getSubResourceEntityClass($subresource),
271 2
            'json',
272 2
            [RestDenormalizer::DDR_REST_METHOD => Method::POST]
273
        );
274
275 2
        $entity = $this->buildAssociation($parent, $subresource, $entity);
276 2
        $entity = $this->postProcessSubResourcePostedEntity($parent, $subresource, $entity);
277
278 2
        $errors = $this->getValidator()->validate($entity);
279
280 2
        if ($errors->count() > 0) {
281
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
282
        }
283
284 2
        $entity = $this->createAssociation($entity);
285
286 2
        $response = new JsonResponse(null, Response::HTTP_CREATED);
287 2
        $json = $this->getSerializer()->serialize(
288 2
            $entity,
289 2
            'json',
290
            [
291 2
                RestNormalizer::DDR_REST_INCLUDES => $this->parseIncludes($request),
292 2
                RestNormalizer::DDR_REST_DEPTH    => 0,
293 2
                RestNormalizer::DDR_REST_PATH     => ''
294
            ]
295
        );
296 2
        $response->setJson($json);
297
298 2
        return $response;
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304 12
    public function putSubresourceAction(Request $request, $id, string $subresource, $subId)
305
    {
306 12
        $parent = $this->fetchEntity($id);
307 12
        $this->assertSubResourceMethodGranted(Method::PUT, $parent, $subresource);
308 12
        $this->addAssociation($parent, $subresource, $subId);
309
310 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
311
    }
312
313
    /**
314
     * {@inheritdoc}
315
     */
316 12
    public function deleteSubresourceAction(Request $request, $id, string $subresource, $subId = null)
317
    {
318 12
        $parent = $this->fetchEntity($id);
319 12
        $this->assertSubResourceMethodGranted(Method::DELETE, $parent, $subresource);
320 12
        $this->removeAssociation($parent, $subresource, $subId);
321
322 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
323
    }
324
325
    /**
326
     * @param object $entity
327
     *
328
     * @return object
329
     */
330 12
    protected function postProcessPostedEntity($entity)
331
    {
332 12
        return $entity;
333
    }
334
335
    /**
336
     * @param object $entity
337
     *
338
     * @return object
339
     */
340 10
    protected function postProcessPuttedEntity($entity)
341
    {
342 10
        return $entity;
343
    }
344
345
    /**
346
     * @param object $parent
347
     * @param string $subresource
348
     * @param object $entity
349
     *
350
     * @return object
351
     */
352 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...
353
    {
354 2
        return $entity;
355
    }
356
357 80
    protected function getEntityClass()
358
    {
359 80
        return $this->getCurrentRequest()->attributes->get('_entityClass');
360
    }
361
362 2
    protected function getSubResourceEntityClass($subresource)
363
    {
364
        /** @var PropertyMetadata $propertyMetadata */
365 2
        $propertyMetadata = $this->getClassMetadata()->propertyMetadata[$subresource];
366
367 2
        return $propertyMetadata->getType();
368
    }
369
370 80
    protected function getCurrentRequest()
371
    {
372 80
        return $this->getRequestStack()->getCurrentRequest();
373
    }
374
375 70
    protected function assertMethodGranted(string $methodName, $entity = null)
376
    {
377 70
        $method = $this->getClassMetadata()->getMethod($methodName);
378 70
        if ($method !== null && null !== $right = $method->right) {
379 36
            $this->assertRightGranted($right, $entity);
380
        }
381 58
    }
382
383
    /**
384
     * @param string $methodName
385
     * @param object $entity
386
     * @param string $subresource
387
     */
388 30
    protected function assertSubResourceMethodGranted($methodName, $entity, string $subresource): void
389
    {
390 30
        $classMetadata = $this->getClassMetadata();
391
        /** @var PropertyMetadata $propertyMetadata */
392 30
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
393 30
        $method = $propertyMetadata->getMethod($methodName);
394 30
        if (null !== $right = $method->right) {
395 14
            $this->assertRightGranted($right, $entity);
396
        }
397 28
    }
398
399
    /**
400
     * @return ClassMetadata
401
     */
402 80
    protected function getClassMetadata()
403
    {
404 80
        $metaDataFactory = $this->getMetadataFactory();
405
        /** @var ClassMetadata $classMetaData */
406 80
        $classMetaData = $metaDataFactory->getMetadataForClass($this->getEntityClass());
407
408 80
        return $classMetaData;
409
    }
410
411
    protected function resolveSubject($entity, $propertyPath)
412
    {
413
        if ('this' === $propertyPath) {
414
            return $entity;
415
        }
416
        $propertyAccessor = $this->getPropertyAccessor();
417
418
        return $propertyAccessor->getValue($entity, $propertyPath);
419
    }
420
421
    /**
422
     * @param Right  $right
423
     * @param object $entity
424
     */
425 46
    protected function assertRightGranted(Right $right, $entity = null)
426
    {
427 46
        $propertyPath = $right->propertyPath;
428 46
        if (null === $propertyPath || null == $entity) {
429 46
            $this->denyAccessUnlessGranted($right->attributes);
430
        } else {
431
            $subject = $this->resolveSubject($entity, $propertyPath);
432
            $this->denyAccessUnlessGranted($right->attributes, $subject);
433
        }
434 32
    }
435
436 62
    protected function parseIncludes(Request $request)
437
    {
438 62
        $defaultIncludes = $request->attributes->get('_defaultincludes');
439 62
        if (null == $defaultIncludes) {
440 30
            $defaultIncludes = [];
441
        }
442
443 62
        $includeString = $request->query->get('include');
444 62
        if (empty($includeString)) {
445 48
            $includes = [];
446
        } else {
447 16
            $includes = explode(',', $includeString);
448
        }
449
450 62
        return array_merge($defaultIncludes, $includes);
451
    }
452
453 46
    protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.')
454
    {
455 46
        $authorizationChecker = $this->getAuthorizationChecker();
456 46
        if (null === $authorizationChecker) {
457
            throw new AccessDeniedException('No authorization checker configured');
458
        }
459
460 46
        if (!$authorizationChecker->isGranted($attributes, $object)) {
461 14
            throw new AccessDeniedException($message);
462
        }
463 32
    }
464
465 2
    protected function parseConstraintViolations(ConstraintViolationListInterface $errors)
466
    {
467 2
        $data = [];
468
        /** @var ConstraintViolationInterface $error */
469 2
        foreach ($errors as $error) {
470 2
            $data[] = [
471 2
                'propertyPath' => $error->getPropertyPath(),
472 2
                'message'      => $error->getMessage(),
473 2
                'value'        => $error->getInvalidValue()
474
            ];
475
        }
476
477 2
        return $data;
478
    }
479
480 12
    protected function addPaginationHeaders(Response $response, int $page, int $perPage, int $total)
481
    {
482 12
        $response->headers->add(
483
            [
484 12
                'x-pagination-current-page' => $page,
485 12
                'x-pagination-per-page'     => $perPage,
486 12
                'x-pagination-total'        => $total,
487 12
                'x-pagination-total-pages'  => (int)(($total - 1) / $perPage + 1)
488
            ]
489
        );
490 12
    }
491
492 24
    protected function getValidator()
493
    {
494 24
        return $this->validator;
495
    }
496
497 80
    protected function getRequestStack()
498
    {
499 80
        return $this->requestStack;
500
    }
501
502 80
    protected function getMetadataFactory()
503
    {
504 80
        return $this->metadataFactory;
505
    }
506
507
    protected function getPropertyAccessor()
508
    {
509
        return $this->propertyAccessor;
510
    }
511
512 46
    protected function getAuthorizationChecker(): ?AuthorizationCheckerInterface
513
    {
514 46
        return $this->authorizationChecker;
515
    }
516
517 62
    protected function getSerializer(): SerializerInterface
518
    {
519 62
        return $this->serializer;
520
    }
521
522
    /**
523
     * @param AuthorizationCheckerInterface $authorizationChecker
524
     */
525 76
    public function setAuthorizationChecker(AuthorizationCheckerInterface $authorizationChecker): void
526
    {
527 76
        $this->authorizationChecker = $authorizationChecker;
528 76
    }
529
530
    /**
531
     * @param int $page
532
     * @param int $perPage
533
     *
534
     * @return Paginator|array
535
     */
536
    abstract protected function listEntities(int $page = 1, int $perPage = 50);
537
538
    /**
539
     * @param int|string $id
540
     *
541
     * @return object
542
     *
543
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
544
     */
545
    abstract protected function fetchEntity($id);
546
547
    /**
548
     * @param object $entity
549
     *
550
     * @return object
551
     */
552
    abstract protected function createEntity($entity);
553
554
    /**
555
     * @param object $entity
556
     *
557
     * @return object
558
     *
559
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
560
     */
561
    abstract protected function updateEntity($entity);
562
563
    /**
564
     * @param $entity
565
     *
566
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
567
     */
568
    abstract protected function removeEntity($entity);
569
570
    /**
571
     * @param object $entity
572
     * @param string $subresource
573
     * @param int    $page
574
     * @param int    $perPage
575
     *
576
     * @return Paginator|array
577
     */
578
    abstract protected function listSubresource($entity, string $subresource, int $page = 1, int $perPage = 50);
579
580
    /**
581
     * @param object $parent
582
     * @param string $subresource
583
     *
584
     * @return object
585
     */
586
    abstract protected function buildAssociation($parent, string $subresource, $entity);
587
588
    /**
589
     * @param object $associatedEntity
590
     *
591
     * @return object
592
     */
593
    abstract protected function createAssociation($associatedEntity);
594
595
    /**
596
     * @param object     $parent
597
     * @param string     $subresource
598
     * @param int|string $subId
599
     *
600
     * @return object
601
     */
602
    abstract protected function addAssociation($parent, string $subresource, $subId);
603
604
    /**
605
     * @param object          $parent
606
     * @param string          $subresource
607
     * @param int|string|null $subId
608
     *
609
     * @return mixed
610
     */
611
    abstract protected function removeAssociation($parent, string $subresource, $subId = null);
612
}
613