Passed
Push — menu ( 2edc93...bd9bd5 )
by Arnaud
14:43 queued 11:06
created

Page   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 646
Duplicated Lines 0 %

Test Coverage

Coverage 92.96%

Importance

Changes 4
Bugs 3 Features 0
Metric Value
eloc 184
c 4
b 3
f 0
dl 0
loc 646
ccs 185
cts 199
cp 0.9296
rs 2.4
wmc 75

34 Methods

Rating   Name   Duplication   Size   Complexity  
A createId() 0 12 3
A slugify() 0 7 2
A __construct() 0 14 1
A isVirtual() 0 3 1
B setFile() 0 55 7
A getType() 0 3 1
A getPath() 0 3 1
A setBodyHtml() 0 5 1
A getBodyHtml() 0 3 1
A getFrontmatter() 0 3 1
A getFolder() 0 3 1
A setVirtual() 0 5 1
A setFolder() 0 5 1
A getFmVariables() 0 3 1
A setVariables() 0 7 2
A getVariable() 0 4 2
B getOutputFile() 0 39 10
A parse() 0 8 1
A getIdWithoutLang() 0 3 1
A hasVariable() 0 3 1
A setType() 0 5 1
A getBody() 0 3 1
A setFmVariables() 0 5 1
A setSection() 0 5 1
A getVariables() 0 3 1
A getUrl() 0 10 3
A setPath() 0 32 6
A getSection() 0 3 2
A unVariable() 0 7 2
B setVariable() 0 45 10
A getSlug() 0 3 1
A setSlug() 0 12 4
A getContent() 0 3 1
A getPathname() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Page 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.

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 Page, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the Cecil/Cecil package.
4
 *
5
 * Copyright (c) Arnaud Ligny <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace Cecil\Collection\Page;
14
15
use Cecil\Collection\Item;
16
use Cecil\Config;
17
use Cecil\Util;
18
use Cocur\Slugify\Slugify;
19
use Symfony\Component\Finder\SplFileInfo;
20
21
/**
22
 * Class Page.
23
 */
24
class Page extends Item
25
{
26
    const SLUGIFY_PATTERN = '/(^\/|[^._a-z0-9\/]|-)+/'; // should be '/^\/|[^_a-z0-9\/]+/'
27
28
    /** @var bool True if page is not created from a Markdown file. */
29
    protected $virtual;
30
    /** @var SplFileInfo */
31
    protected $file;
32
    /** @var string Homepage, Page, Section, etc. */
33
    protected $type;
34
    /** @var string */
35
    protected $folder;
36
    /** @var string */
37
    protected $slug;
38
    /** @var string folder + slug */
39
    protected $path;
40
    /** @var string */
41
    protected $section;
42
    /** @var string */
43
    protected $frontmatter;
44
    /** @var string Body before conversion. */
45
    protected $body;
46
    /** @var array Front matter before conversion. */
47
    protected $fmVariables = [];
48
    /** @var string Body after Markdown conversion. */
49
    protected $html;
50
    /** @var Slugify */
51
    private static $slugifier;
52
53
    /**
54
     * @param string $id
55
     */
56 1
    public function __construct(string $id)
57
    {
58 1
        parent::__construct($id);
59 1
        $this->setVirtual(true);
60 1
        $this->setType(Type::PAGE);
61
        // default variables
62 1
        $this->setVariables([
63 1
            'title'            => 'Page Title',
64 1
            'date'             => new \DateTime(),
65 1
            'updated'          => new \DateTime(),
66
            'weight'           => null,
67
            'filepath'         => null,
68
            'published'        => true,
69 1
            'content_template' => 'page.content.twig',
70
        ]);
71 1
    }
72
73
    /**
74
     * Turns a path (string) into a slug (URI).
75
     *
76
     * @param string $path
77
     *
78
     * @return string
79
     */
80 1
    public static function slugify(string $path): string
81
    {
82 1
        if (!self::$slugifier instanceof Slugify) {
83 1
            self::$slugifier = Slugify::create(['regexp' => self::SLUGIFY_PATTERN]);
84
        }
85
86 1
        return self::$slugifier->slugify($path);
87
    }
88
89
    /**
90
     * Creates the ID from the file path.
91
     *
92
     * @param SplFileInfo $file
93
     *
94
     * @return string
95
     */
96 1
    public static function createId(SplFileInfo $file): string
97
    {
98 1
        $relativepath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
99 1
        $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.'.$file->getExtension())));
100
        // case of "README" -> index
101 1
        $basename = str_ireplace('readme', 'index', $basename);
102
        // case of section's index: "section/index" -> "section"
103 1
        if (!empty($relativepath) && $basename == 'index') {
104 1
            return $relativepath;
105
        }
106
107 1
        return trim(Util::joinPath($relativepath, $basename), '/');
0 ignored issues
show
Bug introduced by
It seems like $basename can also be of type array; however, parameter $path of Cecil\Util::joinPath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

107
        return trim(Util::joinPath($relativepath, /** @scrutinizer ignore-type */ $basename), '/');
Loading history...
108
    }
109
110
    /**
111
     * Returns the Id of a page withour language suffix.
112
     *
113
     * @return string
114
     */
115 1
    public function getIdWithoutLang(): string
116
    {
117 1
        return PrefixSuffix::sub($this->getId());
118
    }
119
120
    /**
121
     * Set file.
122
     *
123
     * @param SplFileInfo $file
124
     *
125
     * @return self
126
     */
127 1
    public function setFile(SplFileInfo $file): self
128
    {
129 1
        $this->setVirtual(false);
130 1
        $this->file = $file;
131
132
        /*
133
         * File path components
134
         */
135 1
        $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
136 1
        $fileExtension = $this->file->getExtension();
137 1
        $fileName = $this->file->getBasename('.'.$fileExtension);
138
        // case of "README" -> "index"
139 1
        $fileName = str_ireplace('readme', 'index', $fileName);
140
        // case of "index" = home page
141 1
        if (empty($this->file->getRelativePath()) && PrefixSuffix::sub($fileName) == 'index') {
0 ignored issues
show
Bug introduced by
It seems like $fileName can also be of type array; however, parameter $string of Cecil\Collection\Page\PrefixSuffix::sub() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

141
        if (empty($this->file->getRelativePath()) && PrefixSuffix::sub(/** @scrutinizer ignore-type */ $fileName) == 'index') {
Loading history...
142 1
            $this->setType(Type::HOMEPAGE);
143
        }
144
        /*
145
         * Set protected variables
146
         */
147 1
        $this->setFolder($fileRelativePath); // ie: "blog"
148 1
        $this->setSlug($fileName); // ie: "post-1"
0 ignored issues
show
Bug introduced by
It seems like $fileName can also be of type array; however, parameter $slug of Cecil\Collection\Page\Page::setSlug() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

148
        $this->setSlug(/** @scrutinizer ignore-type */ $fileName); // ie: "post-1"
Loading history...
149 1
        $this->setPath($this->getFolder().'/'.$this->getSlug()); // ie: "blog/post-1"
150
        /*
151
         * Set default variables
152
         */
153 1
        $this->setVariables([
154 1
            'title'    => PrefixSuffix::sub($fileName),
155 1
            'date'     => (new \DateTime())->setTimestamp($this->file->getCTime()),
156 1
            'updated'  => (new \DateTime())->setTimestamp($this->file->getMTime()),
157 1
            'filepath' => $this->file->getRelativePathname(),
158
        ]);
159
        /*
160
         * Set specific variables
161
         */
162
        // is file has a prefix?
163 1
        if (PrefixSuffix::hasPrefix($fileName)) {
0 ignored issues
show
Bug introduced by
It seems like $fileName can also be of type array; however, parameter $string of Cecil\Collection\Page\PrefixSuffix::hasPrefix() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
        if (PrefixSuffix::hasPrefix(/** @scrutinizer ignore-type */ $fileName)) {
Loading history...
164 1
            $prefix = PrefixSuffix::getPrefix($fileName);
0 ignored issues
show
Bug introduced by
It seems like $fileName can also be of type array; however, parameter $string of Cecil\Collection\Page\PrefixSuffix::getPrefix() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

164
            $prefix = PrefixSuffix::getPrefix(/** @scrutinizer ignore-type */ $fileName);
Loading history...
165 1
            if ($prefix !== null) {
166
                // prefix is a valid date?
167 1
                if (Util\Date::isDateValid($prefix)) {
168 1
                    $this->setVariable('date', (string) $prefix);
169
                } else {
170
                    // prefix is an integer: used for sorting
171 1
                    $this->setVariable('weight', (int) $prefix);
172
                }
173
            }
174
        }
175
        // is file has a language suffix?
176 1
        if (PrefixSuffix::hasSuffix($fileName)) {
0 ignored issues
show
Bug introduced by
It seems like $fileName can also be of type array; however, parameter $string of Cecil\Collection\Page\PrefixSuffix::hasSuffix() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
        if (PrefixSuffix::hasSuffix(/** @scrutinizer ignore-type */ $fileName)) {
Loading history...
177 1
            $this->setVariable('language', PrefixSuffix::getSuffix($fileName));
0 ignored issues
show
Bug introduced by
It seems like $fileName can also be of type array; however, parameter $string of Cecil\Collection\Page\PrefixSuffix::getSuffix() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

177
            $this->setVariable('language', PrefixSuffix::getSuffix(/** @scrutinizer ignore-type */ $fileName));
Loading history...
178
        }
179 1
        $this->setVariable('langref', PrefixSuffix::sub($fileName));
180
181 1
        return $this;
182
    }
183
184
    /**
185
     * Parse file content.
186
     *
187
     * @return self
188
     */
189 1
    public function parse(): self
190
    {
191 1
        $parser = new Parser($this->file);
192 1
        $parsed = $parser->parse();
193 1
        $this->frontmatter = $parsed->getFrontmatter();
194 1
        $this->body = $parsed->getBody();
195
196 1
        return $this;
197
    }
198
199
    /**
200
     * Get frontmatter.
201
     *
202
     * @return string|null
203
     */
204 1
    public function getFrontmatter(): ?string
205
    {
206 1
        return $this->frontmatter;
207
    }
208
209
    /**
210
     * Get body as raw.
211
     *
212
     * @return string
213
     */
214 1
    public function getBody(): ?string
215
    {
216 1
        return $this->body;
217
    }
218
219
    /**
220
     * Set virtual status.
221
     *
222
     * @param bool $virtual
223
     *
224
     * @return self
225
     */
226 1
    public function setVirtual(bool $virtual): self
227
    {
228 1
        $this->virtual = $virtual;
229
230 1
        return $this;
231
    }
232
233
    /**
234
     * Is current page is virtual?
235
     *
236
     * @return bool
237
     */
238 1
    public function isVirtual(): bool
239
    {
240 1
        return $this->virtual;
241
    }
242
243
    /**
244
     * Set page type.
245
     *
246
     * @param string $type
247
     *
248
     * @return self
249
     */
250 1
    public function setType(string $type): self
251
    {
252 1
        $this->type = new Type($type);
253
254 1
        return $this;
255
    }
256
257
    /**
258
     * Get page type.
259
     *
260
     * @return string
261
     */
262 1
    public function getType(): string
263
    {
264 1
        return (string) $this->type;
265
    }
266
267
    /**
268
     * Set path without slug.
269
     *
270
     * @param string $folder
271
     *
272
     * @return self
273
     */
274 1
    public function setFolder(string $folder): self
275
    {
276 1
        $this->folder = self::slugify($folder);
277
278 1
        return $this;
279
    }
280
281
    /**
282
     * Get path without slug.
283
     *
284
     * @return string|null
285
     */
286 1
    public function getFolder(): ?string
287
    {
288 1
        return $this->folder;
289
    }
290
291
    /**
292
     * Set slug.
293
     *
294
     * @param string $slug
295
     *
296
     * @return self
297
     */
298 1
    public function setSlug(string $slug): self
299
    {
300 1
        if (!$this->slug) {
301 1
            $slug = self::slugify(PrefixSuffix::sub($slug));
302
        }
303
        // force slug and update path
304 1
        if ($this->slug && $this->slug != $slug) {
305 1
            $this->setPath($this->getFolder().'/'.$slug);
306
        }
307 1
        $this->slug = $slug;
308
309 1
        return $this;
310
    }
311
312
    /**
313
     * Get slug.
314
     *
315
     * @return string
316
     */
317 1
    public function getSlug(): string
318
    {
319 1
        return $this->slug;
320
    }
321
322
    /**
323
     * Set path.
324
     *
325
     * @param string $path
326
     *
327
     * @return self
328
     */
329 1
    public function setPath(string $path): self
330
    {
331 1
        $path = self::slugify(PrefixSuffix::sub($path));
332
333
        // case of homepage
334 1
        if ($path == 'index') {
335 1
            $this->path = '';
336
337 1
            return $this;
338
        }
339
340
        // case of custom sections' index (ie: content/section/index.md)
341 1
        if (substr($path, -6) == '/index') {
342 1
            $path = substr($path, 0, strlen($path) - 6);
343
        }
344 1
        $this->path = $path;
345
346
        // case of root pages
347 1
        $lastslash = strrpos($this->path, '/');
348 1
        if ($lastslash === false) {
349 1
            $this->slug = $this->path;
350
351 1
            return $this;
352
        }
353
354 1
        if (!$this->virtual && $this->getSection() === null) {
355 1
            $this->section = explode('/', $this->path)[0];
356
        }
357 1
        $this->folder = substr($this->path, 0, $lastslash);
358 1
        $this->slug = substr($this->path, -(strlen($this->path) - $lastslash - 1));
359
360 1
        return $this;
361
    }
362
363
    /**
364
     * Get path.
365
     *
366
     * @return string|null
367
     */
368 1
    public function getPath(): ?string
369
    {
370 1
        return $this->path;
371
    }
372
373
    /**
374
     * @see getPath()
375
     *
376
     * @return string|null
377
     */
378
    public function getPathname(): ?string
379
    {
380
        return $this->getPath();
381
    }
382
383
    /**
384
     * Set section.
385
     *
386
     * @param string $section
387
     *
388
     * @return self
389
     */
390 1
    public function setSection(string $section): self
391
    {
392 1
        $this->section = $section;
393
394 1
        return $this;
395
    }
396
397
    /**
398
     * Get section.
399
     *
400
     * @return string|null
401
     */
402 1
    public function getSection(): ?string
403
    {
404 1
        return !empty($this->section) ? $this->section : null;
405
    }
406
407
    /**
408
     * Set body as HTML.
409
     *
410
     * @param string $html
411
     *
412
     * @return self
413
     */
414 1
    public function setBodyHtml(string $html): self
415
    {
416 1
        $this->html = $html;
417
418 1
        return $this;
419
    }
420
421
    /**
422
     * Get body as HTML.
423
     *
424
     * @return string|null
425
     */
426 1
    public function getBodyHtml(): ?string
427
    {
428 1
        return $this->html;
429
    }
430
431
    /**
432
     * @see getBodyHtml()
433
     *
434
     * @return string|null
435
     */
436 1
    public function getContent(): ?string
437
    {
438 1
        return $this->getBodyHtml();
439
    }
440
441
    /**
442
     * Returns the path to the output (rendered) file.
443
     *
444
     * Use cases:
445
     * - default: path + suffix + extension (ie: blog/post-1/index.html)
446
     * - subpath: path + subpath + suffix + extension (ie: blog/post-1/amp/index.html)
447
     * - ugly: path + extension (ie: 404.html, sitemap.xml, robots.txt)
448
     * - path only (ie: _redirects)
449
     * - i18n: language + path + suffix + extension (ie: fr/blog/page/index.html)
450
     *
451
     * @param string      $format
452
     * @param Config|null $config
453
     *
454
     * @return string
455
     */
456 1
    public function getOutputFile(string $format, Config $config = null): string
457
    {
458 1
        $path = $this->getPath();
459 1
        $subpath = '';
460 1
        $suffix = '/index';
461 1
        $extension = 'html';
462 1
        $uglyurl = (bool) $this->getVariable('uglyurl');
463 1
        $language = $this->getVariable('language');
464
465
        // site config
466 1
        if ($config) {
467 1
            $subpath = (string) $config->getOutputFormatProperty($format, 'subpath');
468 1
            $suffix = (string) $config->getOutputFormatProperty($format, 'suffix');
469 1
            $extension = (string) $config->getOutputFormatProperty($format, 'extension');
470
        }
471
472
        // if ugly URL: not suffix
473 1
        if ($uglyurl) {
474 1
            $suffix = null;
475
        }
476
        // formatting strings
477 1
        if ($subpath) {
478
            $subpath = \sprintf('/%s', ltrim($subpath, '/'));
479
        }
480 1
        if ($suffix) {
481 1
            $suffix = \sprintf('%s%s', empty($path) ? '' : '/', ltrim($suffix, '/'));
482
        }
483 1
        if ($extension) {
484 1
            $extension = \sprintf('.%s', $extension);
485
        }
486 1
        if (!is_null($language)) {
487 1
            $language = \sprintf('%s/', $language);
488
        }
489
        // homepage special case: path = 'index'
490 1
        if (empty($path) && empty($suffix)) {
491 1
            $path = 'index';
492
        }
493
494 1
        return $language.$path.$subpath.$suffix.$extension;
495
    }
496
497
    /**
498
     * Returns the public URL.
499
     *
500
     * @param string      $format Output format (ie: html, amp, json, etc.)
501
     * @param Config|null $config
502
     *
503
     * @return string
504
     */
505 1
    public function getUrl(string $format = 'html', Config $config = null): string
506
    {
507 1
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
508 1
        $output = $this->getOutputFile($format, $config);
509
510 1
        if (!$uglyurl) {
511 1
            $output = str_replace('index.html', '', $output);
512
        }
513
514 1
        return $output;
515
    }
516
517
    /*
518
     * Helpers to set and get variables.
519
     */
520
521
    /**
522
     * Set an array as variables.
523
     *
524
     * @param array $variables
525
     *
526
     * @throws \Exception
527
     *
528
     * @return self
529
     */
530 1
    public function setVariables(array $variables): self
531
    {
532 1
        foreach ($variables as $key => $value) {
533 1
            $this->setVariable($key, $value);
534
        }
535
536 1
        return $this;
537
    }
538
539
    /**
540
     * Get all variables.
541
     *
542
     * @return array
543
     */
544 1
    public function getVariables(): array
545
    {
546 1
        return $this->properties;
547
    }
548
549
    /**
550
     * Set a variable.
551
     *
552
     * @param string $name
553
     * @param mixed  $value
554
     *
555
     * @throws \Exception
556
     *
557
     * @return self
558
     */
559 1
    public function setVariable(string $name, $value): self
560
    {
561 1
        if (is_bool($value)) {
562 1
            $value = $value ?: 0;
563
        }
564
        switch ($name) {
565 1
            case 'date':
566
                try {
567 1
                    $date = Util\Date::dateToDatetime($value);
568
                } catch (\Exception $e) {
569
                    throw new \Exception(sprintf(
570
                        'Expected date format (ie: "2012-10-08") for "date" in "%s" instead of "%s"',
571
                        $this->getId(),
572
                        (string) $value
573
                    ));
574
                }
575 1
                $this->offsetSet('date', $date);
576 1
                break;
577 1
            case 'draft':
578 1
                if ($value === true) {
579 1
                    $this->offsetSet('published', false);
580
                }
581 1
                break;
582 1
            case 'path':
583 1
            case 'slug':
584 1
                $slugify = self::slugify((string) $value);
585 1
                if ($value != $slugify) {
586
                    throw new \Exception(sprintf(
587
                        '"%s" variable should be "%s" (not "%s") in "%s"',
588
                        $name,
589
                        $slugify,
590
                        (string) $value,
591
                        $this->getId()
592
                    ));
593
                }
594
                /** @see setPath() */
595
                /** @see setSlug() */
596 1
                $method = 'set'.\ucfirst($name);
597 1
                $this->$method($value);
598 1
                break;
599
            default:
600 1
                $this->offsetSet($name, $value);
601
        }
602
603 1
        return $this;
604
    }
605
606
    /**
607
     * Is variable exists?
608
     *
609
     * @param string $name
610
     *
611
     * @return bool
612
     */
613 1
    public function hasVariable(string $name): bool
614
    {
615 1
        return $this->offsetExists($name);
616
    }
617
618
    /**
619
     * Get a variable.
620
     *
621
     * @param string $name
622
     *
623
     * @return mixed|null
624
     */
625 1
    public function getVariable(string $name)
626
    {
627 1
        if ($this->offsetExists($name)) {
628 1
            return $this->offsetGet($name);
629
        }
630 1
    }
631
632
    /**
633
     * Unset a variable.
634
     *
635
     * @param string $name
636
     *
637
     * @return self
638
     */
639 1
    public function unVariable(string $name): self
640
    {
641 1
        if ($this->offsetExists($name)) {
642 1
            $this->offsetUnset($name);
643
        }
644
645 1
        return $this;
646
    }
647
648
    /**
649
     * Set front matter (only) variables.
650
     *
651
     * @param array $variables
652
     *
653
     * @return self
654
     */
655 1
    public function setFmVariables(array $variables): self
656
    {
657 1
        $this->fmVariables = $variables;
658
659 1
        return $this;
660
    }
661
662
    /**
663
     * Get front matter variables.
664
     *
665
     * @return array
666
     */
667 1
    public function getFmVariables(): array
668
    {
669 1
        return $this->fmVariables;
670
    }
671
}
672