Passed
Pull Request — master (#2215)
by Arnaud
08:31 queued 03:40
created

Page   F

Complexity

Total Complexity 98

Size/Duplication

Total Lines 686
Duplicated Lines 0 %

Test Coverage

Coverage 92.54%

Importance

Changes 4
Bugs 2 Features 0
Metric Value
eloc 200
c 4
b 2
f 0
dl 0
loc 686
ccs 211
cts 228
cp 0.9254
rs 2
wmc 98

47 Methods

Rating   Name   Duplication   Size   Complexity  
A getFrontmatter() 0 3 1
A isVirtual() 0 3 1
A getType() 0 3 1
A getFolder() 0 3 1
A setFolder() 0 5 1
A setVirtual() 0 5 1
A parse() 0 8 1
A setType() 0 5 1
A getBody() 0 3 1
A getSlug() 0 3 1
A setId() 0 3 1
A getIdWithoutLang() 0 8 3
A slugify() 0 7 2
A __toString() 0 3 1
A __construct() 0 26 4
A getPath() 0 3 1
A getBodyHtml() 0 3 1
A getRendered() 0 3 1
A getSection() 0 3 2
A getContent() 0 3 1
B setFile() 0 47 9
A setBodyHtml() 0 5 1
A getPagination() 0 3 1
A getPages() 0 3 1
A getFmVariables() 0 3 1
A setVariables() 0 7 2
A getVariable() 0 7 2
A createIdFromFile() 0 21 6
A setTerms() 0 5 1
A hasVariable() 0 3 1
A setFmVariables() 0 5 1
A addRendered() 0 5 1
A getFileName() 0 7 2
A unSection() 0 5 1
A getPaginator() 0 3 1
A setPaginator() 0 5 1
A setSection() 0 5 1
A getVariables() 0 3 1
A setPath() 0 35 6
A unVariable() 0 7 2
A setPages() 0 5 1
C setVariable() 0 50 17
A getFilePath() 0 7 3
A setSlug() 0 12 4
A getTerms() 0 3 1
A filterBool() 0 5 2
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
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Cecil\Collection\Page;
15
16
use Cecil\Collection\Item;
17
use Cecil\Exception\RuntimeException;
18
use Cecil\Util;
19
use Cocur\Slugify\Slugify;
20
use Symfony\Component\Finder\SplFileInfo;
21
22
/**
23
 * Page class.
24
 *
25
 * Represents a page in the collection, which can be created from a file or be virtual.
26
 * Provides methods to manage page properties, variables, and rendering.
27
 */
28
class Page extends Item
29
{
30
    public const SLUGIFY_PATTERN = '/(^\/|[^._a-z0-9\/]|-)+/'; // should be '/^\/|[^_a-z0-9\/]+/'
31
32
    /** @var bool True if page is not created from a file. */
33
    protected $virtual;
34
35
    /** @var SplFileInfo */
36
    protected $file;
37
38
    /** @var Type Type */
39
    protected $type;
40
41
    /** @var string */
42
    protected $folder;
43
44
    /** @var string */
45
    protected $slug;
46
47
    /** @var string path = folder + slug. */
48
    protected $path;
49
50
    /** @var string */
51
    protected $section;
52
53
    /** @var string */
54
    protected $frontmatter;
55
56
    /** @var array Front matter before conversion. */
57
    protected $fmVariables = [];
58
59
    /** @var string Body before conversion. */
60
    protected $body;
61
62
    /** @var string Body after conversion. */
63
    protected $html;
64
65
    /** @var array Output, by format */
66
    protected $rendered = [];
67
68
    /** @var Collection Subpages of a list page. */
69
    protected $subPages;
70
71
    /** @var array */
72
    protected $paginator = [];
73
74
    /** @var \Cecil\Collection\Taxonomy\Vocabulary Terms of a vocabulary. */
75
    protected $terms;
76
77
    /** @var Slugify */
78
    private static $slugifier;
79
80 1
    public function __construct(mixed $id)
81
    {
82 1
        if (!\is_string($id) && !$id instanceof SplFileInfo) {
83
            throw new RuntimeException('Create a page with a string ID or a SplFileInfo.');
84
        }
85
86
        // default properties
87 1
        $this->setVirtual(true);
88 1
        $this->setType(Type::PAGE->value);
89 1
        $this->setVariables([
90 1
            'title'            => 'Page Title',
91 1
            'date'             => new \DateTime(),
92 1
            'updated'          => new \DateTime(),
93 1
            'weight'           => null,
94 1
            'filepath'         => null,
95 1
            'published'        => true,
96 1
            'content_template' => 'page.content.twig',
97 1
        ]);
98
99 1
        if ($id instanceof SplFileInfo) {
100 1
            $file = $id;
101 1
            $this->setFile($file);
102 1
            $id = self::createIdFromFile($file);
103
        }
104
105 1
        parent::__construct($id);
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111 1
    public function setId(string $id): self
112
    {
113 1
        return parent::setId($id);
114
    }
115
116
    /**
117
     * toString magic method to prevent Twig get_attribute fatal error.
118
     *
119
     * @return string
120
     */
121 1
    public function __toString()
122
    {
123 1
        return $this->getId();
124
    }
125
126
    /**
127
     * Turns a path (string) into a slug (URI).
128
     */
129 1
    public static function slugify(string $path): string
130
    {
131 1
        if (!self::$slugifier instanceof Slugify) {
132 1
            self::$slugifier = Slugify::create(['regexp' => self::SLUGIFY_PATTERN]);
133
        }
134
135 1
        return self::$slugifier->slugify($path);
136
    }
137
138
    /**
139
     * Returns the ID of a page without language.
140
     */
141 1
    public function getIdWithoutLang(): string
142
    {
143 1
        $langPrefix = $this->getVariable('language') . '/';
144 1
        if ($this->hasVariable('language') && Util\Str::startsWith($this->getId(), $langPrefix)) {
145 1
            return substr($this->getId(), \strlen($langPrefix));
146
        }
147
148 1
        return $this->getId();
149
    }
150
151
    /**
152
     * Set file.
153
     */
154 1
    public function setFile(SplFileInfo $file): self
155
    {
156 1
        $this->file = $file;
157
158 1
        $relativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
159 1
        $filename = $this->file->getFilenameWithoutExtension();
160
        // renames "README" to "index"
161 1
        $filename = strtolower($filename) == 'readme' ? 'index' : $filename;
162
163
        // page properties
164 1
        $this->setVirtual(false);
165 1
        $this->setFolder($relativePath);
166 1
        $this->setSlug($filename);
167 1
        $this->setPath($this->getFolder() . '/' . $this->getSlug());
168
        // if "index", type = "homepage"
169 1
        if (empty($this->file->getRelativePath()) && PrefixSuffix::sub($filename) == 'index') {
170 1
            $this->setType(Type::HOMEPAGE->value);
171
        }
172
        // set variables
173 1
        $this->setVariables([
174 1
            'title'    => PrefixSuffix::sub($filename),
175 1
            'date'     => (new \DateTime())->setTimestamp($this->file->getMTime()),
176 1
            'updated'  => (new \DateTime())->setTimestamp($this->file->getMTime()),
177 1
            'filepath' => $this->file->getRelativePathname(),
178 1
        ]);
179
        // prefix : set weight or date
180 1
        if (PrefixSuffix::hasPrefix($filename)) {
181 1
            $prefix = PrefixSuffix::getPrefix($filename);
182 1
            if ($prefix !== null) {
183
                // prefix is an integer: used for sorting
184 1
                if (is_numeric($prefix)) {
185 1
                    $this->setVariable('weight', (int) $prefix);
186
                }
187
                // prefix is a valid date?
188 1
                if (Util\Date::isValid($prefix)) {
189 1
                    $this->setVariable('date', (string) $prefix);
190
                }
191
            }
192
        }
193
        // suffix : set language
194 1
        if (PrefixSuffix::hasSuffix($filename)) {
195 1
            $this->setVariable('language', PrefixSuffix::getSuffix($filename));
196
        }
197
        // set reference between page's translations, even if it exist in only one language
198 1
        $this->setVariable('langref', $this->getPath());
199
200 1
        return $this;
201
    }
202
203
    /**
204
     * Returns file name, with extension.
205
     */
206
    public function getFileName(): ?string
207
    {
208
        if ($this->file === null) {
209
            return null;
210
        }
211
212
        return $this->file->getBasename();
213
    }
214
215
    /**
216
     * Returns file real path.
217
     */
218 1
    public function getFilePath(): ?string
219
    {
220 1
        if ($this->file === null) {
221
            return null;
222
        }
223
224 1
        return $this->file->getRealPath() === false ? null : $this->file->getRealPath();
225
    }
226
227
    /**
228
     * Parse file content.
229
     */
230 1
    public function parse(): self
231
    {
232 1
        $parser = new Parser($this->file);
233 1
        $parsed = $parser->parse();
234 1
        $this->frontmatter = $parsed->getFrontmatter();
235 1
        $this->body = $parsed->getBody();
236
237 1
        return $this;
238
    }
239
240
    /**
241
     * Get front matter.
242
     */
243 1
    public function getFrontmatter(): ?string
244
    {
245 1
        return $this->frontmatter;
246
    }
247
248
    /**
249
     * Get body as raw.
250
     */
251 1
    public function getBody(): ?string
252
    {
253 1
        return $this->body;
254
    }
255
256
    /**
257
     * Set virtual status.
258
     */
259 1
    public function setVirtual(bool $virtual): self
260
    {
261 1
        $this->virtual = $virtual;
262
263 1
        return $this;
264
    }
265
266
    /**
267
     * Is current page is virtual?
268
     */
269 1
    public function isVirtual(): bool
270
    {
271 1
        return $this->virtual;
272
    }
273
274
    /**
275
     * Set page type.
276
     */
277 1
    public function setType(string $type): self
278
    {
279 1
        $this->type = Type::from($type);
280
281 1
        return $this;
282
    }
283
284
    /**
285
     * Get page type.
286
     */
287 1
    public function getType(): string
288
    {
289 1
        return $this->type->value;
290
    }
291
292
    /**
293
     * Set path without slug.
294
     */
295 1
    public function setFolder(string $folder): self
296
    {
297 1
        $this->folder = self::slugify($folder);
298
299 1
        return $this;
300
    }
301
302
    /**
303
     * Get path without slug.
304
     */
305 1
    public function getFolder(): ?string
306
    {
307 1
        return $this->folder;
308
    }
309
310
    /**
311
     * Set slug.
312
     */
313 1
    public function setSlug(string $slug): self
314
    {
315 1
        if (!$this->slug) {
316 1
            $slug = self::slugify(PrefixSuffix::sub($slug));
317
        }
318
        // force slug and update path
319 1
        if ($this->slug && $this->slug != $slug) {
320 1
            $this->setPath($this->getFolder() . '/' . $slug);
321
        }
322 1
        $this->slug = $slug;
323
324 1
        return $this;
325
    }
326
327
    /**
328
     * Get slug.
329
     */
330 1
    public function getSlug(): string
331
    {
332 1
        return $this->slug;
333
    }
334
335
    /**
336
     * Set path.
337
     */
338 1
    public function setPath(string $path): self
339
    {
340 1
        $path = trim($path, '/');
341
342
        // homepage : path is empty
343 1
        if ($path == 'index') {
344 1
            $this->path = '';
345
346 1
            return $this;
347
        }
348
349
        // section/index : path = section
350 1
        if (substr($path, -6) == '/index') {
351 1
            $path = substr($path, 0, \strlen($path) - 6);
352
        }
353 1
        $this->path = $path;
354
355 1
        $lastslash = strrpos($this->path, '/');
356
357
        // top-level pages : slug = path
358 1
        if ($lastslash === false) {
359 1
            $this->slug = $this->path;
360
361 1
            return $this;
362
        }
363
364
        // set section
365 1
        if (!$this->virtual && $this->getSection() === null) {
366 1
            $this->section = explode('/', $this->path)[0];
367
        }
368
        // set/update folder and slug
369 1
        $this->folder = substr($this->path, 0, $lastslash);
370 1
        $this->slug = substr($this->path, -(\strlen($this->path) - $lastslash - 1));
371
372 1
        return $this;
373
    }
374
375
    /**
376
     * Get path.
377
     */
378 1
    public function getPath(): ?string
379
    {
380 1
        return $this->path;
381
    }
382
383
    /**
384
     * @see getPath()
385
     */
386
    public function getPathname(): ?string
387
    {
388
        return $this->getPath();
389
    }
390
391
    /**
392
     * Set section.
393
     */
394 1
    public function setSection(string $section): self
395
    {
396 1
        $this->section = $section;
397
398 1
        return $this;
399
    }
400
401
    /**
402
     * Get section.
403
     */
404 1
    public function getSection(): ?string
405
    {
406 1
        return !empty($this->section) ? $this->section : null;
407
    }
408
409
    /**
410
     * Unset section.
411
     */
412
    public function unSection(): self
413
    {
414
        $this->section = null;
415
416
        return $this;
417
    }
418
419
    /**
420
     * Set body as HTML.
421
     */
422 1
    public function setBodyHtml(string $html): self
423
    {
424 1
        $this->html = $html;
425
426 1
        return $this;
427
    }
428
429
    /**
430
     * Get body as HTML.
431
     */
432 1
    public function getBodyHtml(): ?string
433
    {
434 1
        return $this->html;
435
    }
436
437
    /**
438
     * @see getBodyHtml()
439
     */
440 1
    public function getContent(): ?string
441
    {
442 1
        return $this->getBodyHtml();
443
    }
444
445
    /**
446
     * Add rendered.
447
     */
448 1
    public function addRendered(array $rendered): self
449
    {
450 1
        $this->rendered += $rendered;
451
452 1
        return $this;
453
    }
454
455
    /**
456
     * Get rendered.
457
     */
458 1
    public function getRendered(): array
459
    {
460 1
        return $this->rendered;
461
    }
462
463
    /**
464
     * Set Subpages.
465
     *
466
     * @todo should be removed
467
     */
468 1
    public function setPages(\Cecil\Collection\Page\Collection $subPages): self
469
    {
470 1
        $this->subPages = $subPages;
471
472 1
        return $this;
473
    }
474
475
    /**
476
     * Get Subpages.
477
     *
478
     * @todo should returns pages with "parent" contains current section page
479
     */
480 1
    public function getPages(): ?\Cecil\Collection\Page\Collection
481
    {
482 1
        return $this->subPages;
483
    }
484
485
    /**
486
     * Set paginator.
487
     */
488 1
    public function setPaginator(array $paginator): self
489
    {
490 1
        $this->paginator = $paginator;
491
492 1
        return $this;
493
    }
494
495
    /**
496
     * Get paginator.
497
     */
498 1
    public function getPaginator(): array
499
    {
500 1
        return $this->paginator;
501
    }
502
503
    /**
504
     * Paginator backward compatibility.
505
     */
506
    public function getPagination(): array
507
    {
508
        return $this->getPaginator();
509
    }
510
511
    /**
512
     * Set vocabulary terms.
513
     */
514 1
    public function setTerms(\Cecil\Collection\Taxonomy\Vocabulary $terms): self
515
    {
516 1
        $this->terms = $terms;
517
518 1
        return $this;
519
    }
520
521
    /**
522
     * Get vocabulary terms.
523
     */
524 1
    public function getTerms(): \Cecil\Collection\Taxonomy\Vocabulary
525
    {
526 1
        return $this->terms;
527
    }
528
529
    /*
530
     * Helpers to set and get variables.
531
     */
532
533
    /**
534
     * Set an array as variables.
535
     *
536
     * @throws RuntimeException
537
     */
538 1
    public function setVariables(array $variables): self
539
    {
540 1
        foreach ($variables as $key => $value) {
541 1
            $this->setVariable($key, $value);
542
        }
543
544 1
        return $this;
545
    }
546
547
    /**
548
     * Get all variables.
549
     */
550 1
    public function getVariables(): array
551
    {
552 1
        return $this->properties;
553
    }
554
555
    /**
556
     * Set a variable.
557
     *
558
     * @param string $name  Name of the variable
559
     * @param mixed  $value Value of the variable
560
     *
561
     * @throws RuntimeException
562
     */
563 1
    public function setVariable(string $name, $value): self
564
    {
565 1
        $this->filterBool($value);
566
        switch ($name) {
567 1
            case 'date':
568 1
            case 'updated':
569 1
            case 'lastmod':
570
                try {
571 1
                    $date = Util\Date::toDatetime($value);
572
                } catch (\Exception) {
573
                    throw new \Exception(\sprintf('The value of "%s" is not a valid date: "%s".', $name, var_export($value, true)));
574
                }
575 1
                $this->offsetSet($name == 'lastmod' ? 'updated' : $name, $date);
576 1
                break;
577
578 1
            case 'schedule':
579
                /*
580
                 * publish: 2012-10-08
581
                 * expiry: 2012-10-09
582
                 */
583 1
                $this->offsetSet('published', false);
584 1
                if (\is_array($value)) {
585 1
                    if (\array_key_exists('publish', $value) && Util\Date::toDatetime($value['publish']) <= Util\Date::toDatetime('now')) {
586 1
                        $this->offsetSet('published', true);
587
                    }
588 1
                    if (\array_key_exists('expiry', $value) && Util\Date::toDatetime($value['expiry']) >= Util\Date::toDatetime('now')) {
589
                        $this->offsetSet('published', true);
590
                    }
591
                }
592 1
                break;
593 1
            case 'draft':
594
                // draft: true = published: false
595 1
                if ($value === true) {
596 1
                    $this->offsetSet('published', false);
597
                }
598 1
                break;
599 1
            case 'path':
600 1
            case 'slug':
601 1
                $slugify = self::slugify((string) $value);
602 1
                if ($value != $slugify) {
603
                    throw new RuntimeException(\sprintf('"%s" variable should be "%s" (not "%s") in "%s".', $name, $slugify, (string) $value, $this->getId()));
604
                }
605 1
                $method = 'set' . ucfirst($name);
606 1
                $this->$method($value);
607 1
                break;
608
            default:
609 1
                $this->offsetSet($name, $value);
610
        }
611
612 1
        return $this;
613
    }
614
615
    /**
616
     * Is variable exists?
617
     *
618
     * @param string $name Name of the variable
619
     */
620 1
    public function hasVariable(string $name): bool
621
    {
622 1
        return $this->offsetExists($name);
623
    }
624
625
    /**
626
     * Get a variable.
627
     *
628
     * @param string     $name    Name of the variable
629
     * @param mixed|null $default Default value
630
     *
631
     * @return mixed|null
632
     */
633 1
    public function getVariable(string $name, $default = null)
634
    {
635 1
        if ($this->offsetExists($name)) {
636 1
            return $this->offsetGet($name);
637
        }
638
639 1
        return $default;
640
    }
641
642
    /**
643
     * Unset a variable.
644
     *
645
     * @param string $name Name of the variable
646
     */
647 1
    public function unVariable(string $name): self
648
    {
649 1
        if ($this->offsetExists($name)) {
650 1
            $this->offsetUnset($name);
651
        }
652
653 1
        return $this;
654
    }
655
656
    /**
657
     * Set front matter (only) variables.
658
     */
659 1
    public function setFmVariables(array $variables): self
660
    {
661 1
        $this->fmVariables = $variables;
662
663 1
        return $this;
664
    }
665
666
    /**
667
     * Get front matter variables.
668
     */
669 1
    public function getFmVariables(): array
670
    {
671 1
        return $this->fmVariables;
672
    }
673
674
    /**
675
     * Creates a page ID from a file (based on path).
676
     */
677 1
    private static function createIdFromFile(SplFileInfo $file): string
678
    {
679 1
        $relativePath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
680 1
        $filename = self::slugify(PrefixSuffix::subPrefix($file->getFilenameWithoutExtension()));
681
        // if file is "README.md", ID is "index"
682 1
        $filename = strtolower($filename) == 'readme' ? 'index' : $filename;
683
        // if file is section's index (ie: if file is "section/index.md", ID is "section")
684 1
        if (!empty($relativePath) && PrefixSuffix::sub($filename) == 'index') {
685
            // localized section (ie: if file is "section/index.fr.md", ID is "fr/section")
686 1
            if (PrefixSuffix::hasSuffix($filename)) {
687 1
                return PrefixSuffix::getSuffix($filename) . '/' . $relativePath;
688
            }
689
690 1
            return $relativePath;
691
        }
692
        // localized page (ie: if file is "page.fr.md", ID is "fr/page")
693 1
        if (PrefixSuffix::hasSuffix($filename)) {
694 1
            return trim(Util::joinPath(PrefixSuffix::getSuffix($filename), $relativePath, PrefixSuffix::sub($filename)), '/');
0 ignored issues
show
Bug introduced by
It seems like Cecil\Collection\Page\Pr...x::getSuffix($filename) can also be of type null; 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

694
            return trim(Util::joinPath(/** @scrutinizer ignore-type */ PrefixSuffix::getSuffix($filename), $relativePath, PrefixSuffix::sub($filename)), '/');
Loading history...
695
        }
696
697 1
        return trim(Util::joinPath($relativePath, $filename), '/');
698
    }
699
700
    /**
701
     * Cast "boolean" string (or array of strings) to boolean.
702
     *
703
     * @param mixed $value Value to filter
704
     *
705
     * @return bool|mixed
706
     *
707
     * @see strToBool()
708
     */
709 1
    private function filterBool(&$value)
710
    {
711 1
        \Cecil\Util\Str::strToBool($value);
712 1
        if (\is_array($value)) {
713 1
            array_walk_recursive($value, '\Cecil\Util\Str::strToBool');
714
        }
715
    }
716
}
717