Completed
Push — standalone ( f309de...aaef72 )
by Philip
05:10
created

RestResourceController::postAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 10
cts 10
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 10
nc 2
nop 1
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 12
    public function postAction(Request $request)
67
    {
68 12
        $this->assertPostGranted();
69 10
        $entity = $this->getRequestParser()->parseEntity($request, $this->getEntityClass());
70 10
        $entity = $this->postProcessPostedEntity($entity);
71
72 10
        $errors = $this->getValidator()->validate($entity);
73 10
        if ($errors->count() > 0) {
74 2
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
75
        }
76
77 8
        $entity = $this->createEntity($entity);
78
79 8
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
80
81 8
        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 10
    public function putAction(Request $request, $id)
101
    {
102 10
        $entity = $this->fetchEntity($id);
103 10
        $this->assertPutGranted($entity);
104 8
        $entity = $this->getRequestParser()->parseEntity($request, $this->getEntityClass(), $entity);
105 8
        $entity = $this->postProcessPuttedEntity($entity);
106
107 8
        $errors = $this->getValidator()->validate($entity);
108 8
        if ($errors->count() > 0) {
109
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
110
        }
111
112 8
        $entity = $this->updateEntity($entity);
113
114 8
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
115
116 8
        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)
135
    {
136 6
        $page = $request->query->get('page', 1);
137 6
        $perPage = $request->query->get('perPage', 50);
138
139 6
        $subresource = $this->getSubresource();
140 6
        $entity = $this->fetchEntity($id);
141 6
        $this->assertSubresourceListGranted($entity, $subresource);
142
143 6
        $listResult = $this->listSubresource($entity, $subresource, $page, $perPage);
144
145 6
        $response = new JsonResponse();
146
147 6
        if ($listResult instanceof Paginator) {
148 6
            $entities = iterator_to_array($listResult->getIterator());
149 6
            $total = $listResult->count();
150 6
            $this->addPaginationHeaders($response, $page, $perPage, $total);
151
        } else {
152
            $entities = $listResult;
153
        }
154
155 6
        $content = $this->getNormalizer()->normalize($entities, $this->parseIncludes($request));
156
157 6
        $response->setData($content);
158
159 6
        return $response;
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165 4
    public function postSubresourceAction(Request $request, $id)
166
    {
167 4
        $subresource = $this->getSubresource();
168 4
        $parent = $this->fetchEntity($id);
169 4
        $this->assertSubresourcePostGranted($parent, $subresource);
170 2
        $entity = $this->getRequestParser()->parseEntity(
171
            $request,
172 2
            $this->getSubResourceEntityClass($subresource)
173
        );
174 2
        $entity = $this->postProcessSubResourcePostedEntity($subresource, $entity, $parent);
175
176 2
        $errors = $this->getValidator()->validate($entity);
177
178 2
        if ($errors->count() > 0) {
179
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
180
        }
181
182 2
        $entity = $this->createSubResource($parent, $subresource, $entity);
183
184 2
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
185
186 2
        return new JsonResponse($content, Response::HTTP_CREATED);
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 12
    public function putSubresourceAction(Request $request, $id, $subId)
193
    {
194 12
        $subresource = $this->getSubresource();
195 12
        $parent = $this->fetchEntity($id);
196 12
        $this->assertSubresourcePutGranted($parent, $subresource);
197 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...
198
199 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205 12
    public function deleteSubresourceAction(Request $request, $id, $subId = null)
206
    {
207 12
        $subresource = $this->getSubresource();
208 12
        $parent = $this->fetchEntity($id);
209 12
        $this->assertSubresourceDeleteGranted($parent, $subresource);
210 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...
211
212 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
213
    }
214
215
    /**
216
     * @return CrudServiceInterface
217
     */
218 68
    protected function getService(): CrudServiceInterface
219
    {
220 68
        $serviceId = $this->getServiceId();
221 68
        if (null === $serviceId) {
222 66
            $entityClass = $this->getEntityClass();
223 66
            if (null === $entityClass) {
224
                throw new \RuntimeException('No service or entity class given');
225
            }
226
            /** @var EntityManagerInterface $entityManager */
227 66
            $entityManager = $this->container->get('doctrine.orm.entity_manager');
228 66
            $repository = $entityManager->getRepository($entityClass);
229 66
            if (!$repository instanceof CrudServiceInterface) {
230
                throw new \RuntimeException(
231
                    'Your Entity Repository needs to be an instance of ' . CrudServiceInterface::class . '.'
232
                );
233
            }
234
235 66
            return $repository;
236
        } else {
237
            /** @var CrudServiceInterface $service */
238 2
            $service = $this->container->get($serviceId);
239
240 2
            return $service;
241
        }
242
    }
243
244
    /**
245
     * @param object $entity
246
     *
247
     * @return object
248
     */
249 10
    protected function postProcessPostedEntity($entity)
250
    {
251 10
        return $entity;
252
    }
253
254
    /**
255
     * @param object $entity
256
     *
257
     * @return object
258
     */
259 8
    protected function postProcessPuttedEntity($entity)
260
    {
261 8
        return $entity;
262
    }
263
264
    /**
265
     * @param string $subresource
266
     * @param object $parent
267
     * @param object $entity
268
     *
269
     * @return object
270
     */
271 2
    protected function postProcessSubResourcePostedEntity($subresource, $entity, $parent)
272
    {
273 2
        return $entity;
274
    }
275
276 54
    protected function fetchEntity($id)
277
    {
278 54
        $entity = $this->getService()->find($id);
279 54
        if (null === $entity) {
280 2
            throw new NotFoundHttpException();
281
        }
282
283 54
        return $entity;
284
    }
285
286
    /**
287
     * @param int $page
288
     * @param int $perPage
289
     *
290
     * @return Paginator|array
291
     */
292 6
    protected function listEntities(int $page = 1, int $perPage = 50)
293
    {
294 6
        return $this->getService()->findAllPaginated($page, $perPage);
295
    }
296
297 8
    protected function createEntity($entity)
298
    {
299 8
        return $this->getService()->create($entity);
300
    }
301
302 8
    protected function updateEntity($entity)
303
    {
304 8
        return $this->getService()->update($entity);
305
    }
306
307
    /**
308
     * @param object $entity
309
     * @param string $property
310
     * @param int    $page
311
     * @param int    $perPage
312
     *
313
     * @return Paginator|array
314
     */
315 6
    protected function listSubresource($entity, string $property, int $page = 1, int $perPage = 50)
316
    {
317 6
        return $this->getService()->findAssociationPaginated($entity, $property, $page, $perPage);
318
    }
319
320 76
    protected function getEntityClass()
321
    {
322 76
        return $this->getCurrentRequest()->attributes->get('_entityClass');
323
    }
324
325
    protected function getShortName()
326
    {
327
        return Inflector::tableize($this->getClassMetadata()->reflection->getShortName());
328
    }
329
330 68
    protected function getServiceId()
331
    {
332 68
        return $this->getCurrentRequest()->attributes->get('_service');
333
    }
334
335 76
    protected function getCurrentRequest()
336
    {
337 76
        return $this->getRequestStack()->getCurrentRequest();
338
    }
339
340 10
    protected function assertListGranted()
341
    {
342 10
        $method = $this->getClassMetadata()->getMethod(Method::LIST);
343 10
        if ($method !== null && null !== $right = $method->right) {
344 8
            $this->denyAccessUnlessGranted($right->attributes);
345
        }
346 6
    }
347
348 12
    protected function assertPostGranted()
349
    {
350 12
        $method = $this->getClassMetadata()->getMethod(Method::POST);
351 12
        if ($method !== null && null !== $right = $method->right) {
352 6
            $this->denyAccessUnlessGranted($right->attributes);
353
        }
354 10
    }
355
356 30
    protected function assertGetGranted($entity)
357
    {
358 30
        $method = $this->getClassMetadata()->getMethod(Method::GET);
359 30
        if ($method !== null && null !== $right = $method->right) {
360 10
            $this->assertRightGranted($entity, $right);
361
        }
362 28
    }
363
364 10
    protected function assertPutGranted($entity)
365
    {
366 10
        $method = $this->getClassMetadata()->getMethod(Method::PUT);
367 10
        if ($method !== null && null !== $right = $method->right) {
368 4
            $this->assertRightGranted($entity, $right);
369
        }
370 8
    }
371
372 4
    protected function assertDeleteGranted($entity)
373
    {
374 4
        $method = $this->getClassMetadata()->getMethod(Method::POST);
375 4
        if ($method !== null && null !== $right = $method->right) {
376 4
            $this->assertRightGranted($entity, $right);
377
        }
378 2
    }
379
380 6
    protected function assertSubresourceListGranted($entity, $subresource)
381
    {
382 6
        $classMetadata = $this->getClassMetadata();
383
        /** @var PropertyMetadata $propertyMetadata */
384 6
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
385 6
        $method = $propertyMetadata->getMethod(Method::LIST);
386 6
        $right = $method->right;
387 6
        if (null === $right) {
388
            return;
389
        }
390
391 6
        $this->assertRightGranted($entity, $right);
392 6
    }
393
394 4
    protected function assertSubresourcePostGranted($entity, $subresource)
395
    {
396 4
        $classMetadata = $this->getClassMetadata();
397
        /** @var PropertyMetadata $propertyMetadata */
398 4
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
399 4
        $method = $propertyMetadata->getMethod(Method::POST);
400 4
        $right = $method->right;
401 4
        if (null === $right) {
402
            throw new AccessDeniedException();
403
        }
404
405 4
        $this->assertRightGranted($entity, $right);
406 2
    }
407
408 12
    protected function assertSubresourcePutGranted($entity, $subresource)
409
    {
410 12
        $classMetadata = $this->getClassMetadata();
411
        /** @var PropertyMetadata $propertyMetadata */
412 12
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
413 12
        $method = $propertyMetadata->getMethod(Method::PUT);
414 12
        if (null !== $right = $method->right) {
415 4
            $this->assertRightGranted($entity, $right);
416
        }
417 12
    }
418
419 12
    protected function assertSubresourceDeleteGranted($entity, $subresource)
420
    {
421 12
        $classMetadata = $this->getClassMetadata();
422
        /** @var PropertyMetadata $propertyMetadata */
423 12
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
424 12
        $method = $propertyMetadata->getMethod(Method::PUT);
425 12
        if (null !== $right = $method->right) {
426 4
            $this->assertRightGranted($entity, $right);
427
        }
428 12
    }
429
430
    /**
431
     * @return ClassMetadata
432
     */
433 76
    protected function getClassMetadata()
434
    {
435 76
        $metaDataFactory = $this->getMetadataFactory();
436
        /** @var ClassMetadata $classMetaData */
437 76
        $classMetaData = $metaDataFactory->getMetadataForClass($this->getEntityClass());
438
439 76
        return $classMetaData;
440
    }
441
442 2
    protected function getSubResourceEntityClass($subresource)
443
    {
444
        /** @var PropertyMetadata $propertyMetadata */
445 2
        $propertyMetadata = $this->getClassMetadata()->propertyMetadata[$subresource];
446
447 2
        return $propertyMetadata->getType();
448
    }
449
450
    protected function resolveSubject($entity, $propertyPath)
451
    {
452
        if ('this' === $propertyPath) {
453
            return $entity;
454
        }
455
        $propertyAccessor = $this->getPropertyAccessor();
456
457
        return $propertyAccessor->getValue($entity, $propertyPath);
458
    }
459
460
    /**
461
     * @param object $entity
462
     * @param Right  $right
463
     */
464 28
    protected function assertRightGranted($entity, Right $right)
465
    {
466 28
        $propertyPath = $right->propertyPath;
467 28
        if (null === $propertyPath) {
468 28
            $this->denyAccessUnlessGranted($right->attributes);
469
        } else {
470
            $subject = $this->resolveSubject($entity, $propertyPath);
471
            $this->denyAccessUnlessGranted($right->attributes, $subject);
472
        }
473 20
    }
474
475
    /**
476
     * @param object $parent
477
     * @param string $subresource
478
     * @param object $entity
479
     *
480
     * @return
481
     */
482 2
    protected function createSubResource($parent, $subresource, $entity)
483
    {
484 2
        return $this->getService()->createAssociation($parent, $subresource, $entity);
485
    }
486
487
    /**
488
     * @return string|null
489
     */
490 30
    protected function getSubresource()
491
    {
492 30
        return $this->getCurrentRequest()->attributes->get('_subresource');
493
    }
494
495 58
    protected function parseIncludes(Request $request)
496
    {
497 58
        $defaultIncludes = $request->attributes->get('_defaultincludes');
498 58
        if (null == $defaultIncludes) {
499 30
            $defaultIncludes = [];
500
        }
501
502 58
        $includeString = $request->query->get('include');
503 58
        if (empty($includeString)) {
504 44
            $includes = [];
505
        } else {
506 16
            $includes = explode(',', $includeString);
507
        }
508
509 58
        return array_merge($defaultIncludes, $includes);
510
    }
511
512 2
    private function parseConstraintViolations(ConstraintViolationListInterface $errors)
513
    {
514 2
        $data = [];
515
        /** @var ConstraintViolationInterface $error */
516 2
        foreach ($errors as $error) {
517 2
            $data[] = [
518 2
                'propertyPath' => $error->getPropertyPath(),
519 2
                'message'      => $error->getMessage(),
520 2
                'value'        => $error->getInvalidValue()
521
            ];
522
        }
523
524 2
        return $data;
525
    }
526
527 12
    private function addPaginationHeaders(Response $response, int $page, int $perPage, int $total)
528
    {
529 12
        $response->headers->add(
530
            [
531 12
                'x-pagination-current-page' => $page,
532 12
                'x-pagination-per-page'     => $perPage,
533 12
                'x-pagination-total'        => $total,
534 12
                'x-pagination-total-pages'  => (int)(($total - 1) / $perPage + 1)
535
            ]
536
        );
537 12
    }
538
539 42
    protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.')
540
    {
541 42
        if (!$this->getAuthorizationChecker()->isGranted($attributes, $object)) {
542 14
            throw new AccessDeniedException($message);
543
        }
544 28
    }
545
546
    /**
547
     * @return Normalizer
548
     */
549 58
    protected function getNormalizer()
550
    {
551 58
        return $this->container->get('ddr_rest.normalizer');
552
    }
553
554
    /**
555
     * @return ValidatorInterface
556
     */
557 20
    protected function getValidator()
558
    {
559 20
        return $this->container->get('validator');
560
    }
561
562
    /**
563
     * @return RestRequestParser
564
     */
565 20
    protected function getRequestParser()
566
    {
567 20
        return $this->container->get('ddr.rest.parser.request');
568
    }
569
570
    /**
571
     * @return RequestStack
572
     */
573 76
    protected function getRequestStack()
574
    {
575 76
        return $this->container->get('request_stack');
576
    }
577
578
    /**
579
     * @return MetadataFactoryInterface
580
     */
581 76
    protected function getMetadataFactory()
582
    {
583 76
        return $this->container->get('ddr_rest.metadata.factory');
584
    }
585
586
    /**
587
     * @return PropertyAccessorInterface
588
     */
589
    protected function getPropertyAccessor()
590
    {
591
        return $this->container->get('property_accessor');
592
    }
593
594
    /**
595
     * @return AuthorizationCheckerInterface
596
     */
597 42
    protected function getAuthorizationChecker()
598
    {
599 42
        return $this->container->get('security.authorization_checker');
600
    }
601
}
602