Completed
Push — master ( bb6905...021686 )
by Grégoire
18s queued 12s
created

src/Controller/Api/GalleryController.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\MediaBundle\Controller\Api;
15
16
use FOS\RestBundle\Context\Context;
17
use FOS\RestBundle\Controller\Annotations\QueryParam;
18
use FOS\RestBundle\Controller\Annotations\View;
19
use FOS\RestBundle\Request\ParamFetcherInterface;
20
use FOS\RestBundle\View\View as FOSRestView;
21
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
22
use Sonata\DatagridBundle\Pager\PagerInterface;
23
use Sonata\MediaBundle\Model\GalleryInterface;
24
use Sonata\MediaBundle\Model\GalleryItemInterface;
25
use Sonata\MediaBundle\Model\GalleryManagerInterface;
26
use Sonata\MediaBundle\Model\MediaInterface;
27
use Sonata\MediaBundle\Model\MediaManagerInterface;
28
use Symfony\Component\Form\FormFactoryInterface;
29
use Symfony\Component\Form\FormInterface;
30
use Symfony\Component\HttpFoundation\Request;
31
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
32
33
/**
34
 * @final since sonata-project/media-bundle 3.21.0
35
 *
36
 * @author Hugo Briand <[email protected]>
37
 */
38
class GalleryController
39
{
40
    /**
41
     * @var GalleryManagerInterface
42
     */
43
    protected $galleryManager;
44
45
    /**
46
     * @var MediaManagerInterface
47
     */
48
    protected $mediaManager;
49
50
    /**
51
     * @var FormFactoryInterface
52
     */
53
    protected $formFactory;
54
55
    /**
56
     * @var string
57
     */
58
    protected $galleryItemClass;
59
60
    /**
61
     * Constructor.
62
     *
63
     * @param string $galleryItemClass
64
     */
65
    public function __construct(GalleryManagerInterface $galleryManager, MediaManagerInterface $mediaManager, FormFactoryInterface $formFactory, $galleryItemClass)
66
    {
67
        $this->galleryManager = $galleryManager;
68
        $this->mediaManager = $mediaManager;
69
        $this->formFactory = $formFactory;
70
        $this->galleryItemClass = $galleryItemClass;
71
    }
72
73
    /**
74
     * Retrieves the list of galleries (paginated).
75
     *
76
     * @ApiDoc(
77
     *  resource=true,
78
     *  output={"class"="Sonata\DatagridBundle\Pager\PagerInterface", "groups"={"sonata_api_read"}}
79
     * )
80
     *
81
     * @QueryParam(
82
     *     name="page",
83
     *     requirements="\d+",
84
     *     default="1",
85
     *     description="Page for gallery list pagination"
86
     * )
87
     * @QueryParam(
88
     *     name="count",
89
     *     requirements="\d+",
90
     *     default="10",
91
     *     description="Number of galleries by page"
92
     * )
93
     * @QueryParam(
94
     *     name="enabled",
95
     *     requirements="0|1",
96
     *     nullable=true,
97
     *     strict=true,
98
     *     description="Enabled/Disabled galleries filter"
99
     * )
100
     * @QueryParam(
101
     *     name="orderBy",
102
     *     map=true,
103
     *     requirements="ASC|DESC",
104
     *     nullable=true,
105
     *     strict=true,
106
     *     description="Order by array (key is field, value is direction)"
107
     * )
108
     *
109
     * @View(serializerGroups={"sonata_api_read"}, serializerEnableMaxDepthChecks=true)
110
     *
111
     * @return PagerInterface
112
     */
113
    public function getGalleriesAction(ParamFetcherInterface $paramFetcher)
114
    {
115
        $supportedCriteria = [
116
            'enabled' => '',
117
        ];
118
119
        $page = $paramFetcher->get('page');
120
        $limit = $paramFetcher->get('count');
121
        $sort = $paramFetcher->get('orderBy');
122
        $criteria = array_intersect_key($paramFetcher->all(), $supportedCriteria);
123
124
        foreach ($criteria as $key => $value) {
125
            if (null === $value) {
126
                unset($criteria[$key]);
127
            }
128
        }
129
130
        if (!$sort) {
131
            $sort = [];
132
        } elseif (!\is_array($sort)) {
133
            $sort = [$sort => 'asc'];
134
        }
135
136
        return $this->getGalleryManager()->getPager($criteria, $page, $limit, $sort);
137
    }
138
139
    /**
140
     * Retrieves a specific gallery.
141
     *
142
     * @ApiDoc(
143
     *  requirements={
144
     *      {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="gallery id"}
145
     *  },
146
     *  output={"class"="sonata_media_api_form_gallery", "groups"={"sonata_api_read"}},
147
     *  statusCodes={
148
     *      200="Returned when successful",
149
     *      404="Returned when gallery is not found"
150
     *  }
151
     * )
152
     *
153
     * @View(serializerGroups={"sonata_api_read"}, serializerEnableMaxDepthChecks=true)
154
     *
155
     * @param $id
156
     *
157
     * @return GalleryInterface
158
     */
159
    public function getGalleryAction($id)
160
    {
161
        return $this->getGallery($id);
162
    }
163
164
    /**
165
     * Retrieves the medias of specified gallery.
166
     *
167
     * @ApiDoc(
168
     *  requirements={
169
     *      {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="gallery id"}
170
     *  },
171
     *  output={"class"="Sonata\MediaBundle\Model\Media", "groups"={"sonata_api_read"}},
172
     *  statusCodes={
173
     *      200="Returned when successful",
174
     *      404="Returned when gallery is not found"
175
     *  }
176
     * )
177
     *
178
     * @View(serializerGroups={"sonata_api_read"}, serializerEnableMaxDepthChecks=true)
179
     *
180
     * @param $id
181
     *
182
     * @return MediaInterface[]
183
     */
184
    public function getGalleryMediasAction($id)
185
    {
186
        $galleryItems = $this->getGallery($id)->getGalleryItems();
187
188
        $media = [];
189
        foreach ($galleryItems as $galleryItem) {
190
            $media[] = $galleryItem->getMedia();
191
        }
192
193
        return $media;
194
    }
195
196
    /**
197
     * Retrieves the gallery items of specified gallery.
198
     *
199
     * @ApiDoc(
200
     *  requirements={
201
     *      {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="gallery id"}
202
     *  },
203
     *  output={"class"="Sonata\MediaBundle\Model\GalleryItem", "groups"={"sonata_api_read"}},
204
     *  statusCodes={
205
     *      200="Returned when successful",
206
     *      404="Returned when gallery is not found"
207
     *  }
208
     * )
209
     *
210
     * @View(serializerGroups={"sonata_api_read"}, serializerEnableMaxDepthChecks=true)
211
     *
212
     * @param $id
213
     *
214
     * @return GalleryItemInterface[]
215
     */
216
    public function getGalleryGalleryItemAction($id)
217
    {
218
        return $this->getGallery($id)->getGalleryItems();
219
    }
220
221
    /**
222
     * Adds a gallery.
223
     *
224
     * @ApiDoc(
225
     *  input={"class"="sonata_media_api_form_gallery", "name"="", "groups"={"sonata_api_write"}},
226
     *  output={"class"="sonata_media_api_form_gallery", "groups"={"sonata_api_read"}},
227
     *  statusCodes={
228
     *      200="Returned when successful",
229
     *      400="Returned when an error has occurred while gallery creation",
230
     *  }
231
     * )
232
     *
233
     * @param Request $request A Symfony request
234
     *
235
     * @throws NotFoundHttpException
236
     *
237
     * @return GalleryInterface
238
     */
239
    public function postGalleryAction(Request $request)
240
    {
241
        return $this->handleWriteGallery($request);
242
    }
243
244
    /**
245
     * Updates a gallery.
246
     *
247
     * @ApiDoc(
248
     *  requirements={
249
     *      {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="gallery identifier"}
250
     *  },
251
     *  input={"class"="sonata_media_api_form_gallery", "name"="", "groups"={"sonata_api_write"}},
252
     *  output={"class"="sonata_media_api_form_gallery", "groups"={"sonata_api_read"}},
253
     *  statusCodes={
254
     *      200="Returned when successful",
255
     *      400="Returned when an error has occurred while gallery creation",
256
     *      404="Returned when unable to find gallery"
257
     *  }
258
     * )
259
     *
260
     * @param int     $id      User id
261
     * @param Request $request A Symfony request
262
     *
263
     * @throws NotFoundHttpException
264
     *
265
     * @return GalleryInterface
266
     */
267
    public function putGalleryAction($id, Request $request)
268
    {
269
        return $this->handleWriteGallery($request, $id);
270
    }
271
272
    /**
273
     * Adds a media to a gallery.
274
     *
275
     * @ApiDoc(
276
     *  requirements={
277
     *      {"name"="galleryId", "dataType"="integer", "requirement"="\d+", "description"="gallery identifier"},
278
     *      {"name"="mediaId", "dataType"="integer", "requirement"="\d+", "description"="media identifier"}
279
     *  },
280
     *  input={"class"="sonata_media_api_form_gallery_item", "name"="", "groups"={"sonata_api_write"}},
281
     *  output={"class"="sonata_media_api_form_gallery", "groups"={"sonata_api_read"}},
282
     *  statusCodes={
283
     *      200="Returned when successful",
284
     *      400="Returned when an error has occurred while gallery/media attachment",
285
     *  }
286
     * )
287
     *
288
     * @param int     $galleryId A gallery identifier
289
     * @param int     $mediaId   A media identifier
290
     * @param Request $request   A Symfony request
291
     *
292
     * @throws NotFoundHttpException
293
     *
294
     * @return GalleryInterface
295
     */
296
    public function postGalleryMediaGalleryItemAction($galleryId, $mediaId, Request $request)
297
    {
298
        $gallery = $this->getGallery($galleryId);
299
        $media = $this->getMedia($mediaId);
300
301
        foreach ($gallery->getGalleryItems() as $galleryItem) {
302
            if ($galleryItem->getMedia()->getId() === $media->getId()) {
303
                return FOSRestView::create([
304
                    'error' => sprintf('Gallery "%s" already has media "%s"', $galleryId, $mediaId),
305
                ], 400);
306
            }
307
        }
308
309
        return $this->handleWriteGalleryItem($gallery, $media, null, $request);
310
    }
311
312
    /**
313
     * Updates a media to a gallery.
314
     *
315
     * @ApiDoc(
316
     *  requirements={
317
     *      {"name"="galleryId", "dataType"="integer", "requirement"="\d+", "description"="gallery identifier"},
318
     *      {"name"="mediaId", "dataType"="integer", "requirement"="\d+", "description"="media identifier"}
319
     *  },
320
     *  input={"class"="sonata_media_api_form_gallery_item", "name"="", "groups"={"sonata_api_write"}},
321
     *  output={"class"="sonata_media_api_form_gallery", "groups"={"sonata_api_read"}},
322
     *  statusCodes={
323
     *      200="Returned when successful",
324
     *      404="Returned when an error if media cannot be found in gallery",
325
     *  }
326
     * )
327
     *
328
     * @param int     $galleryId A gallery identifier
329
     * @param int     $mediaId   A media identifier
330
     * @param Request $request   A Symfony request
331
     *
332
     * @throws NotFoundHttpException
333
     *
334
     * @return GalleryInterface
335
     */
336
    public function putGalleryMediaGalleryItemAction($galleryId, $mediaId, Request $request)
337
    {
338
        $gallery = $this->getGallery($galleryId);
339
        $media = $this->getMedia($mediaId);
340
341
        foreach ($gallery->getGalleryItems() as $galleryItem) {
342
            if ($galleryItem->getMedia()->getId() === $media->getId()) {
343
                return $this->handleWriteGalleryItem($gallery, $media, $galleryItem, $request);
344
            }
345
        }
346
347
        throw new NotFoundHttpException(sprintf('Gallery "%s" does not have media "%s"', $galleryId, $mediaId));
348
    }
349
350
    /**
351
     * Deletes a media association to a gallery.
352
     *
353
     * @ApiDoc(
354
     *  requirements={
355
     *      {"name"="galleryId", "dataType"="integer", "requirement"="\d+", "description"="gallery identifier"},
356
     *      {"name"="mediaId", "dataType"="integer", "requirement"="\d+", "description"="media identifier"}
357
     *  },
358
     *  statusCodes={
359
     *      200="Returned when media is successfully deleted from gallery",
360
     *      400="Returned when an error has occurred while media deletion of gallery",
361
     *      404="Returned when unable to find gallery or media"
362
     *  }
363
     * )
364
     *
365
     * @param int $galleryId A gallery identifier
366
     * @param int $mediaId   A media identifier
367
     *
368
     * @throws NotFoundHttpException
369
     *
370
     * @return View
371
     */
372
    public function deleteGalleryMediaGalleryItemAction($galleryId, $mediaId)
373
    {
374
        $gallery = $this->getGallery($galleryId);
375
        $media = $this->getMedia($mediaId);
376
377
        foreach ($gallery->getGalleryItems() as $key => $galleryItem) {
378
            if ($galleryItem->getMedia()->getId() === $media->getId()) {
379
                $gallery->getGalleryItems()->remove($key);
0 ignored issues
show
The method remove cannot be called on $gallery->getGalleryItems() (of type array<integer,object<Son...\GalleryItemInterface>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
380
                $this->getGalleryManager()->save($gallery);
381
382
                return ['deleted' => true];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array('deleted' => true); (array<string,boolean>) is incompatible with the return type documented by Sonata\MediaBundle\Contr...yMediaGalleryItemAction of type FOS\RestBundle\Controller\Annotations\View.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
383
            }
384
        }
385
386
        return FOSRestView::create([
387
            'error' => sprintf('Gallery "%s" does not have media "%s" associated', $galleryId, $mediaId),
388
        ], 400);
389
    }
390
391
    /**
392
     * Deletes a gallery.
393
     *
394
     * @ApiDoc(
395
     *  requirements={
396
     *      {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="gallery identifier"}
397
     *  },
398
     *  statusCodes={
399
     *      200="Returned when gallery is successfully deleted",
400
     *      400="Returned when an error has occurred while gallery deletion",
401
     *      404="Returned when unable to find gallery"
402
     *  }
403
     * )
404
     *
405
     * @param int $id A Gallery identifier
406
     *
407
     * @throws NotFoundHttpException
408
     *
409
     * @return View
410
     */
411
    public function deleteGalleryAction($id)
412
    {
413
        $gallery = $this->getGallery($id);
414
415
        $this->galleryManager->delete($gallery);
416
417
        return ['deleted' => true];
418
    }
419
420
    /**
421
     * Write a GalleryItem, this method is used by both POST and PUT action methods.
422
     *
423
     * @return FormInterface
424
     */
425
    protected function handleWriteGalleryItem(GalleryInterface $gallery, MediaInterface $media, GalleryItemInterface $galleryItem = null, Request $request)
426
    {
427
        $form = $this->formFactory->createNamed(null, 'sonata_media_api_form_gallery_item', $galleryItem, [
428
            'csrf_protection' => false,
429
        ]);
430
431
        $form->handleRequest($request);
432
433
        if ($form->isValid()) {
434
            $galleryItem = $form->getData();
435
            $galleryItem->setMedia($media);
436
437
            $gallery->addGalleryItem($galleryItem);
438
            $this->galleryManager->save($gallery);
439
440
            $view = FOSRestView::create($galleryItem);
441
442
            $context = new Context();
443
            $context->setGroups(['sonata_api_read']);
444
            $context->enableMaxDepth();
445
446
            $view->setContext($context);
447
448
            return $view;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $view; (FOS\RestBundle\View\View) is incompatible with the return type documented by Sonata\MediaBundle\Contr...:handleWriteGalleryItem of type Symfony\Component\Form\FormInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
449
        }
450
451
        return $form;
452
    }
453
454
    /**
455
     * Retrieves gallery with id $id or throws an exception if it doesn't exist.
456
     *
457
     * @param $id
458
     *
459
     * @throws NotFoundHttpException
460
     *
461
     * @return GalleryInterface
462
     */
463
    protected function getGallery($id)
464
    {
465
        $gallery = $this->getGalleryManager()->findOneBy(['id' => $id]);
466
467
        if (null === $gallery) {
468
            throw new NotFoundHttpException(sprintf('Gallery (%d) not found', $id));
469
        }
470
471
        return $gallery;
472
    }
473
474
    /**
475
     * Retrieves media with id $id or throws an exception if it doesn't exist.
476
     *
477
     * @param $id
478
     *
479
     * @throws NotFoundHttpException
480
     *
481
     * @return MediaInterface
482
     */
483
    protected function getMedia($id)
484
    {
485
        $media = $this->getMediaManager()->findOneBy(['id' => $id]);
486
487
        if (null === $media) {
488
            throw new NotFoundHttpException(sprintf('Media (%d) not found', $id));
489
        }
490
491
        return $media;
492
    }
493
494
    /**
495
     * @return GalleryManagerInterface
496
     */
497
    protected function getGalleryManager()
498
    {
499
        return $this->galleryManager;
500
    }
501
502
    /**
503
     * @return MediaManagerInterface
504
     */
505
    protected function getMediaManager()
506
    {
507
        return $this->mediaManager;
508
    }
509
510
    /**
511
     * Write a Gallery, this method is used by both POST and PUT action methods.
512
     *
513
     * @param Request  $request Symfony request
514
     * @param int|null $id      A Gallery identifier
515
     *
516
     * @return View|FormInterface
517
     */
518
    protected function handleWriteGallery($request, $id = null)
519
    {
520
        $gallery = $id ? $this->getGallery($id) : null;
521
522
        $form = $this->formFactory->createNamed(null, 'sonata_media_api_form_gallery', $gallery, [
523
            'csrf_protection' => false,
524
        ]);
525
526
        $form->handleRequest($request);
527
528
        if ($form->isValid()) {
529
            $gallery = $form->getData();
530
            $this->galleryManager->save($gallery);
531
532
            $context = new Context();
533
            $context->setGroups(['sonata_api_read']);
534
            $context->enableMaxDepth();
535
536
            $view = FOSRestView::create($gallery);
537
            $view->setContext($context);
538
539
            return $view;
540
        }
541
542
        return $form;
543
    }
544
}
545