Completed
Push — standalone ( be1b50...88d8ec )
by Philip
04:15 queued 01:02
created

RestResourceController::saveSubResource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
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\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 4
    public function listAction(Request $request)
24
    {
25 4
        $page = $request->query->get('page', 1);
26 4
        $perPage = $request->query->get('perPage', 50);
27
28 4
        $this->assertListGranted();
29
30 3
        $listResult = $this->listEntities($page, $perPage);
31
32 3
        $response = new JsonResponse();
33
34 3
        if ($listResult instanceof Paginator) {
35 3
            $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 3
            $total = $listResult->count();
37 3
            $this->addPaginationHeaders($response, $page, $perPage, $total);
38
        } else {
39
            $entities = $listResult;
40
        }
41
42 3
        $normalizer = $this->get('ddr_rest.normalizer');
43 3
        $content = $normalizer->normalize($entities, $this->parseIncludes($request));
44
45 3
        $response->setData($content);
46
47 3
        return $response;
48
    }
49
50 1
    public function postAction(Request $request)
51
    {
52 1
        $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 5
    public function getAction(Request $request, $id)
72
    {
73 5
        $entity = $this->fetchEntity($id);
74 5
        $this->assertGetGranted($entity);
75
76 4
        $normalizer = $this->get('ddr_rest.normalizer');
77 4
        $content = $normalizer->normalize($entity, $this->parseIncludes($request, ['details']));
78
79 4
        return new JsonResponse($content);
80
    }
81
82 3
    public function putAction(Request $request, $id)
83
    {
84 3
        $entity = $this->fetchEntity($id);
85 3
        $this->assertPutGranted($entity);
86 1
        $entity = $this->parseRequest($request, $entity, $this->getEntityClass());
87 1
        $entity = $this->postProcessPuttedEntity($entity);
88
89
        /** @var ValidatorInterface $validator */
90 1
        $validator = $this->get('validator');
91 1
        $errors = $validator->validate($entity);
92 1
        if ($errors->count() > 0) {
93
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
94
        }
95
96 1
        $entity = $this->updateEntity($entity);
97
98 1
        $normalizer = $this->get('ddr_rest.normalizer');
99 1
        $content = $normalizer->normalize($entity);
100
101 1
        return new JsonResponse($content);
102
    }
103
104 1
    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 1
        $entity = $this->fetchEntity($id);
107 1
        $this->assertDeleteGranted($entity);
108
        $this->getService()->remove($entity);
109
110
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
111
    }
112
113 3
    public function listSubresourceAction(Request $request, $id)
114
    {
115 3
        $page = $request->query->get('page', 1);
116 3
        $perPage = $request->query->get('perPage', 50);
117
118 3
        $subresource = $this->getSubresource();
119 3
        $entity = $this->fetchEntity($id);
120 3
        $this->assertSubresourceListGranted($entity, $subresource);
121
122 3
        $listResult = $this->listSubresource(
123
            $entity,
124
            $subresource,
125
            $page,
126
            $perPage
127
        );
128
129 3
        $response = new JsonResponse();
130
131 3
        if ($listResult instanceof Paginator) {
132 3
            $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 3
            $total = $listResult->count();
134 3
            $this->addPaginationHeaders($response, $page, $perPage, $total);
135
        } else {
136
            $entities = $listResult;
137
        }
138
139 3
        $normalizer = $this->get('ddr_rest.normalizer');
140 3
        $content = $normalizer->normalize($entities, $this->parseIncludes($request));
141
142 3
        $response->setData($content);
143
144 3
        return $response;
145
    }
146
147 2
    public function postSubresourceAction(Request $request, $id)
148
    {
149 2
        $subresource = $this->getSubresource();
150 2
        $parent = $this->fetchEntity($id);
151 2
        $this->assertSubresourcePostGranted($parent, $subresource);
152 1
        $entity = $this->parseRequest($request, null, $this->getSubResourceEntityClass($subresource));
153 1
        $entity = $this->postProcessSubResourcePostedEntity($subresource, $entity, $parent);
154
155
        /** @var ValidatorInterface $validator */
156 1
        $validator = $this->get('validator');
157 1
        $errors = $validator->validate($entity);
158
159 1
        if ($errors->count() > 0) {
160
            return new JsonResponse($this->parseConstraintViolations($errors), Response::HTTP_BAD_REQUEST);
161
        }
162
163 1
        $entity = $this->createSubResource($parent, $subresource, $entity);
164
165 1
        $normalizer = $this->get('ddr_rest.normalizer');
166 1
        $content = $normalizer->normalize($entity, ['details']);
167
168 1
        return new JsonResponse($content, Response::HTTP_CREATED);
169
    }
170
171 1
    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 1
        $subresource = $this->getSubresource();
174 1
        $parent = $this->fetchEntity($id);
175 1
        $this->assertSubresourcePutGranted($parent, $subresource);
176 1
        $this->getService()->addToCollection($parent, $subresource, $subId);
177
178 1
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
179
    }
180
181 1
    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 1
        $subresource = $this->getSubresource();
184 1
        $parent = $this->fetchEntity($id);
185 1
        $this->assertSubresourceDeleteGranted($parent, $subresource);
186 1
        $this->getService()->removeFromCollection($parent, $subresource, $subId);
187
188 1
        return new JsonResponse(null, Response::HTTP_NO_CONTENT);
189
    }
190
191
    /**
192
     * @return CrudServiceInterface
193
     */
194 17
    protected function getService(): CrudServiceInterface
195
    {
196 17
        $serviceId = $this->getServiceId();
197 17
        if (null === $serviceId) {
198 16
            $entityClass = $this->getEntityClass();
199 16
            if (null === $entityClass) {
200
                throw new \RuntimeException('No service or entity class given');
201
            }
202
            /** @var EntityManagerInterface $entityManager */
203 16
            $entityManager = $this->get('doctrine.orm.entity_manager');
204 16
            $repository = $entityManager->getRepository($entityClass);
205 16
            if (!$repository instanceof CrudServiceInterface) {
206
                throw new \RuntimeException(
207
                    'Your Entity Repository needs to be an instance of ' . CrudServiceInterface::class . '.'
208
                );
209
            }
210
211 16
            return $repository;
212
        } else {
213
            /** @var CrudServiceInterface $service */
214 1
            $service = $this->get($serviceId);
215
216 1
            return $service;
217
        }
218
    }
219
220 2
    protected function parseRequest(Request $request, $entity = null, $entityClass = null)
221
    {
222 2
        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 1
    protected function postProcessPuttedEntity($entity)
241
    {
242 1
        return $entity;
243
    }
244
245
    /**
246
     * @param string $subresource
247
     * @param object $parent
248
     * @param object $entity
249
     *
250
     * @return object
251
     */
252 1
    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 1
        return $entity;
255
    }
256
257 14
    protected function fetchEntity($id)
258
    {
259 14
        $entity = $this->getService()->find($id);
260 14
        if (null === $entity) {
261
            throw new NotFoundHttpException();
262
        }
263
264 14
        return $entity;
265
    }
266
267
    /**
268
     * @param int $page
269
     * @param int $perPage
270
     *
271
     * @return Paginator|array
272
     */
273 3
    protected function listEntities(int $page = 1, int $perPage = 50)
274
    {
275 3
        return $this->getService()->findAllPaginated($page, $perPage);
0 ignored issues
show
Bug introduced by
The method findAllPaginated() 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...
276
    }
277
278
    protected function createEntity($entity)
279
    {
280
        return $this->getService()->create($entity);
281
    }
282
283 1
    protected function updateEntity($entity)
284
    {
285 1
        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 3
    protected function listSubresource($entity, string $property, int $page = 1, int $perPage = 50)
297
    {
298 3
        return $this->getService()->findAssociationPaginated($entity, $property, $page, $perPage);
0 ignored issues
show
Bug introduced by
The method findAssociationPaginated() 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...
299
    }
300
301 19
    protected function getEntityClass()
302
    {
303 19
        return $this->getCurrentRequest()->attributes->get('_entityClass');
304
    }
305
306
    protected function getShortName()
307
    {
308
        return Inflector::tableize($this->getClassMetadata()->reflection->getShortName());
309
    }
310
311 17
    protected function getServiceId()
312
    {
313 17
        return $this->getCurrentRequest()->attributes->get('_service');
314
    }
315
316 19
    protected function getCurrentRequest()
317
    {
318 19
        return $this->get('request_stack')->getCurrentRequest();
319
    }
320
321 4
    protected function assertListGranted()
322
    {
323 4
        $classMetadata = $this->getClassMetadata();
324 4
        $right = $classMetadata->getListRight();
325 4
        if (null === $right) {
326 2
            return;
327
        }
328
329 2
        $this->denyAccessUnlessGranted($right->attributes);
330 1
    }
331
332 1
    protected function assertPostGranted()
333
    {
334 1
        $classMetadata = $this->getClassMetadata();
335 1
        $right = $classMetadata->getPostRight();
336 1
        if (null === $right) {
337 1
            throw $this->createAccessDeniedException();
338
        }
339
340
        $this->denyAccessUnlessGranted($right->attributes);
341
    }
342
343 5
    protected function assertGetGranted($entity)
344
    {
345 5
        $classMetadata = $this->getClassMetadata();
346 5
        $right = $classMetadata->getGetRight();
347 5
        if (null === $right) {
348 2
            return;
349
        }
350
351 3
        $this->assertRightGranted($entity, $right);
352 2
    }
353
354 3
    protected function assertPutGranted($entity)
355
    {
356 3
        $classMetadata = $this->getClassMetadata();
357 3
        $right = $classMetadata->getPutRight();
358 3
        if (null === $right) {
359 1
            throw $this->createAccessDeniedException();
360
        }
361
362 2
        $this->assertRightGranted($entity, $right);
363 1
    }
364
365 1
    protected function assertDeleteGranted($entity)
366
    {
367 1
        $classMetadata = $this->getClassMetadata();
368 1
        $right = $classMetadata->getDeleteRight();
369 1
        if (null === $right) {
370 1
            throw $this->createAccessDeniedException();
371
        }
372
373
        $this->assertRightGranted($entity, $right);
374
    }
375
376 3
    protected function assertSubresourceListGranted($entity, $subresource)
377
    {
378 3
        $classMetadata = $this->getClassMetadata();
379
        /** @var PropertyMetadata $propertyMetadata */
380 3
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
381 3
        $right = $propertyMetadata->getSubResourceListRight();
382 3
        if (null === $right) {
383 3
            return;
384
        }
385
386
        $this->assertRightGranted($entity, $right);
387
    }
388
389 2
    protected function assertSubresourcePostGranted($entity, $subresource)
390
    {
391 2
        $classMetadata = $this->getClassMetadata();
392
        /** @var PropertyMetadata $propertyMetadata */
393 2
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
394 2
        $right = $propertyMetadata->getSubResourcePostRight();
395 2
        if (null === $right) {
396
            throw $this->createAccessDeniedException();
397
        }
398
399 2
        $this->assertRightGranted($entity, $right);
400 1
    }
401
402 1
    protected function assertSubresourcePutGranted($entity, $subresource)
403
    {
404 1
        $classMetadata = $this->getClassMetadata();
405
        /** @var PropertyMetadata $propertyMetadata */
406 1
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
407 1
        $right = $propertyMetadata->getSubResourcePutRight();
408 1
        if (null === $right) {
409
            throw $this->createAccessDeniedException();
410
        }
411
412 1
        $this->assertRightGranted($entity, $right);
413 1
    }
414
415 1
    protected function assertSubresourceDeleteGranted($entity, $subresource)
416
    {
417 1
        $classMetadata = $this->getClassMetadata();
418
        /** @var PropertyMetadata $propertyMetadata */
419 1
        $propertyMetadata = $classMetadata->propertyMetadata[$subresource];
420 1
        $right = $propertyMetadata->getSubResourceDeleteRight();
421 1
        if (null === $right) {
422
            throw $this->createAccessDeniedException();
423
        }
424
425 1
        $this->assertRightGranted($entity, $right);
426 1
    }
427
428
    /**
429
     * @return ClassMetadata
430
     */
431 19
    protected function getClassMetadata()
432
    {
433 19
        $metaDataFactory = $this->get('ddr_rest.metadata.factory');
434
        /** @var ClassMetadata $classMetaData */
435 19
        $classMetaData = $metaDataFactory->getMetadataForClass($this->getEntityClass());
436
437 19
        return $classMetaData;
438
    }
439
440 1
    protected function getSubResourceEntityClass($subresource)
441
    {
442
        /** @var PropertyMetadata $propertyMetadata */
443 1
        $propertyMetadata = $this->getClassMetadata()->propertyMetadata[$subresource];
444
445 1
        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 9
    protected function assertRightGranted($entity, Right $right)
463
    {
464 9
        $propertyPath = $right->propertyPath;
465 9
        if (null === $propertyPath) {
466 9
            $this->denyAccessUnlessGranted($right->attributes);
467
        } else {
468
            $subject = $this->resolveSubject($entity, $propertyPath);
469
            $this->denyAccessUnlessGranted($right->attributes, $subject);
470
        }
471 6
    }
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 1
    protected function createSubResource($parent, $subresource, $entity)
489
    {
490 1
        return $this->getService()->createAssociation($parent, $subresource, $entity);
0 ignored issues
show
Bug introduced by
The method createAssociation() does not exist on Dontdrinkandroot\Service\CrudServiceInterface. Did you maybe mean create()?

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...
491
    }
492
493
    /**
494
     * @return string|null
495
     */
496 5
    protected function getSubresource()
497
    {
498 5
        return $this->getCurrentRequest()->attributes->get('_subresource');
499
    }
500
501 10
    protected function parseIncludes(Request $request, array $defaultIncludes = [])
502
    {
503 10
        $includeString = $request->query->get('include');
504 10
        if (empty($includeString)) {
505 9
            return $defaultIncludes;
506
        }
507
508 1
        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 6
    private function addPaginationHeaders(Response $response, int $page, int $perPage, int $total)
527
    {
528 6
        $response->headers->add(
529
            [
530 6
                'x-pagination-current-page' => $page,
531 6
                'x-pagination-per-page'     => $perPage,
532 6
                'x-pagination-total'        => $total,
533 6
                'x-pagination-total-pages'  => (int)(($total - 1) / $perPage + 1)
534
            ]
535
        );
536 6
    }
537
}
538