Completed
Push — standalone ( b13ba2...a782f9 )
by Philip
05:27
created

RestResourceController::createAssociation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
crap 1
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
169 2
        $restRequestParser = $this->getRequestParser();
170 2
        $entity = $this->createAssociation($parent, $subresource);
171 2
        $entity = $restRequestParser->parseEntity($request, $this->getSubResourceEntityClass($subresource), $entity);
172
173 2
        $entity = $this->postProcessSubResourcePostedEntity($parent, $subresource, $entity);
174
175 2
        $errors = $this->getValidator()->validate($entity);
176
177 2
        if ($errors->count() > 0) {
178
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
179
        }
180
181 2
        $entity = $this->createSubResource($parent, $subresource, $entity);
182
183 2
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
184
185 2
        return new JsonResponse($content, Response::HTTP_CREATED);
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191 12
    public function putSubresourceAction(Request $request, $id, string $subresource, $subId)
192
    {
193 12
        $parent = $this->fetchEntity($id);
194 12
        $this->assertSubresourcePutGranted($parent, $subresource);
195 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...
196
197 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203 12
    public function deleteSubresourceAction(Request $request, $id, string $subresource, $subId = null)
204
    {
205 12
        $parent = $this->fetchEntity($id);
206 12
        $this->assertSubresourceDeleteGranted($parent, $subresource);
207 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...
208
209 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
210
    }
211
212
    /**
213
     * @return CrudServiceInterface
214
     */
215 72
    protected function getService(): CrudServiceInterface
216
    {
217 72
        $serviceId = $this->getServiceId();
218 72
        if (null === $serviceId) {
219 70
            $entityClass = $this->getEntityClass();
220 70
            if (null === $entityClass) {
221
                throw new \RuntimeException('No service or entity class given');
222
            }
223
            /** @var EntityManagerInterface $entityManager */
224 70
            $entityManager = $this->container->get('doctrine.orm.entity_manager');
225 70
            $repository = $entityManager->getRepository($entityClass);
226 70
            if (!$repository instanceof CrudServiceInterface) {
227
                throw new \RuntimeException(
228
                    'Your Entity Repository needs to be an instance of ' . CrudServiceInterface::class . '.'
229
                );
230
            }
231
232 70
            return $repository;
233
        } else {
234
            /** @var CrudServiceInterface $service */
235 2
            $service = $this->container->get($serviceId);
236
237 2
            return $service;
238
        }
239
    }
240
241
    /**
242
     * @param object $entity
243
     *
244
     * @return object
245
     */
246 12
    protected function postProcessPostedEntity($entity)
247
    {
248 12
        return $entity;
249
    }
250
251
    /**
252
     * @param object $entity
253
     *
254
     * @return object
255
     */
256 10
    protected function postProcessPuttedEntity($entity)
257
    {
258 10
        return $entity;
259
    }
260
261
    /**
262
     * @param object $parent
263
     * @param string $subresource
264
     * @param object $entity
265
     *
266
     * @return object
267
     */
268 2
    protected function postProcessSubResourcePostedEntity($parent, $subresource, $entity)
269
    {
270 2
        return $entity;
271
    }
272
273 56
    protected function fetchEntity($id)
274
    {
275 56
        $entity = $this->getService()->find($id);
276 56
        if (null === $entity) {
277 2
            throw new NotFoundHttpException();
278
        }
279
280 56
        return $entity;
281
    }
282
283
    /**
284
     * @param int $page
285
     * @param int $perPage
286
     *
287
     * @return Paginator|array
288
     */
289 6
    protected function listEntities(int $page = 1, int $perPage = 50)
290
    {
291 6
        return $this->getService()->findAllPaginated($page, $perPage);
292
    }
293
294 10
    protected function createEntity($entity)
295
    {
296 10
        return $this->getService()->create($entity);
297
    }
298
299 10
    protected function updateEntity($entity)
300
    {
301 10
        return $this->getService()->update($entity);
302
    }
303
304
    /**
305
     * @param object $parent
306
     * @param string $subresource
307
     *
308
     * @return object
309
     */
310 2
    protected function createAssociation($parent, string $subresource)
311
    {
312 2
        return $this->getService()->createAssociation($parent, $subresource);
0 ignored issues
show
Bug introduced by
The call to createAssociation() misses a required argument $child.

This check looks for function calls that miss required arguments.

Loading history...
313
    }
314
315
    /**
316
     * @param object $entity
317
     * @param string $property
318
     * @param int    $page
319
     * @param int    $perPage
320
     *
321
     * @return Paginator|array
322
     */
323 6
    protected function listSubresource($entity, string $property, int $page = 1, int $perPage = 50)
324
    {
325 6
        return $this->getService()->findAssociationPaginated($entity, $property, $page, $perPage);
326
    }
327
328 80
    protected function getEntityClass()
329
    {
330 80
        return $this->getCurrentRequest()->attributes->get('_entityClass');
331
    }
332
333
    protected function getShortName()
334
    {
335
        return Inflector::tableize($this->getClassMetadata()->reflection->getShortName());
336
    }
337
338 72
    protected function getServiceId()
339
    {
340 72
        return $this->getCurrentRequest()->attributes->get('_service');
341
    }
342
343 80
    protected function getCurrentRequest()
344
    {
345 80
        return $this->getRequestStack()->getCurrentRequest();
346
    }
347
348 10
    protected function assertListGranted()
349
    {
350 10
        $method = $this->getClassMetadata()->getMethod(Method::LIST);
351 10
        if ($method !== null && null !== $right = $method->right) {
352 8
            $this->denyAccessUnlessGranted($right->attributes);
353
        }
354 6
    }
355
356 14
    protected function assertPostGranted()
357
    {
358 14
        $method = $this->getClassMetadata()->getMethod(Method::POST);
359 14
        if ($method !== null && null !== $right = $method->right) {
360 8
            $this->denyAccessUnlessGranted($right->attributes);
361
        }
362 12
    }
363
364 30
    protected function assertGetGranted($entity)
365
    {
366 30
        $method = $this->getClassMetadata()->getMethod(Method::GET);
367 30
        if ($method !== null && null !== $right = $method->right) {
368 10
            $this->assertRightGranted($entity, $right);
369
        }
370 28
    }
371
372 12
    protected function assertPutGranted($entity)
373
    {
374 12
        $method = $this->getClassMetadata()->getMethod(Method::PUT);
375 12
        if ($method !== null && null !== $right = $method->right) {
376 6
            $this->assertRightGranted($entity, $right);
377
        }
378 10
    }
379
380 4
    protected function assertDeleteGranted($entity)
381
    {
382 4
        $method = $this->getClassMetadata()->getMethod(Method::DELETE);
383 4
        if ($method !== null && null !== $right = $method->right) {
384 4
            $this->assertRightGranted($entity, $right);
385
        }
386 2
    }
387
388 6
    protected function assertSubresourceListGranted($entity, $subresource)
389
    {
390 6
        $classMetadata = $this->getClassMetadata();
391
        /** @var PropertyMetadata $propertyMetadata */
392 6
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
393 6
        $method = $propertyMetadata->getMethod(Method::LIST);
394 6
        $right = $method->right;
395 6
        if (null === $right) {
396
            return;
397
        }
398
399 6
        $this->assertRightGranted($entity, $right);
400 6
    }
401
402 4
    protected function assertSubresourcePostGranted($entity, $subresource)
403
    {
404 4
        $classMetadata = $this->getClassMetadata();
405
        /** @var PropertyMetadata $propertyMetadata */
406 4
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
407 4
        $method = $propertyMetadata->getMethod(Method::POST);
408 4
        $right = $method->right;
409 4
        if (null === $right) {
410
            throw new AccessDeniedException();
411
        }
412
413 4
        $this->assertRightGranted($entity, $right);
414 2
    }
415
416 12
    protected function assertSubresourcePutGranted($entity, $subresource)
417
    {
418 12
        $classMetadata = $this->getClassMetadata();
419
        /** @var PropertyMetadata $propertyMetadata */
420 12
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
421 12
        $method = $propertyMetadata->getMethod(Method::PUT);
422 12
        if (null !== $right = $method->right) {
423 4
            $this->assertRightGranted($entity, $right);
424
        }
425 12
    }
426
427 12
    protected function assertSubresourceDeleteGranted($entity, $subresource)
428
    {
429 12
        $classMetadata = $this->getClassMetadata();
430
        /** @var PropertyMetadata $propertyMetadata */
431 12
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
432 12
        $method = $propertyMetadata->getMethod(Method::PUT);
433 12
        if (null !== $right = $method->right) {
434 4
            $this->assertRightGranted($entity, $right);
435
        }
436 12
    }
437
438
    /**
439
     * @return ClassMetadata
440
     */
441 80
    protected function getClassMetadata()
442
    {
443 80
        $metaDataFactory = $this->getMetadataFactory();
444
        /** @var ClassMetadata $classMetaData */
445 80
        $classMetaData = $metaDataFactory->getMetadataForClass($this->getEntityClass());
446
447 80
        return $classMetaData;
448
    }
449
450 2
    protected function getSubResourceEntityClass($subresource)
451
    {
452
        /** @var PropertyMetadata $propertyMetadata */
453 2
        $propertyMetadata = $this->getClassMetadata()->propertyMetadata[$subresource];
454
455 2
        return $propertyMetadata->getType();
456
    }
457
458
    protected function resolveSubject($entity, $propertyPath)
459
    {
460
        if ('this' === $propertyPath) {
461
            return $entity;
462
        }
463
        $propertyAccessor = $this->getPropertyAccessor();
464
465
        return $propertyAccessor->getValue($entity, $propertyPath);
466
    }
467
468
    /**
469
     * @param object $entity
470
     * @param Right  $right
471
     */
472 30
    protected function assertRightGranted($entity, Right $right)
473
    {
474 30
        $propertyPath = $right->propertyPath;
475 30
        if (null === $propertyPath) {
476 30
            $this->denyAccessUnlessGranted($right->attributes);
477
        } else {
478
            $subject = $this->resolveSubject($entity, $propertyPath);
479
            $this->denyAccessUnlessGranted($right->attributes, $subject);
480
        }
481 22
    }
482
483
    /**
484
     * @param object $parent
485
     * @param string $subresource
486
     * @param object $entity
487
     *
488
     * @return object
489
     */
490 2
    protected function createSubResource($parent, $subresource, $entity)
0 ignored issues
show
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...
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...
491
    {
492 2
        return $this->getService()->create($entity);
493
    }
494
495
    /**
496
     * @return string|null
497
     */
498
    protected function getSubresource()
499
    {
500
        return $this->getCurrentRequest()->attributes->get('_subresource');
501
    }
502
503 62
    protected function parseIncludes(Request $request)
504
    {
505 62
        $defaultIncludes = $request->attributes->get('_defaultincludes');
506 62
        if (null == $defaultIncludes) {
507 30
            $defaultIncludes = [];
508
        }
509
510 62
        $includeString = $request->query->get('include');
511 62
        if (empty($includeString)) {
512 48
            $includes = [];
513
        } else {
514 16
            $includes = explode(',', $includeString);
515
        }
516
517 62
        return array_merge($defaultIncludes, $includes);
518
    }
519
520 2
    private function parseConstraintViolations(ConstraintViolationListInterface $errors)
521
    {
522 2
        $data = [];
523
        /** @var ConstraintViolationInterface $error */
524 2
        foreach ($errors as $error) {
525 2
            $data[] = [
526 2
                'propertyPath' => $error->getPropertyPath(),
527 2
                'message'      => $error->getMessage(),
528 2
                'value'        => $error->getInvalidValue()
529
            ];
530
        }
531
532 2
        return $data;
533
    }
534
535 12
    private function addPaginationHeaders(Response $response, int $page, int $perPage, int $total)
536
    {
537 12
        $response->headers->add(
538
            [
539 12
                'x-pagination-current-page' => $page,
540 12
                'x-pagination-per-page'     => $perPage,
541 12
                'x-pagination-total'        => $total,
542 12
                'x-pagination-total-pages'  => (int)(($total - 1) / $perPage + 1)
543
            ]
544
        );
545 12
    }
546
547 46
    protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.')
548
    {
549 46
        if (!$this->getAuthorizationChecker()->isGranted($attributes, $object)) {
550 14
            throw new AccessDeniedException($message);
551
        }
552 32
    }
553
554
    /**
555
     * @return Normalizer
556
     */
557 62
    protected function getNormalizer()
558
    {
559 62
        return $this->container->get('ddr_rest.normalizer');
560
    }
561
562
    /**
563
     * @return ValidatorInterface
564
     */
565 24
    protected function getValidator()
566
    {
567 24
        return $this->container->get('validator');
568
    }
569
570
    /**
571
     * @return RestRequestParser
572
     */
573 24
    protected function getRequestParser()
574
    {
575 24
        return $this->container->get('ddr.rest.parser.request');
576
    }
577
578
    /**
579
     * @return RequestStack
580
     */
581 80
    protected function getRequestStack()
582
    {
583 80
        return $this->container->get('request_stack');
584
    }
585
586
    /**
587
     * @return MetadataFactoryInterface
588
     */
589 80
    protected function getMetadataFactory()
590
    {
591 80
        return $this->container->get('ddr_rest.metadata.factory');
592
    }
593
594
    /**
595
     * @return PropertyAccessorInterface
596
     */
597
    protected function getPropertyAccessor()
598
    {
599
        return $this->container->get('property_accessor');
600
    }
601
602
    /**
603
     * @return AuthorizationCheckerInterface
604
     */
605 46
    protected function getAuthorizationChecker()
606
    {
607 46
        return $this->container->get('security.authorization_checker');
608
    }
609
}
610