Completed
Push — 3.x-dev-kit ( 21be5a )
by
unknown
03:10
created

PostController::putPostCommentsAction()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 37
rs 8.5806
cc 4
eloc 21
nc 4
nop 3
1
<?php
2
3
/*
4
 * This file is part of the Sonata Project package.
5
 *
6
 * (c) Thomas Rabaix <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sonata\NewsBundle\Controller\Api;
13
14
use Application\Sonata\NewsBundle\Entity\Post;
15
use FOS\RestBundle\Controller\Annotations\QueryParam;
16
use FOS\RestBundle\Controller\Annotations\Route;
17
use FOS\RestBundle\Controller\Annotations\View;
18
use FOS\RestBundle\Request\ParamFetcherInterface;
19
use JMS\Serializer\SerializationContext;
20
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
21
use Sonata\DatagridBundle\Pager\PagerInterface;
22
use Sonata\FormatterBundle\Formatter\Pool as FormatterPool;
23
use Sonata\NewsBundle\Mailer\MailerInterface;
24
use Sonata\NewsBundle\Model\Comment;
25
use Sonata\NewsBundle\Model\CommentManagerInterface;
26
use Sonata\NewsBundle\Model\PostManagerInterface;
27
use Symfony\Component\Form\FormFactoryInterface;
28
use Symfony\Component\Form\FormInterface;
29
use Symfony\Component\HttpFoundation\Request;
30
use Symfony\Component\HttpKernel\Exception\HttpException;
31
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
32
33
/**
34
 * Class PostController.
35
 *
36
 *
37
 * @author Hugo Briand <[email protected]>
38
 */
39
class PostController
40
{
41
    /**
42
     * @var PostManagerInterface
43
     */
44
    protected $postManager;
45
46
    /**
47
     * @var CommentManagerInterface
48
     */
49
    protected $commentManager;
50
51
    /**
52
     * @var MailerInterface
53
     */
54
    protected $mailer;
55
56
    /**
57
     * @var FormFactoryInterface
58
     */
59
    protected $formFactory;
60
61
    /**
62
     * @var FormatterPool
63
     */
64
    protected $formatterPool;
65
66
    /**
67
     * Constructor.
68
     *
69
     * @param PostManagerInterface    $postManager
70
     * @param CommentManagerInterface $commentManager
71
     * @param MailerInterface         $mailer
72
     * @param FormFactoryInterface    $formFactory
73
     * @param FormatterPool           $formatterPool
74
     */
75
    public function __construct(PostManagerInterface $postManager, CommentManagerInterface $commentManager, MailerInterface $mailer, FormFactoryInterface $formFactory, FormatterPool $formatterPool)
76
    {
77
        $this->postManager = $postManager;
78
        $this->commentManager = $commentManager;
79
        $this->mailer = $mailer;
80
        $this->formFactory = $formFactory;
81
        $this->formatterPool = $formatterPool;
82
    }
83
84
    /**
85
     * Retrieves the list of posts (paginated) based on criteria.
86
     *
87
     * @ApiDoc(
88
     *  resource=true,
89
     *  output={"class"="Sonata\DatagridBundle\Pager\PagerInterface", "groups"={"sonata_api_read"}}
90
     * )
91
     *
92
     * @QueryParam(name="page", requirements="\d+", default="1", description="Page for posts list pagination")
93
     * @QueryParam(name="count", requirements="\d+", default="10", description="Number of posts by page")
94
     * @QueryParam(name="enabled", requirements="0|1", nullable=true, strict=true, description="Enabled/Disabled posts filter")
95
     * @QueryParam(name="dateQuery", requirements=">|<|=", default=">", description="Date filter orientation (>, < or =)")
96
     * @QueryParam(name="dateValue", requirements="[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]([+-][0-9]{2}(:)?[0-9]{2})?", nullable=true, strict=true, description="Date filter value")
97
     * @QueryParam(name="tag", requirements="\S+", nullable=true, strict=true, description="Tag name filter")
98
     * @QueryParam(name="author", requirements="\S+", nullable=true, strict=true, description="Author filter")
99
     * @QueryParam(name="mode", requirements="public|admin", default="public", description="'public' mode filters posts having enabled tags and author")
100
     *
101
     * @View(serializerGroups="sonata_api_read", serializerEnableMaxDepthChecks=true)
102
     *
103
     * @Route(requirements={"_format"="json|xml"})
104
     *
105
     * @param ParamFetcherInterface $paramFetcher
106
     *
107
     * @return PagerInterface
108
     */
109
    public function getPostsAction(ParamFetcherInterface $paramFetcher)
110
    {
111
        $page = $paramFetcher->get('page');
112
        $count = $paramFetcher->get('count');
113
114
        $pager = $this->postManager->getPager($this->filterCriteria($paramFetcher), $page, $count);
115
116
        return $pager;
117
    }
118
119
    /**
120
     * Retrieves a specific post.
121
     *
122
     * @ApiDoc(
123
     *  requirements={
124
     *      {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="post id"}
125
     *  },
126
     *  output={"class"="sonata_news_api_form_post", "groups"={"sonata_api_read"}},
127
     *  statusCodes={
128
     *      200="Returned when successful",
129
     *      404="Returned when post is not found"
130
     *  }
131
     * )
132
     *
133
     * @View(serializerGroups="sonata_api_read", serializerEnableMaxDepthChecks=true)
134
     *
135
     * @Route(requirements={"_format"="json|xml"})
136
     *
137
     * @param int $id A post identifier
138
     *
139
     * @return Post
140
     */
141
    public function getPostAction($id)
142
    {
143
        return $this->getPost($id);
144
    }
145
146
    /**
147
     * Adds a post.
148
     *
149
     * @ApiDoc(
150
     *  input={"class"="sonata_news_api_form_post", "name"="", "groups"={"sonata_api_write"}},
151
     *  output={"class"="sonata_news_api_form_post", "groups"={"sonata_api_read"}},
152
     *  statusCodes={
153
     *      200="Returned when successful",
154
     *      400="Returned when an error has occurred while post creation",
155
     *  }
156
     * )
157
     *
158
     * @Route(requirements={"_format"="json|xml"})
159
     *
160
     * @param Request $request A Symfony request
161
     *
162
     * @return Post
163
     *
164
     * @throws NotFoundHttpException
165
     */
166
    public function postPostAction(Request $request)
167
    {
168
        return $this->handleWritePost($request);
169
    }
170
171
    /**
172
     * Updates a post.
173
     *
174
     * @ApiDoc(
175
     *  requirements={
176
     *      {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="post identifier"}
177
     *  },
178
     *  input={"class"="sonata_news_api_form_post", "name"="", "groups"={"sonata_api_write"}},
179
     *  output={"class"="sonata_news_api_form_post", "groups"={"sonata_api_read"}},
180
     *  statusCodes={
181
     *      200="Returned when successful",
182
     *      400="Returned when an error has occurred while post update",
183
     *      404="Returned when unable to find post"
184
     *  }
185
     * )
186
     *
187
     * @Route(requirements={"_format"="json|xml"})
188
     *
189
     * @param int     $id      A Post identifier
190
     * @param Request $request A Symfony request
191
     *
192
     * @return Post
193
     *
194
     * @throws NotFoundHttpException
195
     */
196
    public function putPostAction($id, Request $request)
197
    {
198
        return $this->handleWritePost($request, $id);
199
    }
200
201
    /**
202
     * Deletes a post.
203
     *
204
     * @ApiDoc(
205
     *  requirements={
206
     *      {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="post identifier"}
207
     *  },
208
     *  statusCodes={
209
     *      200="Returned when post is successfully deleted",
210
     *      400="Returned when an error has occurred while post deletion",
211
     *      404="Returned when unable to find post"
212
     *  }
213
     * )
214
     *
215
     * @Route(requirements={"_format"="json|xml"})
216
     *
217
     * @param int $id A Post identifier
218
     *
219
     * @return View
220
     *
221
     * @throws NotFoundHttpException
222
     */
223
    public function deletePostAction($id)
224
    {
225
        $post = $this->getPost($id);
226
227
        try {
228
            $this->postManager->delete($post);
229
        } catch (\Exception $e) {
230
            return \FOS\RestBundle\View\View::create(array('error' => $e->getMessage()), 400);
231
        }
232
233
        return array('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\NewsBundle\Contro...oller::deletePostAction 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...
234
    }
235
236
    /**
237
     * Retrieves the comments of specified post.
238
     *
239
     * @ApiDoc(
240
     *  requirements={
241
     *      {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="Post id"}
242
     *  },
243
     *  output={"class"="Sonata\DatagridBundle\Pager\PagerInterface", "groups"={"sonata_api_read"}},
244
     *  statusCodes={
245
     *      200="Returned when successful",
246
     *      404="Returned when post is not found"
247
     *  }
248
     * )
249
     *
250
     * @QueryParam(name="page", requirements="\d+", default="1", description="Page for comments list pagination")
251
     * @QueryParam(name="count", requirements="\d+", default="10", description="Number of comments by page")
252
     *
253
     * @Route(requirements={"_format"="json|xml"})
254
     *
255
     * @View(serializerGroups="sonata_api_read", serializerEnableMaxDepthChecks=true)
256
     *
257
     * @param int                   $id           A post identifier
258
     * @param ParamFetcherInterface $paramFetcher
259
     *
260
     * @return PagerInterface
261
     */
262
    public function getPostCommentsAction($id, ParamFetcherInterface $paramFetcher)
263
    {
264
        $post = $this->getPost($id);
265
266
        $page = $paramFetcher->get('page');
267
        $count = $paramFetcher->get('count');
268
269
        $criteria = $this->filterCriteria($paramFetcher);
270
        $criteria['postId'] = $post->getId();
271
272
        /** @var PagerInterface $pager */
273
        $pager = $this->commentManager->getPager($criteria, $page, $count);
274
275
        return $pager;
276
    }
277
278
    /**
279
     * Adds a comment to a post.
280
     *
281
     * @ApiDoc(
282
     *  requirements={
283
     *      {"name"="id", "dataType"="integer", "requirement"="\d+", "description"="post id"}
284
     *  },
285
     *  input={"class"="sonata_news_api_form_comment", "name"="", "groups"={"sonata_api_write"}},
286
     *  output={"class"="Sonata\NewsBundle\Model\Comment", "groups"={"sonata_api_read"}},
287
     *  statusCodes={
288
     *      200="Returned when successful",
289
     *      400="Returned when an error has occurred while comment creation",
290
     *      403="Returned when commenting is not enabled on the related post",
291
     *      404="Returned when post is not found"
292
     *  }
293
     * )
294
     *
295
     * @Route(requirements={"_format"="json|xml"})
296
     *
297
     * @param int     $id      A post identifier
298
     * @param Request $request
299
     *
300
     * @return Comment|FormInterface
301
     *
302
     * @throws HttpException
303
     */
304
    public function postPostCommentsAction($id, Request $request)
305
    {
306
        $post = $this->getPost($id);
307
308
        if (!$post->isCommentable()) {
309
            throw new HttpException(403, sprintf('Post (%d) not commentable', $id));
310
        }
311
312
        $comment = $this->commentManager->create();
313
        $comment->setPost($post);
314
315
        $form = $this->formFactory->createNamed(null, 'sonata_news_api_form_comment', $comment, array('csrf_protection' => false));
316
        $form->bind($request);
317
318
        if ($form->isValid()) {
319
            $comment = $form->getData();
320
            $comment->setPost($post);
321
322
            if (!$comment->getStatus()) {
323
                $comment->setStatus($post->getCommentsDefaultStatus());
324
            }
325
326
            $this->commentManager->save($comment);
327
            $this->mailer->sendCommentNotification($comment);
328
329
            $view = \FOS\RestBundle\View\View::create($comment);
330
            $serializationContext = SerializationContext::create();
331
            $serializationContext->setGroups(array('sonata_api_read'));
332
            $serializationContext->enableMaxDepthChecks();
333
            $view->setSerializationContext($serializationContext);
334
335
            return $view;
336
        }
337
338
        return $form;
339
    }
340
341
    /**
342
     * Updates a comment.
343
     *
344
     * @ApiDoc(
345
     *  requirements={
346
     *      {"name"="postId", "dataType"="integer", "requirement"="\d+", "description"="post identifier"},
347
     *      {"name"="commentId", "dataType"="integer", "requirement"="\d+", "description"="comment identifier"}
348
     *  },
349
     *  input={"class"="sonata_news_api_form_comment", "name"="", "groups"={"sonata_api_write"}},
350
     *  output={"class"="Sonata\NewsBundle\Model\Comment", "groups"={"sonata_api_read"}},
351
     *  statusCodes={
352
     *      200="Returned when successful",
353
     *      400="Returned when an error has occurred while comment update",
354
     *      404="Returned when unable to find comment"
355
     *  }
356
     * )
357
     *
358
     * @Route(requirements={"_format"="json|xml"})
359
     *
360
     * @param int     $postId    A post identifier
361
     * @param int     $commentId A comment identifier
362
     * @param Request $request   A Symfony request
363
     *
364
     * @return Comment
365
     *
366
     * @throws NotFoundHttpException
367
     * @throws HttpException
368
     */
369
    public function putPostCommentsAction($postId, $commentId, Request $request)
370
    {
371
        $post = $this->getPost($postId);
372
373
        if (!$post->isCommentable()) {
374
            throw new HttpException(403, sprintf('Post (%d) not commentable', $postId));
375
        }
376
377
        $comment = $this->commentManager->find($commentId);
378
379
        if (null === $comment) {
380
            throw new NotFoundHttpException(sprintf('Comment (%d) not found', $commentId));
381
        }
382
383
        $comment->setPost($post);
384
385
        $form = $this->formFactory->createNamed(null, 'sonata_news_api_form_comment', $comment, array(
386
            'csrf_protection' => false,
387
        ));
388
389
        $form->bind($request);
390
391
        if ($form->isValid()) {
392
            $comment = $form->getData();
393
            $this->commentManager->save($comment);
394
395
            $view = \FOS\RestBundle\View\View::create($comment);
396
            $serializationContext = SerializationContext::create();
397
            $serializationContext->setGroups(array('sonata_api_read'));
398
            $serializationContext->enableMaxDepthChecks();
399
            $view->setSerializationContext($serializationContext);
400
401
            return $view;
402
        }
403
404
        return $form;
405
    }
406
407
    /**
408
     * Filters criteria from $paramFetcher to be compatible with the Pager criteria.
409
     *
410
     * @param ParamFetcherInterface $paramFetcher
411
     *
412
     * @return array The filtered criteria
413
     */
414
    protected function filterCriteria(ParamFetcherInterface $paramFetcher)
415
    {
416
        $criteria = $paramFetcher->all();
417
418
        unset($criteria['page'], $criteria['count']);
419
420
        foreach ($criteria as $key => $value) {
421
            if (null === $value) {
422
                unset($criteria[$key]);
423
            }
424
        }
425
426
        if (array_key_exists('dateValue', $criteria)) {
427
            $date = new \DateTime($criteria['dateValue']);
428
            $criteria['date'] = array(
429
                'query' => sprintf('p.publicationDateStart %s :dateValue', $criteria['dateQuery']),
430
                'params' => array('dateValue' => $date),
431
            );
432
            unset($criteria['dateValue'], $criteria['dateQuery']);
433
        } else {
434
            unset($criteria['dateQuery']);
435
        }
436
437
        return $criteria;
438
    }
439
440
    /**
441
     * Retrieves post with id $id or throws an exception if it doesn't exist.
442
     *
443
     * @param int $id A Post identifier
444
     *
445
     * @return Post
446
     *
447
     * @throws NotFoundHttpException
448
     */
449
    protected function getPost($id)
450
    {
451
        $post = $this->postManager->find($id);
452
453
        if (null === $post) {
454
            throw new NotFoundHttpException(sprintf('Post (%d) not found', $id));
455
        }
456
457
        return $post;
458
    }
459
460
    /**
461
     * Write a post, this method is used by both POST and PUT action methods.
462
     *
463
     * @param Request  $request Symfony request
464
     * @param int|null $id      A post identifier
465
     *
466
     * @return FormInterface
467
     */
468
    protected function handleWritePost($request, $id = null)
469
    {
470
        $post = $id ? $this->getPost($id) : null;
471
472
        $form = $this->formFactory->createNamed(null, 'sonata_news_api_form_post', $post, array(
473
            'csrf_protection' => false,
474
        ));
475
476
        $form->bind($request);
477
478
        if ($form->isValid()) {
479
            $post = $form->getData();
480
            $post->setContent($this->formatterPool->transform($post->getContentFormatter(), $post->getRawContent()));
481
            $this->postManager->save($post);
482
483
            $view = \FOS\RestBundle\View\View::create($post);
484
            $serializationContext = SerializationContext::create();
485
            $serializationContext->setGroups(array('sonata_api_read'));
486
            $serializationContext->enableMaxDepthChecks();
487
            $view->setSerializationContext($serializationContext);
488
489
            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\NewsBundle\Contro...roller::handleWritePost 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...
490
        }
491
492
        return $form;
493
    }
494
}
495