Completed
Push — master ( d6cf83...a2ccb4 )
by Philip
13:40
created

AbstractRestResourceController   C

Complexity

Total Complexity 39

Size/Duplication

Total Lines 468
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 19

Test Coverage

Coverage 92.76%

Importance

Changes 0
Metric Value
wmc 39
lcom 1
cbo 19
dl 0
loc 468
ccs 141
cts 152
cp 0.9276
rs 5.7824
c 0
b 0
f 0

42 Methods

Rating   Name   Duplication   Size   Complexity  
B listAction() 0 25 2
A postAction() 0 18 2
A getAction() 0 9 1
A putAction() 0 18 2
A deleteAction() 0 8 1
B listSubresourceAction() 0 26 2
A postSubresourceAction() 0 23 2
A putSubresourceAction() 0 8 1
A deleteSubresourceAction() 0 8 1
A postProcessPostedEntity() 0 4 1
A postProcessPuttedEntity() 0 4 1
A postProcessSubResourcePostedEntity() 0 4 1
A getEntityClass() 0 4 1
A getSubResourceEntityClass() 0 7 1
A getCurrentRequest() 0 4 1
A assertMethodGranted() 0 7 3
A assertSubResourceMethodGranted() 0 10 2
A getClassMetadata() 0 8 1
A resolveSubject() 0 9 2
A assertRightGranted() 0 10 3
A parseIncludes() 0 16 3
A denyAccessUnlessGranted() 0 6 2
A parseConstraintViolations() 0 14 2
A addPaginationHeaders() 0 11 1
listEntities() 0 1 ?
fetchEntity() 0 1 ?
createEntity() 0 1 ?
updateEntity() 0 1 ?
removeEntity() 0 1 ?
listSubresource() 0 1 ?
createAssociation() 0 1 ?
createSubResource() 0 1 ?
addAssociation() 0 1 ?
removeAssociation() 0 1 ?
getNormalizer() 0 1 ?
getValidator() 0 1 ?
getRequestParser() 0 1 ?
getRequestStack() 0 1 ?
getMetadataFactory() 0 1 ?
getPropertyAccessor() 0 1 ?
getAuthorizationChecker() 0 1 ?
getEntityManager() 0 1 ?
1
<?php
2
3
namespace Dontdrinkandroot\RestBundle\Controller;
4
5
use Doctrine\ORM\EntityManagerInterface;
6
use Doctrine\ORM\Tools\Pagination\Paginator;
7
use Dontdrinkandroot\RestBundle\Metadata\Annotation\Method;
8
use Dontdrinkandroot\RestBundle\Metadata\Annotation\Right;
9
use Dontdrinkandroot\RestBundle\Metadata\ClassMetadata;
10
use Dontdrinkandroot\RestBundle\Metadata\PropertyMetadata;
11
use Dontdrinkandroot\RestBundle\Service\Normalizer;
12
use Dontdrinkandroot\RestBundle\Service\RestRequestParser;
13
use Metadata\MetadataFactoryInterface;
14
use Symfony\Component\HttpFoundation\JsonResponse;
15
use Symfony\Component\HttpFoundation\Request;
16
use Symfony\Component\HttpFoundation\RequestStack;
17
use Symfony\Component\HttpFoundation\Response;
18
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
19
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
20
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
21
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
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
     * {@inheritdoc}
33
     */
34 10
    public function listAction(Request $request)
35
    {
36 10
        $page = $request->query->get('page', 1);
37 10
        $perPage = $request->query->get('perPage', 50);
38
39 10
        $this->assertMethodGranted(Method::LIST);
40
41 6
        $listResult = $this->listEntities($page, $perPage);
42
43 6
        $response = new JsonResponse();
44
45 6
        if ($listResult instanceof Paginator) {
46 6
            $entities = iterator_to_array($listResult->getIterator());
47 6
            $total = $listResult->count();
48 6
            $this->addPaginationHeaders($response, $page, $perPage, $total);
49
        } else {
50
            $entities = $listResult;
51
        }
52
53 6
        $content = $this->getNormalizer()->normalize($entities, $this->parseIncludes($request));
54
55 6
        $response->setData($content);
56
57 6
        return $response;
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 14
    public function postAction(Request $request)
64
    {
65 14
        $this->assertMethodGranted(Method::POST);
66
67 12
        $entity = $this->getRequestParser()->parseEntity($request, $this->getEntityClass());
68 12
        $entity = $this->postProcessPostedEntity($entity);
69
70 12
        $errors = $this->getValidator()->validate($entity);
71 12
        if ($errors->count() > 0) {
72 2
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
73
        }
74
75 10
        $entity = $this->createEntity($entity);
76
77 10
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
78
79 10
        return new JsonResponse($content, Response::HTTP_CREATED);
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85 32
    public function getAction(Request $request, $id)
86
    {
87 32
        $entity = $this->fetchEntity($id);
88 30
        $this->assertMethodGranted(Method::GET, $entity);
89
90 28
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
91
92 28
        return new JsonResponse($content);
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 12
    public function putAction(Request $request, $id)
99
    {
100 12
        $entity = $this->fetchEntity($id);
101 12
        $this->assertMethodGranted(Method::PUT, $entity);
102 10
        $entity = $this->getRequestParser()->parseEntity($request, $this->getEntityClass(), $entity);
103 10
        $entity = $this->postProcessPuttedEntity($entity);
104
105 10
        $errors = $this->getValidator()->validate($entity);
106 10
        if ($errors->count() > 0) {
107
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
108
        }
109
110 10
        $entity = $this->updateEntity($entity);
111
112 10
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
113
114 10
        return new JsonResponse($content);
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 4
    public function deleteAction(Request $request, $id)
121
    {
122 4
        $entity = $this->fetchEntity($id);
123 4
        $this->assertMethodGranted(Method::DELETE, $entity);
124 2
        $this->removeEntity($entity);
125
126 2
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     */
132 6
    public function listSubresourceAction(Request $request, $id, string $subresource)
133
    {
134 6
        $page = $request->query->get('page', 1);
135 6
        $perPage = $request->query->get('perPage', 50);
136
137 6
        $entity = $this->fetchEntity($id);
138 6
        $this->assertSubResourceMethodGranted(Method::LIST, $entity, $subresource);
139
140 6
        $listResult = $this->listSubresource($entity, $subresource, $page, $perPage);
141
142 6
        $response = new JsonResponse();
143
144 6
        if ($listResult instanceof Paginator) {
145 6
            $entities = iterator_to_array($listResult->getIterator());
146 6
            $total = $listResult->count();
147 6
            $this->addPaginationHeaders($response, $page, $perPage, $total);
148
        } else {
149
            $entities = $listResult;
150
        }
151
152 6
        $content = $this->getNormalizer()->normalize($entities, $this->parseIncludes($request));
153
154 6
        $response->setData($content);
155
156 6
        return $response;
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 4
    public function postSubresourceAction(Request $request, $id, string $subresource)
163
    {
164 4
        $parent = $this->fetchEntity($id);
165 4
        $this->assertSubResourceMethodGranted(Method::POST, $parent, $subresource);
166
167 2
        $restRequestParser = $this->getRequestParser();
168 2
        $entity = $this->createAssociation($parent, $subresource);
169 2
        $entity = $restRequestParser->parseEntity($request, $this->getSubResourceEntityClass($subresource), $entity);
170
171 2
        $entity = $this->postProcessSubResourcePostedEntity($parent, $subresource, $entity);
172
173 2
        $errors = $this->getValidator()->validate($entity);
174
175 2
        if ($errors->count() > 0) {
176
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
177
        }
178
179 2
        $entity = $this->createSubResource($parent, $subresource, $entity);
180
181 2
        $content = $this->getNormalizer()->normalize($entity, $this->parseIncludes($request));
182
183 2
        return new JsonResponse($content, Response::HTTP_CREATED);
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     */
189 12
    public function putSubresourceAction(Request $request, $id, string $subresource, $subId)
190
    {
191 12
        $parent = $this->fetchEntity($id);
192 12
        $this->assertSubResourceMethodGranted(Method::PUT, $parent, $subresource);
193 12
        $this->addAssociation($parent, $subresource, $subId);
194
195 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 12
    public function deleteSubresourceAction(Request $request, $id, string $subresource, $subId = null)
202
    {
203 12
        $parent = $this->fetchEntity($id);
204 12
        $this->assertSubResourceMethodGranted(Method::DELETE, $parent, $subresource);
205 12
        $this->removeAssociation($parent, $subresource, $subId);
206
207 12
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
208
    }
209
210
    /**
211
     * @param object $entity
212
     *
213
     * @return object
214
     */
215 12
    protected function postProcessPostedEntity($entity)
216
    {
217 12
        return $entity;
218
    }
219
220
    /**
221
     * @param object $entity
222
     *
223
     * @return object
224
     */
225 10
    protected function postProcessPuttedEntity($entity)
226
    {
227 10
        return $entity;
228
    }
229
230
    /**
231
     * @param object $parent
232
     * @param string $subresource
233
     * @param object $entity
234
     *
235
     * @return object
236
     */
237 2
    protected function postProcessSubResourcePostedEntity($parent, $subresource, $entity)
238
    {
239 2
        return $entity;
240
    }
241
242 80
    protected function getEntityClass()
243
    {
244 80
        return $this->getCurrentRequest()->attributes->get('_entityClass');
245
    }
246
247 2
    protected function getSubResourceEntityClass($subresource)
248
    {
249
        /** @var PropertyMetadata $propertyMetadata */
250 2
        $propertyMetadata = $this->getClassMetadata()->propertyMetadata[$subresource];
251
252 2
        return $propertyMetadata->getType();
253
    }
254
255 80
    protected function getCurrentRequest()
256
    {
257 80
        return $this->getRequestStack()->getCurrentRequest();
258
    }
259
260 70
    protected function assertMethodGranted(string $methodName, $entity = null)
261
    {
262 70
        $method = $this->getClassMetadata()->getMethod($methodName);
263 70
        if ($method !== null && null !== $right = $method->right) {
264 36
            $this->assertRightGranted($right, $entity);
265
        }
266 58
    }
267
268
    /**
269
     * @param string $methodName
270
     * @param object $entity
271
     * @param string $subresource
272
     */
273 30
    protected function assertSubResourceMethodGranted($methodName, $entity, string $subresource): void
274
    {
275 30
        $classMetadata = $this->getClassMetadata();
276
        /** @var PropertyMetadata $propertyMetadata */
277 30
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
278 30
        $method = $propertyMetadata->getMethod($methodName);
279 30
        if (null !== $right = $method->right) {
280 14
            $this->assertRightGranted($right, $entity);
281
        }
282 28
    }
283
284
    /**
285
     * @return ClassMetadata
286
     */
287 80
    protected function getClassMetadata()
288
    {
289 80
        $metaDataFactory = $this->getMetadataFactory();
290
        /** @var ClassMetadata $classMetaData */
291 80
        $classMetaData = $metaDataFactory->getMetadataForClass($this->getEntityClass());
292
293 80
        return $classMetaData;
294
    }
295
296
    protected function resolveSubject($entity, $propertyPath)
297
    {
298
        if ('this' === $propertyPath) {
299
            return $entity;
300
        }
301
        $propertyAccessor = $this->getPropertyAccessor();
302
303
        return $propertyAccessor->getValue($entity, $propertyPath);
304
    }
305
306
    /**
307
     * @param Right  $right
308
     * @param object $entity
309
     */
310 46
    protected function assertRightGranted(Right $right, $entity = null)
311
    {
312 46
        $propertyPath = $right->propertyPath;
313 46
        if (null === $propertyPath || null == $entity) {
314 46
            $this->denyAccessUnlessGranted($right->attributes);
315
        } else {
316
            $subject = $this->resolveSubject($entity, $propertyPath);
317
            $this->denyAccessUnlessGranted($right->attributes, $subject);
318
        }
319 32
    }
320
321 62
    protected function parseIncludes(Request $request)
322
    {
323 62
        $defaultIncludes = $request->attributes->get('_defaultincludes');
324 62
        if (null == $defaultIncludes) {
325 30
            $defaultIncludes = [];
326
        }
327
328 62
        $includeString = $request->query->get('include');
329 62
        if (empty($includeString)) {
330 48
            $includes = [];
331
        } else {
332 16
            $includes = explode(',', $includeString);
333
        }
334
335 62
        return array_merge($defaultIncludes, $includes);
336
    }
337
338 46
    protected function denyAccessUnlessGranted($attributes, $object = null, $message = 'Access Denied.')
339
    {
340 46
        if (!$this->getAuthorizationChecker()->isGranted($attributes, $object)) {
341 14
            throw new AccessDeniedException($message);
342
        }
343 32
    }
344
345 2
    protected function parseConstraintViolations(ConstraintViolationListInterface $errors)
346
    {
347 2
        $data = [];
348
        /** @var ConstraintViolationInterface $error */
349 2
        foreach ($errors as $error) {
350 2
            $data[] = [
351 2
                'propertyPath' => $error->getPropertyPath(),
352 2
                'message'      => $error->getMessage(),
353 2
                'value'        => $error->getInvalidValue()
354
            ];
355
        }
356
357 2
        return $data;
358
    }
359
360 12
    protected function addPaginationHeaders(Response $response, int $page, int $perPage, int $total)
361
    {
362 12
        $response->headers->add(
363
            [
364 12
                'x-pagination-current-page' => $page,
365 12
                'x-pagination-per-page'     => $perPage,
366 12
                'x-pagination-total'        => $total,
367 12
                'x-pagination-total-pages'  => (int)(($total - 1) / $perPage + 1)
368
            ]
369
        );
370 12
    }
371
372
    /**
373
     * @param int $page
374
     * @param int $perPage
375
     *
376
     * @return Paginator|array
377
     */
378
    abstract protected function listEntities(int $page = 1, int $perPage = 50);
379
380
    /**
381
     * @param int|string $id
382
     *
383
     * @return object
384
     *
385
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
386
     */
387
    abstract protected function fetchEntity($id);
388
389
    /**
390
     * @param object $entity
391
     *
392
     * @return object
393
     */
394
    abstract protected function createEntity($entity);
395
396
    /**
397
     * @param object $entity
398
     *
399
     * @return object
400
     *
401
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
402
     */
403
    abstract protected function updateEntity($entity);
404
405
    /**
406
     * @param $entity
407
     *
408
     * @throws NotFoundHttpException Thrown if entity with the given id could not be found.
409
     */
410
    abstract protected function removeEntity($entity);
411
412
    /**
413
     * @param object $entity
414
     * @param string $property
415
     * @param int    $page
416
     * @param int    $perPage
417
     *
418
     * @return Paginator|array
419
     */
420
    abstract protected function listSubresource($entity, string $property, int $page = 1, int $perPage = 50);
421
422
    /**
423
     * @param object $parent
424
     * @param string $subresource
425
     *
426
     * @return object
427
     */
428
    abstract protected function createAssociation($parent, string $subresource);
429
430
    /**
431
     * @param object $parent
432
     * @param string $subresource
433
     * @param object $entity
434
     *
435
     * @return object
436
     */
437
    abstract protected function createSubResource($parent, $subresource, $entity);
438
439
    /**
440
     * @param object     $parent
441
     * @param string     $subresource
442
     * @param int|string $subId
443
     *
444
     * @return object
445
     */
446
    abstract protected function addAssociation($parent, string $subresource, $subId);
447
448
    /**
449
     * @param object          $parent
450
     * @param string          $subresource
451
     * @param int|string|null $subId
452
     *
453
     * @return mixed
454
     */
455
    abstract protected function removeAssociation($parent, string $subresource, $subId = null);
456
457
    /**
458
     * @return Normalizer
459
     */
460
    abstract protected function getNormalizer();
461
462
    /**
463
     * @return ValidatorInterface
464
     */
465
    abstract protected function getValidator();
466
467
    /**
468
     * @return RestRequestParser
469
     */
470
    abstract protected function getRequestParser();
471
472
    /**
473
     * @return RequestStack
474
     */
475
    abstract protected function getRequestStack();
476
477
    /**
478
     * @return MetadataFactoryInterface
479
     */
480
    abstract protected function getMetadataFactory();
481
482
    /**
483
     * @return PropertyAccessorInterface
484
     */
485
    abstract protected function getPropertyAccessor();
486
487
    /**
488
     * @return AuthorizationCheckerInterface
489
     */
490
    abstract protected function getAuthorizationChecker();
491
492
    /**
493
     * @return EntityManagerInterface
494
     */
495
    abstract protected function getEntityManager();
496
}
497