Completed
Push — standalone ( 9e8eb1...b13ba2 )
by Philip
04:41
created

RestResourceController::getSubresource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
namespace Dontdrinkandroot\RestBundle\Controller;
4
5
use Doctrine\Common\Util\Inflector;
6
use Doctrine\ORM\EntityManagerInterface;
7
use Doctrine\ORM\Tools\Pagination\Paginator;
8
use Dontdrinkandroot\RestBundle\Metadata\Annotation\Method;
9
use Dontdrinkandroot\RestBundle\Metadata\Annotation\Right;
10
use Dontdrinkandroot\RestBundle\Metadata\ClassMetadata;
11
use Dontdrinkandroot\RestBundle\Metadata\PropertyMetadata;
12
use Dontdrinkandroot\RestBundle\Service\Normalizer;
13
use Dontdrinkandroot\RestBundle\Service\RestRequestParser;
14
use Dontdrinkandroot\Service\CrudServiceInterface;
15
use Metadata\MetadataFactoryInterface;
16
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
17
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
18
use Symfony\Component\HttpFoundation\JsonResponse;
19
use Symfony\Component\HttpFoundation\Request;
20
use Symfony\Component\HttpFoundation\RequestStack;
21
use Symfony\Component\HttpFoundation\Response;
22
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
23
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
24
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
25
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
26
use Symfony\Component\Validator\ConstraintViolationInterface;
27
use Symfony\Component\Validator\ConstraintViolationListInterface;
28
use Symfony\Component\Validator\Validator\ValidatorInterface;
29
30
class RestResourceController implements ContainerAwareInterface, RestResourceControllerInterface
31
{
32
    use ContainerAwareTrait;
33
34
    /**
35
     * {@inheritdoc}
36
     */
37 10
    public function listAction(Request $request)
38
    {
39 10
        $page = $request->query->get('page', 1);
40 10
        $perPage = $request->query->get('perPage', 50);
41
42 10
        $this->assertListGranted();
43
44 6
        $listResult = $this->listEntities($page, $perPage);
45
46 6
        $response = new JsonResponse();
47
48 6
        if ($listResult instanceof Paginator) {
49 6
            $entities = iterator_to_array($listResult->getIterator());
50 6
            $total = $listResult->count();
51 6
            $this->addPaginationHeaders($response, $page, $perPage, $total);
52
        } else {
53
            $entities = $listResult;
54
        }
55
56 6
        $content = $this->getNormalizer()->normalize($entities, $this->parseIncludes($request));
57
58 6
        $response->setData($content);
59
60 6
        return $response;
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66 14
    public function postAction(Request $request)
67
    {
68 14
        $this->assertPostGranted();
69 12
        $entity = $this->getRequestParser()->parseEntity($request, $this->getEntityClass());
70 12
        $entity = $this->postProcessPostedEntity($entity);
71
72 12
        $errors = $this->getValidator()->validate($entity);
73 12
        if ($errors->count() > 0) {
74 2
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
75
        }
76
77 10
        $entity = $this->createEntity($entity);
78
79 10
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
80
81 10
        return new JsonResponse($content, Response::HTTP_CREATED);
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87 32
    public function getAction(Request $request, $id)
88
    {
89 32
        $entity = $this->fetchEntity($id);
90 30
        $this->assertGetGranted($entity);
91
92 28
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
93
94 28
        return new JsonResponse($content);
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     */
100 12
    public function putAction(Request $request, $id)
101
    {
102 12
        $entity = $this->fetchEntity($id);
103 12
        $this->assertPutGranted($entity);
104 10
        $entity = $this->getRequestParser()->parseEntity($request, $this->getEntityClass(), $entity);
105 10
        $entity = $this->postProcessPuttedEntity($entity);
106
107 10
        $errors = $this->getValidator()->validate($entity);
108 10
        if ($errors->count() > 0) {
109
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
110
        }
111
112 10
        $entity = $this->updateEntity($entity);
113
114 10
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
115
116 10
        return new JsonResponse($content);
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122 4
    public function deleteAction(Request $request, $id)
123
    {
124 4
        $entity = $this->fetchEntity($id);
125 4
        $this->assertDeleteGranted($entity);
126 2
        $this->getService()->remove($entity);
127
128 2
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134 6
    public function listSubresourceAction(Request $request, $id, string $subresource)
135
    {
136 6
        $page = $request->query->get('page', 1);
137 6
        $perPage = $request->query->get('perPage', 50);
138
139 6
        $entity = $this->fetchEntity($id);
140 6
        $this->assertSubresourceListGranted($entity, $subresource);
141
142 6
        $listResult = $this->listSubresource($entity, $subresource, $page, $perPage);
143
144 6
        $response = new JsonResponse();
145
146 6
        if ($listResult instanceof Paginator) {
147 6
            $entities = iterator_to_array($listResult->getIterator());
148 6
            $total = $listResult->count();
149 6
            $this->addPaginationHeaders($response, $page, $perPage, $total);
150
        } else {
151
            $entities = $listResult;
152
        }
153
154 6
        $content = $this->getNormalizer()->normalize($entities, $this->parseIncludes($request));
155
156 6
        $response->setData($content);
157
158 6
        return $response;
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 4
    public function postSubresourceAction(Request $request, $id, string $subresource)
165
    {
166 4
        $parent = $this->fetchEntity($id);
167 4
        $this->assertSubresourcePostGranted($parent, $subresource);
168 2
        $entity = $this->getRequestParser()->parseEntity($request, $this->getSubResourceEntityClass($subresource));
169 2
        $entity = $this->postProcessSubResourcePostedEntity($parent, $subresource, $entity);
170
171 2
        $errors = $this->getValidator()->validate($entity);
172
173 2
        if ($errors->count() > 0) {
174
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
175
        }
176
177 2
        $entity = $this->createSubResource($parent, $subresource, $entity);
178
179 2
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
180
181 2
        return new JsonResponse($content, Response::HTTP_CREATED);
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187 12
    public function putSubresourceAction(Request $request, $id, string $subresource, $subId)
188
    {
189 12
        $parent = $this->fetchEntity($id);
190 12
        $this->assertSubresourcePutGranted($parent, $subresource);
191 12
        $this->getService()->addAssociation($parent, $subresource, $subId);
0 ignored issues
show
Bug introduced by
The method addAssociation() does not seem to exist on object<Dontdrinkandroot\...e\CrudServiceInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
192
193 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199 12
    public function deleteSubresourceAction(Request $request, $id, string $subresource, $subId = null)
200
    {
201 12
        $parent = $this->fetchEntity($id);
202 12
        $this->assertSubresourceDeleteGranted($parent, $subresource);
203 12
        $this->getService()->removeAssociation($parent, $subresource, $subId);
0 ignored issues
show
Bug introduced by
The method removeAssociation() does not exist on Dontdrinkandroot\Service\CrudServiceInterface. Did you maybe mean remove()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
204
205 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
206
    }
207
208
    /**
209
     * @return CrudServiceInterface
210
     */
211 72
    protected function getService(): CrudServiceInterface
212
    {
213 72
        $serviceId = $this->getServiceId();
214 72
        if (null === $serviceId) {
215 70
            $entityClass = $this->getEntityClass();
216 70
            if (null === $entityClass) {
217
                throw new \RuntimeException('No service or entity class given');
218
            }
219
            /** @var EntityManagerInterface $entityManager */
220 70
            $entityManager = $this->container->get('doctrine.orm.entity_manager');
221 70
            $repository = $entityManager->getRepository($entityClass);
222 70
            if (!$repository instanceof CrudServiceInterface) {
223
                throw new \RuntimeException(
224
                    'Your Entity Repository needs to be an instance of ' . CrudServiceInterface::class . '.'
225
                );
226
            }
227
228 70
            return $repository;
229
        } else {
230
            /** @var CrudServiceInterface $service */
231 2
            $service = $this->container->get($serviceId);
232
233 2
            return $service;
234
        }
235
    }
236
237
    /**
238
     * @param object $entity
239
     *
240
     * @return object
241
     */
242 12
    protected function postProcessPostedEntity($entity)
243
    {
244 12
        return $entity;
245
    }
246
247
    /**
248
     * @param object $entity
249
     *
250
     * @return object
251
     */
252 10
    protected function postProcessPuttedEntity($entity)
253
    {
254 10
        return $entity;
255
    }
256
257
    /**
258
     * @param object $parent
259
     * @param string $subresource
260
     * @param object $entity
261
     *
262
     * @return object
263
     */
264 2
    protected function postProcessSubResourcePostedEntity($parent, $subresource, $entity)
265
    {
266 2
        return $entity;
267
    }
268
269 56
    protected function fetchEntity($id)
270
    {
271 56
        $entity = $this->getService()->find($id);
272 56
        if (null === $entity) {
273 2
            throw new NotFoundHttpException();
274
        }
275
276 56
        return $entity;
277
    }
278
279
    /**
280
     * @param int $page
281
     * @param int $perPage
282
     *
283
     * @return Paginator|array
284
     */
285 6
    protected function listEntities(int $page = 1, int $perPage = 50)
286
    {
287 6
        return $this->getService()->findAllPaginated($page, $perPage);
288
    }
289
290 10
    protected function createEntity($entity)
291
    {
292 10
        return $this->getService()->create($entity);
293
    }
294
295 10
    protected function updateEntity($entity)
296
    {
297 10
        return $this->getService()->update($entity);
298
    }
299
300
    /**
301
     * @param object $entity
302
     * @param string $property
303
     * @param int    $page
304
     * @param int    $perPage
305
     *
306
     * @return Paginator|array
307
     */
308 6
    protected function listSubresource($entity, string $property, int $page = 1, int $perPage = 50)
309
    {
310 6
        return $this->getService()->findAssociationPaginated($entity, $property, $page, $perPage);
311
    }
312
313 80
    protected function getEntityClass()
314
    {
315 80
        return $this->getCurrentRequest()->attributes->get('_entityClass');
316
    }
317
318
    protected function getShortName()
319
    {
320
        return Inflector::tableize($this->getClassMetadata()->reflection->getShortName());
321
    }
322
323 72
    protected function getServiceId()
324
    {
325 72
        return $this->getCurrentRequest()->attributes->get('_service');
326
    }
327
328 80
    protected function getCurrentRequest()
329
    {
330 80
        return $this->getRequestStack()->getCurrentRequest();
331
    }
332
333 10
    protected function assertListGranted()
334
    {
335 10
        $method = $this->getClassMetadata()->getMethod(Method::LIST);
336 10
        if ($method !== null && null !== $right = $method->right) {
337 8
            $this->denyAccessUnlessGranted($right->attributes);
338
        }
339 6
    }
340
341 14
    protected function assertPostGranted()
342
    {
343 14
        $method = $this->getClassMetadata()->getMethod(Method::POST);
344 14
        if ($method !== null && null !== $right = $method->right) {
345 8
            $this->denyAccessUnlessGranted($right->attributes);
346
        }
347 12
    }
348
349 30
    protected function assertGetGranted($entity)
350
    {
351 30
        $method = $this->getClassMetadata()->getMethod(Method::GET);
352 30
        if ($method !== null && null !== $right = $method->right) {
353 10
            $this->assertRightGranted($entity, $right);
354
        }
355 28
    }
356
357 12
    protected function assertPutGranted($entity)
358
    {
359 12
        $method = $this->getClassMetadata()->getMethod(Method::PUT);
360 12
        if ($method !== null && null !== $right = $method->right) {
361 6
            $this->assertRightGranted($entity, $right);
362
        }
363 10
    }
364
365 4
    protected function assertDeleteGranted($entity)
366
    {
367 4
        $method = $this->getClassMetadata()->getMethod(Method::DELETE);
368 4
        if ($method !== null && null !== $right = $method->right) {
369 4
            $this->assertRightGranted($entity, $right);
370
        }
371 2
    }
372
373 6
    protected function assertSubresourceListGranted($entity, $subresource)
374
    {
375 6
        $classMetadata = $this->getClassMetadata();
376
        /** @var PropertyMetadata $propertyMetadata */
377 6
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
378 6
        $method = $propertyMetadata->getMethod(Method::LIST);
379 6
        $right = $method->right;
380 6
        if (null === $right) {
381
            return;
382
        }
383
384 6
        $this->assertRightGranted($entity, $right);
385 6
    }
386
387 4
    protected function assertSubresourcePostGranted($entity, $subresource)
388
    {
389 4
        $classMetadata = $this->getClassMetadata();
390
        /** @var PropertyMetadata $propertyMetadata */
391 4
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
392 4
        $method = $propertyMetadata->getMethod(Method::POST);
393 4
        $right = $method->right;
394 4
        if (null === $right) {
395
            throw new AccessDeniedException();
396
        }
397
398 4
        $this->assertRightGranted($entity, $right);
399 2
    }
400
401 12
    protected function assertSubresourcePutGranted($entity, $subresource)
402
    {
403 12
        $classMetadata = $this->getClassMetadata();
404
        /** @var PropertyMetadata $propertyMetadata */
405 12
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
406 12
        $method = $propertyMetadata->getMethod(Method::PUT);
407 12
        if (null !== $right = $method->right) {
408 4
            $this->assertRightGranted($entity, $right);
409
        }
410 12
    }
411
412 12
    protected function assertSubresourceDeleteGranted($entity, $subresource)
413
    {
414 12
        $classMetadata = $this->getClassMetadata();
415
        /** @var PropertyMetadata $propertyMetadata */
416 12
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
417 12
        $method = $propertyMetadata->getMethod(Method::PUT);
418 12
        if (null !== $right = $method->right) {
419 4
            $this->assertRightGranted($entity, $right);
420
        }
421 12
    }
422
423
    /**
424
     * @return ClassMetadata
425
     */
426 80
    protected function getClassMetadata()
427
    {
428 80
        $metaDataFactory = $this->getMetadataFactory();
429
        /** @var ClassMetadata $classMetaData */
430 80
        $classMetaData = $metaDataFactory->getMetadataForClass($this->getEntityClass());
431
432 80
        return $classMetaData;
433
    }
434
435 2
    protected function getSubResourceEntityClass($subresource)
436
    {
437
        /** @var PropertyMetadata $propertyMetadata */
438 2
        $propertyMetadata = $this->getClassMetadata()->propertyMetadata[$subresource];
439
440 2
        return $propertyMetadata->getType();
441
    }
442
443
    protected function resolveSubject($entity, $propertyPath)
444
    {
445
        if ('this' === $propertyPath) {
446
            return $entity;
447
        }
448
        $propertyAccessor = $this->getPropertyAccessor();
449
450
        return $propertyAccessor->getValue($entity, $propertyPath);
451
    }
452
453
    /**
454
     * @param object $entity
455
     * @param Right  $right
456
     */
457 30
    protected function assertRightGranted($entity, Right $right)
458
    {
459 30
        $propertyPath = $right->propertyPath;
460 30
        if (null === $propertyPath) {
461 30
            $this->denyAccessUnlessGranted($right->attributes);
462
        } else {
463
            $subject = $this->resolveSubject($entity, $propertyPath);
464
            $this->denyAccessUnlessGranted($right->attributes, $subject);
465
        }
466 22
    }
467
468
    /**
469
     * @param object $parent
470
     * @param string $subresource
471
     * @param object $entity
472
     *
473
     * @return
474
     */
475 2
    protected function createSubResource($parent, $subresource, $entity)
476
    {
477 2
        return $this->getService()->createAssociation($parent, $subresource, $entity);
478
    }
479
480
    /**
481
     * @return string|null
482
     */
483
    protected function getSubresource()
484
    {
485
        return $this->getCurrentRequest()->attributes->get('_subresource');
486
    }
487
488 62
    protected function parseIncludes(Request $request)
489
    {
490 62
        $defaultIncludes = $request->attributes->get('_defaultincludes');
491 62
        if (null == $defaultIncludes) {
492 30
            $defaultIncludes = [];
493
        }
494
495 62
        $includeString = $request->query->get('include');
496 62
        if (empty($includeString)) {
497 48
            $includes = [];
498
        } else {
499 16
            $includes = explode(',', $includeString);
500
        }
501
502 62
        return array_merge($defaultIncludes, $includes);
503
    }
504
505 2
    private function parseConstraintViolations(ConstraintViolationListInterface $errors)
506
    {
507 2
        $data = [];
508
        /** @var ConstraintViolationInterface $error */
509 2
        foreach ($errors as $error) {
510 2
            $data[] = [
511 2
                'propertyPath' => $error->getPropertyPath(),
512 2
                'message'      => $error->getMessage(),
513 2
                'value'        => $error->getInvalidValue()
514
            ];
515
        }
516
517 2
        return $data;
518
    }
519
520 12
    private function addPaginationHeaders(Response $response, int $page, int $perPage, int $total)
521
    {
522 12
        $response->headers->add(
523
            [
524 12
                'x-pagination-current-page' => $page,
525 12
                'x-pagination-per-page'     => $perPage,
526 12
                'x-pagination-total'        => $total,
527 12
                'x-pagination-total-pages'  => (int)(($total - 1) / $perPage + 1)
528
            ]
529
        );
530 12
    }
531
532 46
    protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.')
533
    {
534 46
        if (!$this->getAuthorizationChecker()->isGranted($attributes, $object)) {
535 14
            throw new AccessDeniedException($message);
536
        }
537 32
    }
538
539
    /**
540
     * @return Normalizer
541
     */
542 62
    protected function getNormalizer()
543
    {
544 62
        return $this->container->get('ddr_rest.normalizer');
545
    }
546
547
    /**
548
     * @return ValidatorInterface
549
     */
550 24
    protected function getValidator()
551
    {
552 24
        return $this->container->get('validator');
553
    }
554
555
    /**
556
     * @return RestRequestParser
557
     */
558 24
    protected function getRequestParser()
559
    {
560 24
        return $this->container->get('ddr.rest.parser.request');
561
    }
562
563
    /**
564
     * @return RequestStack
565
     */
566 80
    protected function getRequestStack()
567
    {
568 80
        return $this->container->get('request_stack');
569
    }
570
571
    /**
572
     * @return MetadataFactoryInterface
573
     */
574 80
    protected function getMetadataFactory()
575
    {
576 80
        return $this->container->get('ddr_rest.metadata.factory');
577
    }
578
579
    /**
580
     * @return PropertyAccessorInterface
581
     */
582
    protected function getPropertyAccessor()
583
    {
584
        return $this->container->get('property_accessor');
585
    }
586
587
    /**
588
     * @return AuthorizationCheckerInterface
589
     */
590 46
    protected function getAuthorizationChecker()
591
    {
592 46
        return $this->container->get('security.authorization_checker');
593
    }
594
}
595