Completed
Push — standalone ( 88d8ec...763a1b )
by Philip
04:53 queued 40s
created

RestResourceController::getAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
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\Right;
9
use Dontdrinkandroot\RestBundle\Metadata\ClassMetadata;
10
use Dontdrinkandroot\RestBundle\Metadata\PropertyMetadata;
11
use Dontdrinkandroot\Service\CrudServiceInterface;
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 RestResourceController 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
        $listResult = $this->listEntities($page, $perPage);
31
32 6
        $response = new JsonResponse();
33
34 6
        if ($listResult instanceof Paginator) {
35 6
            $entities = $listResult->getIterator()->getArrayCopy();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Traversable as the method getArrayCopy() does only exist in the following implementations of said interface: ArrayIterator, ArrayObject, DoctrineTest\Instantiato...tAsset\ArrayObjectAsset, DoctrineTest\Instantiato...lizableArrayObjectAsset, DoctrineTest\Instantiato...ceptionArrayObjectAsset, DoctrineTest\Instantiato...sset\WakeUpNoticesAsset, Issue523, RecursiveArrayIterator, Symfony\Component\Finder...rator\InnerNameIterator, Symfony\Component\Finder...rator\InnerSizeIterator, Symfony\Component\Finder...rator\InnerTypeIterator, Symfony\Component\Finder...or\MockFileListIterator, Symfony\Component\Proper...ss\PropertyPathIterator.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
36 6
            $total = $listResult->count();
37 6
            $this->addPaginationHeaders($response, $page, $perPage, $total);
38
        } else {
39
            $entities = $listResult;
40
        }
41
42 6
        $normalizer = $this->get('ddr_rest.normalizer');
43 6
        $content = $normalizer->normalize($entities, $this->parseIncludes($request));
44
45 6
        $response->setData($content);
46
47 6
        return $response;
48
    }
49
50 2
    public function postAction(Request $request)
51
    {
52 2
        $this->assertPostGranted();
53
        $entity = $this->parseRequest($request, null, $this->getEntityClass());
54
        $entity = $this->postProcessPostedEntity($entity);
55
56
        /** @var ValidatorInterface $validator */
57
        $validator = $this->get('validator');
58
        $errors = $validator->validate($entity);
59
        if ($errors->count() > 0) {
60
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
61
        }
62
63
        $entity = $this->createEntity($entity);
64
65
        $normalizer = $this->get('ddr_rest.normalizer');
66
        $content = $normalizer->normalize($entity);
67
68
        return new JsonResponse($content, Response::HTTP_CREATED);
69
    }
70
71 10
    public function getAction(Request $request, $id)
72
    {
73 10
        $entity = $this->fetchEntity($id);
74 10
        $this->assertGetGranted($entity);
75
76 8
        $normalizer = $this->get('ddr_rest.normalizer');
77 8
        $content = $normalizer->normalize($entity, $this->parseIncludes($request, ['details']));
78
79 8
        return new JsonResponse($content);
80
    }
81
82 6
    public function putAction(Request $request, $id)
83
    {
84 6
        $entity = $this->fetchEntity($id);
85 6
        $this->assertPutGranted($entity);
86 2
        $entity = $this->parseRequest($request, $entity, $this->getEntityClass());
87 2
        $entity = $this->postProcessPuttedEntity($entity);
88
89
        /** @var ValidatorInterface $validator */
90 2
        $validator = $this->get('validator');
91 2
        $errors = $validator->validate($entity);
92 2
        if ($errors->count() > 0) {
93
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
94
        }
95
96 2
        $entity = $this->updateEntity($entity);
97
98 2
        $normalizer = $this->get('ddr_rest.normalizer');
99 2
        $content = $normalizer->normalize($entity);
100
101 2
        return new JsonResponse($content);
102
    }
103
104 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...
105
    {
106 2
        $entity = $this->fetchEntity($id);
107 2
        $this->assertDeleteGranted($entity);
108
        $this->getService()->remove($entity);
109
110
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
111
    }
112
113 6
    public function listSubresourceAction(Request $request, $id)
114
    {
115 6
        $page = $request->query->get('page', 1);
116 6
        $perPage = $request->query->get('perPage', 50);
117
118 6
        $subresource = $this->getSubresource();
119 6
        $entity = $this->fetchEntity($id);
120 6
        $this->assertSubresourceListGranted($entity, $subresource);
121
122 6
        $listResult = $this->listSubresource(
123
            $entity,
124
            $subresource,
125
            $page,
126
            $perPage
127
        );
128
129 6
        $response = new JsonResponse();
130
131 6
        if ($listResult instanceof Paginator) {
132 6
            $entities = $listResult->getIterator()->getArrayCopy();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Traversable as the method getArrayCopy() does only exist in the following implementations of said interface: ArrayIterator, ArrayObject, DoctrineTest\Instantiato...tAsset\ArrayObjectAsset, DoctrineTest\Instantiato...lizableArrayObjectAsset, DoctrineTest\Instantiato...ceptionArrayObjectAsset, DoctrineTest\Instantiato...sset\WakeUpNoticesAsset, Issue523, RecursiveArrayIterator, Symfony\Component\Finder...rator\InnerNameIterator, Symfony\Component\Finder...rator\InnerSizeIterator, Symfony\Component\Finder...rator\InnerTypeIterator, Symfony\Component\Finder...or\MockFileListIterator, Symfony\Component\Proper...ss\PropertyPathIterator.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

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