Completed
Push — standalone ( a782f9...7d7a9d )
by Philip
04:00
created

getSubResourceEntityClass()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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