Completed
Push — master ( b1acc1...427abe )
by Robbie
01:35
created

BlogController::PaginationAbsoluteNextLink()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
1
<?php
2
3
namespace SilverStripe\Blog\Model;
4
5
use PageController;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Control\RSS\RSSFeed;
8
use SilverStripe\ORM\ArrayList;
9
use SilverStripe\ORM\FieldType\DBDatetime;
10
use SilverStripe\ORM\PaginatedList;
11
use SilverStripe\Security\Member;
12
use SilverStripe\View\Parsers\URLSegmentFilter;
13
14
class BlogController extends PageController
15
{
16
    /**
17
     * @var array
18
     */
19
    private static $allowed_actions = [
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...
20
        'archive',
21
        'tag',
22
        'category',
23
        'rss',
24
        'profile'
25
    ];
26
27
    /**
28
     * @var array
29
     */
30
    private static $url_handlers = [
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...
31
        'tag/$Tag!/$Rss' => 'tag',
32
        'category/$Category!/$Rss' => 'category',
33
        'archive/$Year!/$Month/$Day' => 'archive',
34
        'profile/$URLSegment!' => 'profile'
35
    ];
36
37
    /**
38
     * @var array
39
     */
40
    private static $casting = [
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...
41
        'MetaTitle' => 'Text',
42
        'FilterDescription' => 'Text'
43
    ];
44
45
    /**
46
     * The current Blog Post DataList query.
47
     *
48
     * @var DataList
49
     */
50
    protected $blogPosts;
51
52
    /**
53
     * @return string
54
     */
55
    public function index()
56
    {
57
        /**
58
         * @var Blog $dataRecord
59
         */
60
        $dataRecord = $this->dataRecord;
61
62
        $this->blogPosts = $dataRecord->getBlogPosts();
63
64
        return $this->render();
65
    }
66
67
    /**
68
     * Renders a Blog Member's profile.
69
     *
70
     * @return HTTPResponse
71
     */
72
    public function profile()
73
    {
74
        $profile = $this->getCurrentProfile();
75
76
        if (!$profile) {
77
            return $this->httpError(404, 'Not Found');
78
        }
79
80
        $this->blogPosts = $this->getCurrentProfilePosts();
81
82
        return $this->render();
83
    }
84
85
    /**
86
     * Get the Member associated with the current URL segment.
87
     *
88
     * @return null|Member
89
     */
90
    public function getCurrentProfile()
91
    {
92
        $urlSegment = $this->request->param('URLSegment');
93
94
        if ($urlSegment) {
95
            $filter = URLSegmentFilter::create();
96
97
            return Member::get()
98
                ->filter('URLSegment', $filter->filter($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()); (string) 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
            $filter = URLSegmentFilter::create();
250
251
            return $dataRecord->Tags()
252
                ->filter('URLSegment', [$tag, $filter->filter($tag)])
253
                ->first();
254
        }
255
        return null;
256
    }
257
258
    /**
259
     * Renders the blog posts for a given category.
260
     *
261
     * @return null|HTTPResponse
262
     */
263 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...
264
    {
265
        $category = $this->getCurrentCategory();
266
267
        if ($category) {
268
            $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...
269
270
            if ($this->isRSS()) {
271
                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()); (string) 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...
272
            } else {
273
                return $this->render();
274
            }
275
        }
276
277
        $this->httpError(404, 'Not Found');
278
279
        return null;
280
    }
281
282
    /**
283
     * Category Getter for use in templates.
284
     *
285
     * @return null|BlogCategory
286
     */
287 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...
288
    {
289
        /**
290
         * @var Blog $dataRecord
291
         */
292
        $dataRecord = $this->dataRecord;
293
        $category = $this->request->param('Category');
294
        if ($category) {
295
            $filter = URLSegmentFilter::create();
296
297
            return $dataRecord->Categories()
298
                ->filter('URLSegment', [$category, $filter->filter($category)])
299
                ->first();
300
        }
301
        return null;
302
    }
303
304
    /**
305
     * Get the meta title for the current action.
306
     *
307
     * @return string
308
     */
309
    public function getMetaTitle()
310
    {
311
        $title = $this->data()->getTitle();
312
        $filter = $this->getFilterDescription();
313
314
        if ($filter) {
315
            $title = sprintf('%s - %s', $title, $filter);
316
        }
317
318
        $this->extend('updateMetaTitle', $title);
319
320
        return $title;
321
    }
322
323
    /**
324
     * Returns a description of the current filter.
325
     *
326
     * @return string
327
     */
328
    public function getFilterDescription()
329
    {
330
        $items = [];
331
332
        $list = $this->PaginatedList();
333
        $currentPage = $list->CurrentPage();
334
335
        if ($currentPage > 1) {
336
            $items[] = _t(
337
                'SilverStripe\\Blog\\Model\\Blog.FILTERDESCRIPTION_PAGE',
338
                'Page {page}',
339
                null,
340
                [
341
                    'page' => $currentPage
342
                ]
343
            );
344
        }
345
346
        if ($author = $this->getCurrentProfile()) {
347
            $items[] = _t(
348
                'SilverStripe\\Blog\\Model\\Blog.FILTERDESCRIPTION_AUTHOR',
349
                'By {author}',
350
                null,
351
                [
352
                    'author' => $author->Title
353
                ]
354
            );
355
        }
356
357
        if ($tag = $this->getCurrentTag()) {
358
            $items[] = _t(
359
                'SilverStripe\\Blog\\Model\\Blog.FILTERDESCRIPTION_TAG',
360
                'Tagged with {tag}',
361
                null,
362
                [
363
                    'tag' => $tag->Title
364
                ]
365
            );
366
        }
367
368
        if ($category = $this->getCurrentCategory()) {
369
            $items[] = _t(
370
                'SilverStripe\\Blog\\Model\\Blog.FILTERDESCRIPTION_CATEGORY',
371
                'In category {category}',
372
                null,
373
                [
374
                    'category' => $category->Title
375
                ]
376
            );
377
        }
378
379
        if ($this->owner->getArchiveYear()) {
380
            if ($this->owner->getArchiveDay()) {
381
                $date = $this->owner->getArchiveDate()->Nice();
382
            } elseif ($this->owner->getArchiveMonth()) {
383
                $date = $this->owner->getArchiveDate()->format('F, Y');
384
            } else {
385
                $date = $this->owner->getArchiveDate()->format('Y');
386
            }
387
388
            $items[] = _t(
389
                'SilverStripe\\Blog\\Model\\Blog.FILTERDESCRIPTION_DATE',
390
                'In {date}',
391
                null,
392
                [
393
                    'date' => $date,
394
                ]
395
            );
396
        }
397
398
        $result = '';
399
400
        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...
401
            $result = implode(', ', $items);
402
        }
403
404
        $this->extend('updateFilterDescription', $result);
405
406
        return $result;
407
    }
408
409
    /**
410
     * Returns a list of paginated blog posts based on the BlogPost dataList.
411
     *
412
     * @return PaginatedList
413
     */
414
    public function PaginatedList()
415
    {
416
        $allPosts = $this->blogPosts ?: ArrayList::create();
417
        $posts = PaginatedList::create($allPosts);
418
419
        // Set appropriate page size
420
        if ($this->PostsPerPage > 0) {
421
            $pageSize = $this->PostsPerPage;
422
        } elseif ($count = $allPosts->count()) {
423
            $pageSize = $count;
424
        } else {
425
            $pageSize = 99999;
426
        }
427
        $posts->setPageLength($pageSize);
428
429
        // Set current page
430
        $start = $this->request->getVar($posts->getPaginationGetVar());
431
        $posts->setPageStart($start);
432
433
        return $posts;
434
    }
435
436
437
    /**
438
     * Returns the absolute link to the next page for use in the page meta tags. This helps search engines
439
     * find the pagination and index all pages properly.
440
     *
441
     * @example "<% if $PaginationAbsoluteNextLink %><link rel="next" href="$PaginationAbsoluteNextLink"><% end_if %>"
442
     *
443
     * @return string
444
     */
445
    public function PaginationAbsoluteNextLink()
446
    {
447
        $posts = $this->PaginatedList();
448
        if ($posts->NotLastPage()) {
449
            return Director::absoluteURL($posts->NextLink());
450
        }
451
    }
452
453
     /**
454
     * Returns the absolute link to the previous page for use in the page meta tags. This helps search engines
455
     * find the pagination and index all pages properly.
456
     *
457
     * @example "<% if $PaginationAbsolutePrevLink %><link rel="prev" href="$PaginationAbsolutePrevLink"><% end_if %>"
458
     *
459
     * @return string
460
     */
461
    public function PaginationAbsolutePrevLink()
462
    {
463
        $posts = $this->PaginatedList();
464
        if ($posts->NotFirstPage()) {
465
            return Director::absoluteURL($posts->PrevLink());
466
        }
467
    }
468
469
    /**
470
     * Displays an RSS feed of blog posts.
471
     *
472
     * @return string
473
     */
474
    public function rss()
475
    {
476
        /**
477
         * @var Blog $dataRecord
478
         */
479
        $dataRecord = $this->dataRecord;
480
481
        $this->blogPosts = $dataRecord->getBlogPosts();
482
483
        return $this->rssFeed($this->blogPosts, $this->Link());
484
    }
485
486
    /**
487
     * Returns the current archive date.
488
     *
489
     * @return null|Date
490
     */
491
    public function getArchiveDate()
492
    {
493
        $year = $this->getArchiveYear();
494
        $month = $this->getArchiveMonth();
495
        $day = $this->getArchiveDay();
496
497
        if ($year) {
498
            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...
499
                $date = sprintf('%s-%s-01', $year, $month);
500
501
                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...
502
                    $date = sprintf('%s-%s-%s', $year, $month, $day);
503
                }
504
            } else {
505
                $date = sprintf('%s-01-01', $year);
506
            }
507
508
            $obj = DBDatetime::create('date');
509
            $obj->setValue($date);
510
            return $obj;
511
        }
512
513
        return null;
514
    }
515
516
    /**
517
     * Returns a link to the RSS feed.
518
     *
519
     * @return string
520
     */
521
    public function getRSSLink()
522
    {
523
        return $this->Link('rss');
524
    }
525
526
    /**
527
     * Displays an RSS feed of the given blog posts.
528
     *
529
     * @param DataList $blogPosts
530
     * @param string $link
531
     *
532
     * @return string
533
     */
534
    protected function rssFeed($blogPosts, $link)
535
    {
536
        $rss = RSSFeed::create($blogPosts, $link, $this->MetaTitle, $this->MetaDescription);
537
538
        $this->extend('updateRss', $rss);
539
540
        return $rss->outputToBrowser();
541
    }
542
543
    /**
544
     * Returns true if the $Rss sub-action for categories/tags has been set to "rss"
545
     *
546
     * @return bool
547
     */
548
    protected function isRSS()
549
    {
550
        $rss = $this->request->param('Rss');
551
        return (is_string($rss) && strcasecmp($rss, 'rss') == 0);
552
    }
553
}
554