PermalinkGenerator   C
last analyzed

Complexity

Total Complexity 55

Size/Duplication

Total Lines 335
Duplicated Lines 14.93 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 4
dl 50
loc 335
ccs 0
cts 215
cp 0
rs 6
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
C getPermalink() 22 72 16
A getPlacehoders() 0 26 2
A isItemWithDate() 0 6 1
A templateNeedsDate() 0 8 5
A getTitleSlugified() 0 14 4
A getCategoriesPath() 0 16 3
A generatePath() 0 6 1
A generateUrlPath() 0 10 2
A getPreservePathTitleAttribute() 14 14 3
A getDateAttribute() 0 18 4
A getNoHtmlExtensionAttribute() 14 14 3
A isCustomCollection() 0 4 1
A getPermalinkAttribute() 0 15 4
A sanitize() 0 25 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like PermalinkGenerator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PermalinkGenerator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Yosymfony\Spress.
5
 *
6
 * (c) YoSymfony <http://github.com/yosymfony>
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 Yosymfony\Spress\Core\ContentManager\Permalink;
13
14
use Yosymfony\Spress\Core\DataSource\ItemInterface;
15
use Yosymfony\Spress\Core\ContentManager\Exception\AttributeValueException;
16
use Yosymfony\Spress\Core\ContentManager\Exception\MissingAttributeException;
17
use Yosymfony\Spress\Core\Support\StringWrapper;
18
19
/**
20
 * Iterface for a permalink generator. e.g: /my-page/about-me.html.
21
 *
22
 * Attributes with special meaning:
23
 *  - permalink: (string) The permalink template.
24
 *  - preserve_path_title: (bool)
25
 *  - date: (string)
26
 *  - categories: (array)
27
 *
28
 * Placeholders:
29
 *  - ":path"		: /my-page
30
 *  - ":basename"	: about-me
31
 *  - ":extension"	: html
32
 *
33
 *
34
 * @author Victor Puertas <[email protected]>
35
 */
36
class PermalinkGenerator implements PermalinkGeneratorInterface
37
{
38
    /**
39
     * Predefined permalink 'none'.
40
     */
41
    const PERMALINK_NONE = '/:path/:basename.:extension';
42
    /**
43
     * Predefined permalink template for 'date' & 'pretty'
44
     * '/:collection' gets prepended when in a custom collection.
45
     * 'pretty' also forces option 'no_html_extension'.
46
     */
47
    const PERMALINK_DATE = '/:categories/:year/:month/:day/:title.:extension';
48
    /**
49
     * Predefined permalink 'ordinal'.
50
     */
51
    const PERMALINK_ORDINAL = '/:categories/:year/:i_day/:title.:extension';
52
53
    private $defaultPermalink;
54
    private $defaultPreservePathTitle;
55
    private $defaultNoHtmlExtension;
56
57
    /**
58
     * Constructor.
59
     *
60
     * @param string $defaultPermalink
61
     *
62
     *   Each item's URL are prefixed by "/:collection" if the item are included in a custom collection.
63
     *
64
     *   "pretty" permalink style:
65
     *    - item: "/:path/:basename"
66
     *    - item with date: "/:categories/:year/:month/:day/:title/"
67
     *
68
     *   "date" permalink style:
69
     *    - item: "/:path/:basename.:extension"
70
     *    - item with date: "/:categories/:year/:month/:day/:title.:extension"
71
     *
72
     *   "ordinal" permalink style:
73
     *    - item: "/:path/:basename.:extension"
74
     *    - item with date: "/:categories/:year/:i_day/:title.:extension"
75
     *
76
     *   "none" permalink style:
77
     *    - item: "/:path/:basename.:extension"
78
     * @param bool $defaultPreservePathTitle Default value for Preserve-path-title
79
     * @param bool $defaultNoHtmlExtension   Default value for no-html-extension
80
     */
81
    public function __construct(
82
        $defaultPermalink = 'pretty',
83
        $defaultPreservePathTitle = false,
84
        $defaultNoHtmlExtension = false
85
    ) {
86
        $this->defaultPermalink = $defaultPermalink;
87
        $this->defaultPreservePathTitle = $defaultPreservePathTitle;
88
        $this->defaultNoHtmlExtension = $defaultNoHtmlExtension;
89
    }
90
91
    /**
92
     * Gets a permalink. This method uses the SNAPSHOT_PATH_RELATIVE_AFTER_CONVERT of Item path.
93
     *
94
     * For binary items URL path and path point to SNAPSHOT_PATH_RELATIVE.
95
     *
96
     * Item's attributes with special meaning:
97
     *  - title: title of the item.
98
     *  - title_path: title extracted from the date filename pattern.
99
     *  - preserve_path_title: if true "title_path" instead of "title" will be used with ":title" placeholder.
100
     *  - date: date of item.
101
     *  - categories: categories for the item
102
     *  - permalink: permalink sytle.
103
     *  - collection: the name of the item's collection.
104
     *
105
     * @param \Yosymfony\Spress\Core\DataSource\ItemInterface $item
106
     *
107
     * @return \Yosymfony\Spress\Core\ContentManager\Permalink\PermalinkInterface
108
     */
109
    public function getPermalink(ItemInterface $item)
110
    {
111
        if ($item->getPath(ItemInterface::SNAPSHOT_PATH_RELATIVE_AFTER_CONVERT) === '') {
112
            return new Permalink('', '');
113
        }
114
115
        $placeholders = $this->getPlacehoders($item);
116
        $permalinkStyle = $this->getPermalinkAttribute($item);
117
        $noHtmlExtension = $this->getNoHtmlExtensionAttribute($item);
118
119
        if ($item->isBinary() === true) {
120
            $urlTemplate = $this::PERMALINK_NONE;
121
            $path = $this->generatePath($urlTemplate, $placeholders);
122
            $urlPath = $this->generateUrlPath($urlTemplate, $placeholders);
123
124
            return new Permalink($path, $urlPath);
125
        }
126
127
        switch ($permalinkStyle) {
128
            case 'none':
129
                $urlTemplate = $this::PERMALINK_NONE;
130
                break;
131 View Code Duplication
            case 'ordinal':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
132
                if ($this->isItemWithDate($item) === true) {
133
                    $urlTemplate = $this::PERMALINK_ORDINAL;
134
135
                    if ($this->isCustomCollection($item)) {
136
                        $urlTemplate = '/:collection'.$urlTemplate;
137
                    }
138
                } else {
139
                    $urlTemplate = $this::PERMALINK_NONE;
140
                }
141
                break;
142
            case 'pretty':
143
                $noHtmlExtension = true;
144
                // no break
145 View Code Duplication
            case 'date':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
146
                if ($this->isItemWithDate($item) === true) {
147
                    $urlTemplate = $this::PERMALINK_DATE;
148
149
                    if ($this->isCustomCollection($item)) {
150
                        $urlTemplate = '/:collection'.$urlTemplate;
151
                    }
152
                } else {
153
                    $urlTemplate = $this::PERMALINK_NONE;
154
                }
155
                break;
156
            default:
157
                if ($this->templateNeedsDate($permalinkStyle) === false || $this->isItemWithDate($item) === true) {
158
                    $urlTemplate = $permalinkStyle;
159
                } else {
160
                    $urlTemplate = $this::PERMALINK_NONE;
161
                }
162
                break;
163
        }
164
165
        if ($noHtmlExtension && $placeholders[':extension'] === 'html') {
166
            if ($placeholders[':basename'] === 'index') {
167
                $placeholders[':basename'] = '';
168
            }
169
170
            $urlTemplate = str_replace(['.:extension', ':extension'], '', $urlTemplate);
171
            $pathTemplate = $urlTemplate.'/index.html';
172
        } else {
173
            $pathTemplate = $urlTemplate;
174
        }
175
176
        $path = $this->generatePath($pathTemplate, $placeholders);
177
        $urlPath = $this->generateUrlPath($urlTemplate, $placeholders);
178
179
        return new Permalink($path, $urlPath);
180
    }
181
182
    private function getPlacehoders(ItemInterface $item)
183
    {
184
        $fileInfo = new \SplFileInfo($item->getPath(ItemInterface::SNAPSHOT_PATH_RELATIVE_AFTER_CONVERT));
185
186
        $result = [
187
            ':path' => (new StringWrapper($fileInfo->getPath()))->deletePrefix('.'),
188
            ':extension' => $fileInfo->getExtension(),
189
            ':basename' => $fileInfo->getBasename('.'.$fileInfo->getExtension()),
190
            ':collection' => $item->getCollection(),
191
            ':categories' => $this->getCategoriesPath($item),
192
            ':title' => $this->getTitleSlugified($item),
193
        ];
194
195
        if ($this->isItemWithDate($item)) {
196
            $time = $this->getDateAttribute($item);
197
            $result += [
198
                ':year' => $time->format('Y'),
199
                ':month' => $time->format('m'),
200
                ':day' => $time->format('d'),
201
                ':i_month' => $time->format('n'),
202
                ':i_day' => $time->format('j'),
203
            ];
204
        }
205
206
        return $result;
207
    }
208
209
    private function isItemWithDate(ItemInterface $item)
210
    {
211
        $attributes = $item->getAttributes();
212
213
        return isset($attributes['date']);
214
    }
215
216
    private function templateNeedsDate($template)
217
    {
218
        return strpos($template, ':year') !== false
219
            || strpos($template, ':month') !== false
220
            || strpos($template, ':day') !== false
221
            || strpos($template, ':i_month') !== false
222
            || strpos($template, ':i_day') !== false;
223
    }
224
225
    private function getTitleSlugified(ItemInterface $item)
226
    {
227
        $attributes = $item->getAttributes();
228
229
        $preservePathTitle = $this->getPreservePathTitleAttribute($item);
230
231
        if ($preservePathTitle === true && isset($attributes['title_path']) === true) {
232
            return rawurlencode($attributes['title_path']);
233
        }
234
235
        if (isset($attributes['title']) === true) {
236
            return (new StringWrapper($attributes['title']))->slug();
237
        }
238
    }
239
240
    private function getCategoriesPath(ItemInterface $item)
241
    {
242
        $attributes = $item->getAttributes();
243
244
        if (isset($attributes['categories']) === false) {
245
            return;
246
        }
247
248
        if (is_array($attributes['categories']) === false) {
249
            throw new AttributeValueException('Invalid value. Expected array.', 'categories', $item->getPath());
250
        }
251
252
        return implode('/', array_map(function ($a) {
253
            return (new StringWrapper($a))->slug();
254
        }, $attributes['categories']));
255
    }
256
257
    private function generatePath($template, array $placeholders = [])
258
    {
259
        $path = $this->generateUrlPath($template, $placeholders);
260
261
        return ltrim($path, '/');
262
    }
263
264
    private function generateUrlPath($template, array $placeholders = [])
265
    {
266
        if (0 == strlen($template)) {
267
            throw new \InvalidArgumentException('The template param must be a template or a URL.');
268
        }
269
270
        $permalink = str_replace(array_keys($placeholders), $placeholders, $template, $count);
271
272
        return $this->sanitize($permalink);
273
    }
274
275 View Code Duplication
    private function getPreservePathTitleAttribute(ItemInterface $item)
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...
276
    {
277
        $attributes = $item->getAttributes();
278
279
        if (isset($attributes['preserve_path_title']) === true) {
280
            if (is_bool($attributes['preserve_path_title']) === false) {
281
                throw new AttributeValueException('Invalid value. Expected bolean.', 'preserve_path_title', $item->getPath());
282
            }
283
284
            return $attributes['preserve_path_title'];
285
        }
286
287
        return $this->defaultPreservePathTitle;
288
    }
289
290
    private function getDateAttribute(ItemInterface $item)
291
    {
292
        $attributes = $item->getAttributes();
293
294
        if (isset($attributes['date']) === false) {
295
            throw new MissingAttributeException('Attribute date required.', 'date', $item->getPath());
296
        }
297
298
        if (is_string($attributes['date']) === false) {
299
            throw new AttributeValueException('Invalid value. Expected date string.', 'date', $item->getPath());
300
        }
301
302
        try {
303
            return new \DateTime($attributes['date']);
304
        } catch (\Exception $e) {
305
            throw new AttributeValueException('Invalid value. Expected date string.', 'date', $item->getPath());
306
        }
307
    }
308
309 View Code Duplication
    private function getNoHtmlExtensionAttribute(ItemInterface $item)
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...
310
    {
311
        $attributes = $item->getAttributes();
312
313
        if (isset($attributes['no_html_extension']) === true) {
314
            if (is_bool($attributes['no_html_extension']) === false) {
315
                throw new AttributeValueException('Invalid value. Expected boolean.', 'no_html_extension', $item->getPath());
316
            }
317
318
            return $attributes['no_html_extension'];
319
        }
320
321
        return $this->defaultNoHtmlExtension;
322
    }
323
324
    private function isCustomCollection(ItemInterface $item)
325
    {
326
        return !in_array($item->getCollection(), ['posts', 'pages']);
327
    }
328
329
    private function getPermalinkAttribute(ItemInterface $item)
330
    {
331
        $attributes = $item->getAttributes();
332
        $permalink = isset($attributes['permalink']) ? $attributes['permalink'] : $this->defaultPermalink;
333
334
        if (is_string($permalink) === false) {
335
            throw new AttributeValueException('Invalid value. Expected string.', 'permalink', $item->getPath());
336
        }
337
338
        if (trim($permalink) === '') {
339
            throw new AttributeValueException('Invalid value. Expected a non-empty value.', 'permalink', $item->getPath());
340
        }
341
342
        return $permalink;
343
    }
344
345
    private function sanitize($url)
346
    {
347
        $count = 0;
348
349
        if ((new StringWrapper($url))->startWith('/') === false) {
350
            $url = '/'.$url;
351
        }
352
353
        $result = preg_replace('/\/\/+/', '/', $url);
354
        $result = str_replace(':/', '://', $result, $count);
355
356
        if ($result !== '/') {
357
            $result = rtrim($result, '/');
358
        }
359
360
        if ($count > 1) {
361
            throw new \UnexpectedValueException(sprintf('Bad URL: "%s".', $result));
362
        }
363
364
        if (false !== strpos($result, ' ')) {
365
            throw new \UnexpectedValueException(sprintf('Bad URL: "%s". Contain white space/s.', $result));
366
        }
367
368
        return $result;
369
    }
370
}
371