Completed
Pull Request — master (#421)
by Robbie
02:31
created

BlogController::index()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 11
rs 9.4285
c 1
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
1
<?php
2
3
namespace SilverStripe\Blog\Model;
4
5
use PageController;
6
use SilverStripe\Control\RSS\RSSFeed;
7
use SilverStripe\ORM\ArrayList;
8
use SilverStripe\ORM\FieldType\DBDatetime;
9
use SilverStripe\ORM\PaginatedList;
10
use SilverStripe\Security\Member;
11
12
/**
13
 * @package    silverstripe
14
 * @subpackage blog
15
 */
16
class BlogController extends PageController
17
{
18
    /**
19
     * @var array
20
     */
21
    private static $allowed_actions = array(
0 ignored issues
show
Unused Code introduced by
The property $allowed_actions is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
22
        'archive',
23
        'tag',
24
        'category',
25
        'rss',
26
        'profile'
27
    );
28
29
    /**
30
     * @var array
31
     */
32
    private static $url_handlers = array(
0 ignored issues
show
Unused Code introduced by
The property $url_handlers is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
33
        'tag/$Tag!/$Rss' => 'tag',
34
        'category/$Category!/$Rss' => 'category',
35
        'archive/$Year!/$Month/$Day' => 'archive',
36
        'profile/$URLSegment!' => 'profile'
37
    );
38
39
    /**
40
     * @var array
41
     */
42
    private static $casting = array(
0 ignored issues
show
Unused Code introduced by
The property $casting is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
43
        'MetaTitle' => 'Text',
44
        'FilterDescription' => 'Text'
45
    );
46
47
    /**
48
     * The current Blog Post DataList query.
49
     *
50
     * @var DataList
51
     */
52
    protected $blogPosts;
53
54
    /**
55
     * @return string
56
     */
57
    public function index()
58
    {
59
        /**
60
         * @var Blog $dataRecord
61
         */
62
        $dataRecord = $this->dataRecord;
63
64
        $this->blogPosts = $dataRecord->getBlogPosts();
65
66
        return $this->render();
67
    }
68
69
    /**
70
     * Renders a Blog Member's profile.
71
     *
72
     * @return HTTPResponse
73
     */
74
    public function profile()
75
    {
76
        $profile = $this->getCurrentProfile();
77
78
        if (!$profile) {
79
            return $this->httpError(404, 'Not Found');
80
        }
81
82
        $this->blogPosts = $this->getCurrentProfilePosts();
83
84
        return $this->render();
85
    }
86
87
    /**
88
     * Get the Member associated with the current URL segment.
89
     *
90
     * @return null|Member
91
     */
92
    public function getCurrentProfile()
93
    {
94
        $urlSegment = $this->request->param('URLSegment');
95
96
        if ($urlSegment) {
97
            return Member::get()
98
                ->filter('URLSegment', $urlSegment)
99
                ->first();
100
        }
101
102
        return null;
103
    }
104
105
    /**
106
     * Get posts related to the current Member profile.
107
     *
108
     * @return null|DataList
109
     */
110
    public function getCurrentProfilePosts()
111
    {
112
        $profile = $this->getCurrentProfile();
113
114
        if ($profile) {
115
            return $profile->BlogPosts()->filter('ParentID', $this->ID);
116
        }
117
118
        return null;
119
    }
120
121
    /**
122
     * Renders an archive for a specified date. This can be by year or year/month.
123
     *
124
     * @return null|HTTPResponse
125
     */
126
    public function archive()
127
    {
128
        /**
129
         * @var Blog $dataRecord
130
         */
131
        $dataRecord = $this->dataRecord;
132
133
        $year = $this->getArchiveYear();
134
        $month = $this->getArchiveMonth();
135
        $day = $this->getArchiveDay();
136
137
        if ($this->request->param('Month') && !$month) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $month of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
138
            $this->httpError(404, 'Not Found');
139
        }
140
141
        if ($month && $this->request->param('Day') && !$day) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $month of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $day of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
142
            $this->httpError(404, 'Not Found');
143
        }
144
145
        if ($year) {
146
            $this->blogPosts = $dataRecord->getArchivedBlogPosts($year, $month, $day);
147
148
            return $this->render();
149
        }
150
151
        $this->httpError(404, 'Not Found');
152
153
        return null;
154
    }
155
156
    /**
157
     * Fetches the archive year from the url.
158
     *
159
     * @return int
160
     */
161
    public function getArchiveYear()
162
    {
163
        if ($this->request->param('Year')) {
164
            if (preg_match('/^[0-9]{4}$/', $year = $this->request->param('Year'))) {
165
                return (int) $year;
166
            }
167
        } elseif ($this->request->param('Action') == 'archive') {
168
            return DBDatetime::now()->Year();
169
        }
170
171
        return null;
172
    }
173
174
    /**
175
     * Fetches the archive money from the url.
176
     *
177
     * @return null|int
178
     */
179
    public function getArchiveMonth()
180
    {
181
        $month = $this->request->param('Month');
182
183
        if (preg_match('/^[0-9]{1,2}$/', $month)) {
184
            if ($month > 0 && $month < 13) {
185
                if (checkdate($month, 01, $this->getArchiveYear())) {
186
                    return (int) $month;
187
                }
188
            }
189
        }
190
191
        return null;
192
    }
193
194
    /**
195
     * Fetches the archive day from the url.
196
     *
197
     * @return null|int
198
     */
199
    public function getArchiveDay()
200
    {
201
        $day = $this->request->param('Day');
202
203
        if (preg_match('/^[0-9]{1,2}$/', $day)) {
204
            if (checkdate($this->getArchiveMonth(), $day, $this->getArchiveYear())) {
205
                return (int) $day;
206
            }
207
        }
208
209
        return null;
210
    }
211
212
    /**
213
     * Renders the blog posts for a given tag.
214
     *
215
     * @return null|HTTPResponse
216
     */
217 View Code Duplication
    public function tag()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
218
    {
219
        $tag = $this->getCurrentTag();
220
221
        if ($tag) {
222
            $this->blogPosts = $tag->BlogPosts();
0 ignored issues
show
Documentation Bug introduced by
It seems like $tag->BlogPosts() of type object<SilverStripe\ORM\DataList> is incompatible with the declared type object<SilverStripe\Blog\Model\DataList> of property $blogPosts.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
223
224
            if($this->isRSS()) {
225
                return $this->rssFeed($this->blogPosts, $tag->getLink());
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->rssFeed($t...osts, $tag->getLink()); (SilverStripe\ORM\FieldType\DBHTMLText) is incompatible with the return type documented by SilverStripe\Blog\Model\BlogController::tag of type null|SilverStripe\Blog\Model\HTTPResponse.

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...
226
            } else {
227
                return $this->render();
228
            }
229
        }
230
231
        $this->httpError(404, 'Not Found');
232
233
        return null;
234
    }
235
236
    /**
237
     * Tag Getter for use in templates.
238
     *
239
     * @return null|BlogTag
240
     */
241 View Code Duplication
    public function getCurrentTag()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
242
    {
243
        /**
244
         * @var Blog $dataRecord
245
         */
246
        $dataRecord = $this->dataRecord;
247
        $tag = $this->request->param('Tag');
248
        if ($tag) {
249
            return $dataRecord->Tags()
250
                ->filter('URLSegment', array($tag, rawurlencode($tag)))
251
                ->first();
252
        }
253
        return null;
254
    }
255
256
    /**
257
     * Renders the blog posts for a given category.
258
     *
259
     * @return null|HTTPResponse
260
     */
261 View Code Duplication
    public function category()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
262
    {
263
        $category = $this->getCurrentCategory();
264
265
        if ($category) {
266
            $this->blogPosts = $category->BlogPosts();
0 ignored issues
show
Documentation Bug introduced by
It seems like $category->BlogPosts() of type object<SilverStripe\ORM\DataList> is incompatible with the declared type object<SilverStripe\Blog\Model\DataList> of property $blogPosts.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
267
268
            if($this->isRSS()) {
269
                return $this->rssFeed($this->blogPosts, $category->getLink());
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->rssFeed($t... $category->getLink()); (SilverStripe\ORM\FieldType\DBHTMLText) is incompatible with the return type documented by SilverStripe\Blog\Model\BlogController::category of type null|SilverStripe\Blog\Model\HTTPResponse.

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...
270
            } else {
271
                return $this->render();
272
            }
273
        }
274
275
        $this->httpError(404, 'Not Found');
276
277
        return null;
278
    }
279
280
    /**
281
     * Category Getter for use in templates.
282
     *
283
     * @return null|BlogCategory
284
     */
285 View Code Duplication
    public function getCurrentCategory()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
286
    {
287
        /**
288
         * @var Blog $dataRecord
289
         */
290
        $dataRecord = $this->dataRecord;
291
        $category = $this->request->param('Category');
292
        if ($category) {
293
            return $dataRecord->Categories()
294
                ->filter('URLSegment', array($category, rawurlencode($category)))
295
                ->first();
296
        }
297
        return null;
298
    }
299
300
    /**
301
     * Get the meta title for the current action.
302
     *
303
     * @return string
304
     */
305
    public function getMetaTitle()
306
    {
307
        $title = $this->data()->getTitle();
308
        $filter = $this->getFilterDescription();
309
310
        if ($filter) {
311
            $title = sprintf('%s - %s', $title, $filter);
312
        }
313
314
        $this->extend('updateMetaTitle', $title);
315
316
        return $title;
317
    }
318
319
    /**
320
     * Returns a description of the current filter.
321
     *
322
     * @return string
323
     */
324
    public function getFilterDescription()
325
    {
326
        $items = array();
327
328
        $list = $this->PaginatedList();
329
        $currentPage = $list->CurrentPage();
330
331
        if ($currentPage > 1) {
332
            $items[] = _t(
333
                'Blog.FILTERDESCRIPTION_PAGE',
334
                'Page {page}',
335
                null,
336
                array(
337
                    'page' => $currentPage
338
                )
339
            );
340
        }
341
342
        if ($author = $this->getCurrentProfile()) {
343
            $items[] = _t(
344
                'Blog.FILTERDESCRIPTION_AUTHOR',
345
                'By {author}',
346
                null,
347
                array(
348
                    'author' => $author->Title
349
                )
350
            );
351
        }
352
353
        if ($tag = $this->getCurrentTag()) {
354
            $items[] = _t(
355
                'Blog.FILTERDESCRIPTION_TAG',
356
                'Tagged with {tag}',
357
                null,
358
                array(
359
                    'tag' => $tag->Title
360
                )
361
            );
362
        }
363
364
        if ($category = $this->getCurrentCategory()) {
365
            $items[] = _t(
366
                'Blog.FILTERDESCRIPTION_CATEGORY',
367
                'In category {category}',
368
                null,
369
                array(
370
                    'category' => $category->Title
371
                )
372
            );
373
        }
374
375
        if ($this->owner->getArchiveYear()) {
376
            if ($this->owner->getArchiveDay()) {
377
                $date = $this->owner->getArchiveDate()->Nice();
378
            } elseif ($this->owner->getArchiveMonth()) {
379
                $date = $this->owner->getArchiveDate()->format('F, Y');
380
            } else {
381
                $date = $this->owner->getArchiveDate()->format('Y');
382
            }
383
384
            $items[] = _t(
385
                'Blog.FILTERDESCRIPTION_DATE',
386
                'In {date}',
387
                null,
388
                array(
389
                    'date' => $date,
390
                )
391
            );
392
        }
393
394
        $result = '';
395
396
        if ($items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
397
            $result = implode(', ', $items);
398
        }
399
400
        $this->extend('updateFilterDescription', $result);
401
402
        return $result;
403
    }
404
405
    /**
406
     * Returns a list of paginated blog posts based on the BlogPost dataList.
407
     *
408
     * @return PaginatedList
409
     */
410
    public function PaginatedList()
411
    {
412
        $allPosts = $this->blogPosts ?: ArrayList::create();
413
        $posts = PaginatedList::create($allPosts);
414
415
        // Set appropriate page size
416
        if ($this->PostsPerPage > 0) {
417
            $pageSize = $this->PostsPerPage;
418
        } elseif ($count = $allPosts->count()) {
419
            $pageSize = $count;
420
        } else {
421
            $pageSize = 99999;
422
        }
423
        $posts->setPageLength($pageSize);
424
425
        // Set current page
426
        $start = $this->request->getVar($posts->getPaginationGetVar());
427
        $posts->setPageStart($start);
428
429
        return $posts;
430
    }
431
432
    /**
433
     * Displays an RSS feed of blog posts.
434
     *
435
     * @return string
436
     */
437
    public function rss()
438
    {
439
        /**
440
         * @var Blog $dataRecord
441
         */
442
        $dataRecord = $this->dataRecord;
443
444
        $this->blogPosts = $dataRecord->getBlogPosts();
445
446
        return $this->rssFeed($this->blogPosts, $this->Link());
447
    }
448
449
    /**
450
     * Returns the current archive date.
451
     *
452
     * @return null|Date
453
     */
454
    public function getArchiveDate()
455
    {
456
        $year = $this->getArchiveYear();
457
        $month = $this->getArchiveMonth();
458
        $day = $this->getArchiveDay();
459
460
        if ($year) {
461
            if ($month) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $month of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
462
                $date = sprintf('%s-%s-01', $year, $month);
463
464
                if ($day) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $day of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
465
                    $date = sprintf('%s-%s-%s', $year, $month, $day);
466
                }
467
            } else {
468
                $date = sprintf('%s-01-01', $year);
469
            }
470
471
            $obj = DBDatetime::create('date');
472
            $obj->setValue($date);
473
            return $obj;
474
        }
475
476
        return null;
477
    }
478
479
    /**
480
     * Returns a link to the RSS feed.
481
     *
482
     * @return string
483
     */
484
    public function getRSSLink()
485
    {
486
        return $this->Link('rss');
487
    }
488
489
    /**
490
     * Displays an RSS feed of the given blog posts.
491
     *
492
     * @param DataList $blogPosts
493
     * @param string $link
494
     *
495
     * @return string
496
     */
497
    protected function rssFeed($blogPosts, $link)
498
    {
499
        $rss = new RSSFeed($blogPosts, $link, $this->MetaTitle, $this->MetaDescription);
500
501
        $this->extend('updateRss', $rss);
502
503
        return $rss->outputToBrowser();
504
    }
505
506
    /**
507
     * Returns true if the $Rss sub-action for categories/tags has been set to "rss"
508
     *
509
     * @return bool
510
     */
511
    protected function isRSS()
512
    {
513
        $rss = $this->request->param('Rss');
514
        return (is_string($rss) && strcasecmp($rss, 'rss') == 0);
515
    }
516
}
517