Completed
Push — standalone ( 57bbfc...ea253f )
by Philip
03:00
created

EntityController::postSubresourceAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 0
cts 11
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 11
nc 2
nop 2
crap 6
1
<?php
2
3
namespace Dontdrinkandroot\RestBundle\Controller;
4
5
use Doctrine\Common\Util\Inflector;
6
use Doctrine\ORM\Tools\Pagination\Paginator;
7
use Dontdrinkandroot\RestBundle\Metadata\Annotation\Right;
8
use Dontdrinkandroot\RestBundle\Metadata\ClassMetadata;
9
use Dontdrinkandroot\RestBundle\Metadata\PropertyMetadata;
10
use Dontdrinkandroot\RestBundle\Service\CrudServiceInterface;
11
use Dontdrinkandroot\RestBundle\Service\DoctrineEntityRepositoryCrudService;
12
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
13
use Symfony\Component\HttpFoundation\JsonResponse;
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\HttpFoundation\Response;
16
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
17
use Symfony\Component\Validator\ConstraintViolationInterface;
18
use Symfony\Component\Validator\ConstraintViolationListInterface;
19
use Symfony\Component\Validator\Validator\ValidatorInterface;
20
21
class EntityController extends Controller
22
{
23 8
    public function listAction(Request $request)
24
    {
25 8
        $page = $request->query->get('page', 1);
26 8
        $perPage = $request->query->get('perPage', 50);
27
28 8
        $this->assertListGranted();
29
30 6
        $paginator = $this->listEntities($page, $perPage);
31 6
        $total = $paginator->count();
32 6
        $entities = $paginator->getIterator()->getArrayCopy();
33
34 6
        $normalizer = $this->get('ddr_rest.normalizer');
35 6
        $content = $normalizer->normalize($entities, $this->parseIncludes($request));
36
37 6
        $response = new JsonResponse($content, Response::HTTP_OK);
38 6
        $this->addPaginationHeaders($response, $page, $perPage, $total);
39
40 6
        return $response;
41
    }
42
43 2
    public function postAction(Request $request)
44
    {
45 2
        $this->assertPostGranted();
46
        $entity = $this->parseRequest($request, null, $this->getEntityClass());
47
        $entity = $this->postProcessPostedEntity($entity);
48
49
        /** @var ValidatorInterface $validator */
50
        $validator = $this->get('validator');
51
        $errors = $validator->validate($entity);
52
        if ($errors->count() > 0) {
53
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
54
        }
55
56
        $entity = $this->createEntity($entity);
57
58
        $normalizer = $this->get('ddr_rest.normalizer');
59
        $content = $normalizer->normalize($entity);
60
61
        return new JsonResponse($content, Response::HTTP_CREATED);
62
    }
63
64 8
    public function getAction(Request $request, $id)
65
    {
66 8
        $entity = $this->fetchEntity($id);
67 8
        $this->assertGetGranted($entity);
68
69 6
        $normalizer = $this->get('ddr_rest.normalizer');
70 6
        $content = $normalizer->normalize($entity, $this->parseIncludes($request));
71
72 6
        return new JsonResponse($content);
73
    }
74
75 6
    public function putAction(Request $request, $id)
76
    {
77 6
        $entity = $this->fetchEntity($id);
78 6
        $this->assertPutGranted($entity);
79 2
        $entity = $this->parseRequest($request, $entity, $this->getEntityClass());
80 2
        $entity = $this->postProcessPuttedEntity($entity);
81
82
        /** @var ValidatorInterface $validator */
83 2
        $validator = $this->get('validator');
84 2
        $errors = $validator->validate($entity);
85 2
        if ($errors->count() > 0) {
86
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
87
        }
88
89 2
        $entity = $this->updateEntity($entity);
90
91 2
        $normalizer = $this->get('ddr_rest.normalizer');
92 2
        $content = $normalizer->normalize($entity);
93
94 2
        return new JsonResponse($content);
95
    }
96
97 2
    public function deleteAction(Request $request, $id)
0 ignored issues
show
Unused Code introduced by
The parameter $request 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...
98
    {
99 2
        $entity = $this->fetchEntity($id);
100 2
        $this->assertDeleteGranted($entity);
101
        $this->getService()->remove($entity);
102
103
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
104
    }
105
106 6
    public function listSubresourceAction(Request $request, $id)
107
    {
108 6
        $page = $request->query->get('page', 1);
109 6
        $perPage = $request->query->get('perPage', 50);
110
111 6
        $subresource = $this->getSubresource();
112 6
        $entity = $this->fetchEntity($id);
113 6
        $this->assertSubresourceListGranted($entity, $subresource);
114
115 6
        $paginator = $this->listSubresource(
116
            $entity,
117
            $subresource,
118
            $page,
119
            $perPage
120
        );
121 6
        $total = $paginator->count();
122 6
        $entities = $paginator->getIterator()->getArrayCopy();
123
124 6
        $normalizer = $this->get('ddr_rest.normalizer');
125 6
        $content = $normalizer->normalize($entities, $this->parseIncludes($request));
126
127 6
        $response = new JsonResponse($content, Response::HTTP_OK);
128 6
        $this->addPaginationHeaders($response, $page, $perPage, $total);
129
130 6
        return $response;
131
    }
132
133
    public function postSubresourceAction(Request $request, $id)
134
    {
135
        $subresource = $this->getSubresource();
136
        $parent = $this->fetchEntity($id);
137
        $this->assertSubresourcePostGranted($parent, $subresource);
138
        $entity = $this->parseRequest($request, null, $this->getSubResourceEntityClass($subresource));
139
        $entity = $this->postProcessSubResourcePostedEntity($subresource, $entity, $parent);
140
        $errors = $this->validate($entity);
0 ignored issues
show
Bug introduced by
The method validate() does not seem to exist on object<Dontdrinkandroot\...oller\EntityController>.

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...
141
        if ($errors->count() > 0) {
142
            return $this->handleView($this->view($errors, Response::HTTP_BAD_REQUEST));
0 ignored issues
show
Bug introduced by
The method view() does not seem to exist on object<Dontdrinkandroot\...oller\EntityController>.

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...
Bug introduced by
The method handleView() does not seem to exist on object<Dontdrinkandroot\...oller\EntityController>.

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...
143
        }
144
        $entity = $this->saveSubResource($subresource, $entity);
145
146
        return $this->handleView($this->view($entity, Response::HTTP_CREATED));
0 ignored issues
show
Bug introduced by
The method view() does not seem to exist on object<Dontdrinkandroot\...oller\EntityController>.

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...
Bug introduced by
The method handleView() does not seem to exist on object<Dontdrinkandroot\...oller\EntityController>.

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...
147
    }
148
149 2
    public function putSubresourceAction(Request $request, $id, $subId)
0 ignored issues
show
Unused Code introduced by
The parameter $request 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...
150
    {
151 2
        $subresource = $this->getSubresource();
152 2
        $parent = $this->fetchEntity($id);
153 2
        $this->assertSubresourcePutGranted($parent, $subresource);
154 2
        $this->getService()->addToCollection($parent, $subresource, $subId);
155
156 2
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
157
    }
158
159 2
    public function deleteSubresourceAction(Request $request, $id, $subId)
0 ignored issues
show
Unused Code introduced by
The parameter $request 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...
160
    {
161 2
        $subresource = $this->getSubresource();
162 2
        $parent = $this->fetchEntity($id);
163 2
        $this->assertSubresourceDeleteGranted($parent, $subresource);
164 2
        $this->getService()->removeFromCollection($parent, $subresource, $subId);
165
166 2
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
167
    }
168
169
    /**
170
     * @return CrudServiceInterface
171
     */
172 28
    protected function getService()
173
    {
174 28
        $serviceId = $this->getServiceId();
175 28
        if (null === $serviceId) {
176 28
            $entityClass = $this->getEntityClass();
177 28
            if (null === $entityClass) {
178
                throw new \RuntimeException('No service or entity class given');
179
            }
180 28
            $entityManager = $this->get('doctrine.orm.entity_manager');
181
182 28
            return new DoctrineEntityRepositoryCrudService(
183
                $entityManager,
184
                $entityClass
185
            );
186
        } else {
187
            /** @var CrudServiceInterface $service */
188
            $service = $this->get($serviceId);
189
190
            return $service;
191
        }
192
    }
193
194 2
    protected function parseRequest(Request $request, $entity = null, $entityClass = null)
195
    {
196 2
        return $this->get('ddr.rest.parser.request')->parseEntity($request, $entityClass, $entity);
197
    }
198
199
    /**
200
     * @param object $entity
201
     *
202
     * @return object
203
     */
204
    protected function postProcessPostedEntity($entity)
205
    {
206
        return $entity;
207
    }
208
209
    /**
210
     * @param object $entity
211
     *
212
     * @return object
213
     */
214 2
    protected function postProcessPuttedEntity($entity)
215
    {
216 2
        return $entity;
217
    }
218
219
    /**
220
     * @param string $subresource
221
     * @param object $parent
222
     * @param object $entity
223
     *
224
     * @return object
225
     */
226
    protected function postProcessSubResourcePostedEntity($subresource, $entity, $parent)
2 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...
227
    {
228
        return $entity;
229
    }
230
231 22
    protected function fetchEntity($id)
232
    {
233 22
        $entity = $this->getService()->findById($id);
234 22
        if (null === $entity) {
235
            throw new NotFoundHttpException();
236
        }
237
238 22
        return $entity;
239
    }
240
241 6
    protected function listEntities(int $page = 1, int $perPage = 50): Paginator
242
    {
243 6
        $service = $this->getService();
244
245 6
        return $service->listPaginated($page, $perPage);
246
    }
247
248
    protected function createEntity($entity)
249
    {
250
        return $this->getService()->create($entity);
251
    }
252
253 2
    protected function updateEntity($entity)
254
    {
255 2
        return $this->getService()->update($entity);
256
    }
257
258 6
    protected function listSubresource($entity, $property, $page = 1, $perPage = 50): Paginator
259
    {
260 6
        $service = $this->getService();
261
262 6
        return $service->listAssociationPaginated($entity, $property, $page, $perPage);
263
    }
264
265 32
    protected function getEntityClass()
266
    {
267 32
        return $this->getCurrentRequest()->attributes->get('_entityClass');
268
    }
269
270
    protected function getShortName()
271
    {
272
        return Inflector::tableize($this->getClassMetadata()->reflection->getShortName());
273
    }
274
275 28
    protected function getServiceId()
276
    {
277 28
        return $this->getCurrentRequest()->attributes->get('_service');
278
    }
279
280 32
    protected function getCurrentRequest()
281
    {
282 32
        return $this->get('request_stack')->getCurrentRequest();
283
    }
284
285 8
    protected function assertListGranted()
286
    {
287 8
        $classMetadata = $this->getClassMetadata();
288 8
        $right = $classMetadata->getListRight();
289 8
        if (null === $right) {
290 4
            return;
291
        }
292
293 4
        $this->denyAccessUnlessGranted($right->attributes);
294 2
    }
295
296 2
    protected function assertPostGranted()
297
    {
298 2
        $classMetadata = $this->getClassMetadata();
299 2
        $right = $classMetadata->getPostRight();
300 2
        if (null === $right) {
301 2
            throw $this->createAccessDeniedException();
302
        }
303
304
        $this->denyAccessUnlessGranted($right->attributes);
305
    }
306
307 8
    protected function assertGetGranted($entity)
308
    {
309 8
        $classMetadata = $this->getClassMetadata();
310 8
        $right = $classMetadata->getGetRight();
311 8
        if (null === $right) {
312 2
            return;
313
        }
314
315 6
        $this->assertRightGranted($entity, $right);
316 4
    }
317
318 6
    protected function assertPutGranted($entity)
319
    {
320 6
        $classMetadata = $this->getClassMetadata();
321 6
        $right = $classMetadata->getPutRight();
322 6
        if (null === $right) {
323 2
            throw $this->createAccessDeniedException();
324
        }
325
326 4
        $this->assertRightGranted($entity, $right);
327 2
    }
328
329 2
    protected function assertDeleteGranted($entity)
330
    {
331 2
        $classMetadata = $this->getClassMetadata();
332 2
        $right = $classMetadata->getDeleteRight();
333 2
        if (null === $right) {
334 2
            throw $this->createAccessDeniedException();
335
        }
336
337
        $this->assertRightGranted($entity, $right);
338
    }
339
340 6
    protected function assertSubresourceListGranted($entity, $subresource)
341
    {
342 6
        $classMetadata = $this->getClassMetadata();
343
        /** @var PropertyMetadata $propertyMetadata */
344 6
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
345 6
        $right = $propertyMetadata->getSubResourceListRight();
346 6
        if (null === $right) {
347 6
            return;
348
        }
349
350
        $this->assertRightGranted($entity, $right);
351
    }
352
353
    protected function assertSubresourcePostGranted($entity, $subresource)
354
    {
355
        $classMetadata = $this->getClassMetadata();
356
        /** @var PropertyMetadata $propertyMetadata */
357
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
358
        $right = $propertyMetadata->getSubResourcePostRight();
359
        if (null === $right) {
360
            throw $this->createAccessDeniedException();
361
        }
362
363
        $this->assertRightGranted($entity, $right);
364
    }
365
366 2
    protected function assertSubresourcePutGranted($entity, $subresource)
367
    {
368 2
        $classMetadata = $this->getClassMetadata();
369
        /** @var PropertyMetadata $propertyMetadata */
370 2
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
371 2
        $right = $propertyMetadata->getSubResourcePutRight();
372 2
        if (null === $right) {
373
            throw $this->createAccessDeniedException();
374
        }
375
376 2
        $this->assertRightGranted($entity, $right);
377 2
    }
378
379 2
    protected function assertSubresourceDeleteGranted($entity, $subresource)
380
    {
381 2
        $classMetadata = $this->getClassMetadata();
382
        /** @var PropertyMetadata $propertyMetadata */
383 2
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
384 2
        $right = $propertyMetadata->getSubResourceDeleteRight();
385 2
        if (null === $right) {
386
            throw $this->createAccessDeniedException();
387
        }
388
389 2
        $this->assertRightGranted($entity, $right);
390 2
    }
391
392
    /**
393
     * @return ClassMetadata
394
     */
395 32
    protected function getClassMetadata()
396
    {
397 32
        $metaDataFactory = $this->get('ddr_rest.metadata.factory');
398
        /** @var ClassMetadata $classMetaData */
399 32
        $classMetaData = $metaDataFactory->getMetadataForClass($this->getEntityClass());
400
401 32
        return $classMetaData;
402
    }
403
404
    protected function getSubResourceEntityClass($subresource)
405
    {
406
        /** @var PropertyMetadata $propertyMetadata */
407
        $propertyMetadata = $this->getClassMetadata()->propertyMetadata[$subresource];
408
409
        return $propertyMetadata->getTargetClass();
410
    }
411
412
    protected function resolveSubject($entity, $propertyPath)
413
    {
414
        if ('this' === $propertyPath) {
415
            return $entity;
416
        }
417
        $propertyAccessor = $this->get('property_accessor');
418
419
        return $propertyAccessor->getValue($entity, $propertyPath);
420
    }
421
422
    /**
423
     * @param object $entity
424
     * @param Right  $right
425
     */
426 14
    protected function assertRightGranted($entity, Right $right)
427
    {
428 14
        $propertyPath = $right->propertyPath;
429 14
        if (null === $propertyPath) {
430 14
            $this->denyAccessUnlessGranted($right->attributes);
431
        } else {
432
            $subject = $this->resolveSubject($entity, $propertyPath);
433
            $this->denyAccessUnlessGranted($right->attributes, $subject);
434
        }
435 10
    }
436
437
    /**
438
     * @return string[]
439
     */
440
    protected function getSubresourceSerializationGroups($subresource)
441
    {
442
        return ['Default', 'ddr.rest.subresource', 'ddr.rest.' . $this->getShortName() . '.' . $subresource];
443
    }
444
445
    /**
446
     * @param string $subresource
447
     * @param object $entity
448
     *
449
     * @return
450
     */
451
    protected function saveSubResource($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...
452
    {
453
        return $this->getService()->save($entity);
0 ignored issues
show
Bug introduced by
The method save() 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...
454
    }
455
456
    /**
457
     * @return string|null
458
     */
459 6
    protected function getSubresource()
460
    {
461 6
        return $this->getCurrentRequest()->attributes->get('_subresource');
462
    }
463
464 18
    protected function parseIncludes(Request $request)
465
    {
466 18
        $includeString = $request->query->get('include');
467 18
        if (empty($includeString)) {
468 16
            return [];
469
        }
470
471 2
        return explode(',', $includeString);
472
    }
473
474
    private function parseConstraintViolations(ConstraintViolationListInterface $errors)
475
    {
476
        $data = [];
477
        /** @var ConstraintViolationInterface $error */
478
        foreach ($errors as $error) {
479
            $data[] = [
480
                'propertyPath' => $error->getPropertyPath(),
481
                'message'      => $error->getMessage(),
482
                'value'        => $error->getInvalidValue()
483
            ];
484
        }
485
486
        return $data;
487
    }
488
489 12
    private function addPaginationHeaders(Response $response, int $page, int $perPage, int $total)
490
    {
491 12
        $response->headers->add(
492
            [
493 12
                'x-pagination-current-page' => $page,
494 12
                'x-pagination-per-page'     => $perPage,
495 12
                'x-pagination-total'        => $total,
496 12
                'x-pagination-total-pages'  => (int)(($total - 1) / $perPage + 1)
497
            ]
498
        );
499 12
    }
500
}
501