Page   F
last analyzed

Complexity

Total Complexity 99

Size/Duplication

Total Lines 696
Duplicated Lines 0 %

Test Coverage

Coverage 92.64%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 205
dl 0
loc 696
ccs 214
cts 231
cp 0.9264
rs 2
c 4
b 1
f 0
wmc 99

47 Methods

Rating   Name   Duplication   Size   Complexity  
A setTerms() 0 5 1
A setFmVariables() 0 5 1
A getPagination() 0 3 1
A isVirtual() 0 3 1
A getType() 0 3 1
A getFrontmatter() 0 3 1
A setFolder() 0 5 1
A setVirtual() 0 5 1
A getVariable() 0 7 2
A parse() 0 8 1
A setType() 0 5 1
A getBody() 0 3 1
A getPath() 0 3 1
A getPages() 0 3 1
A getFmVariables() 0 3 1
A hasVariable() 0 3 1
A getSection() 0 3 2
A getFilePath() 0 7 3
A getContent() 0 3 1
A getPathname() 0 3 1
A setId() 0 3 1
A __toString() 0 3 1
A __construct() 0 26 4
B setFile() 0 54 9
A setBodyHtml() 0 5 1
A getBodyHtml() 0 3 1
A getFolder() 0 3 1
A setVariables() 0 7 2
A createIdFromFile() 0 21 6
A getIdWithoutLang() 0 8 3
A getRendered() 0 3 1
A slugify() 0 14 3
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 getSlug() 0 3 1
A setSlug() 0 12 4
A getTerms() 0 3 1
A filterBool() 0 5 2

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 pages list. */
69
    protected $pages;
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([
133 1
                'regexp' => self::SLUGIFY_PATTERN,
134 1
            ]);
135
        }
136
137 1
        // Use the Chinese ruleset only when the path contains Chinese (Han) characters.
138
        $options = [];
139
        if (preg_match('/\p{Han}/u', $path)) {
140
            $options['ruleset'] = 'chinese';
141
        }
142
        return self::$slugifier->slugify($path, $options);
143 1
    }
144
145 1
    /**
146 1
     * Returns the ID of a page without language.
147 1
     */
148
    public function getIdWithoutLang(): string
149
    {
150 1
        $langPrefix = $this->getVariable('language') . '/';
151
        if ($this->hasVariable('language') && Util\Str::startsWith($this->getId(), $langPrefix)) {
152
            return substr($this->getId(), \strlen($langPrefix));
153
        }
154
155
        return $this->getId();
156 1
    }
157
158 1
    /**
159 1
     * Set file.
160
     */
161
    public function setFile(SplFileInfo $file): self
162
    {
163
        $this->file = $file;
164 1
        $this->setVirtual(false);
165 1
166 1
        /*
167
         * File path components
168 1
         */
169
        $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
170 1
        $fileExtension = $this->file->getExtension();
171 1
        $fileName = $this->file->getBasename('.' . $fileExtension);
172
        // renames "README" to "index"
173
        $fileName = strtolower($fileName) == 'readme' ? 'index' : $fileName;
174
        // case of "index" = home page
175
        if (empty($this->file->getRelativePath()) && PrefixSuffix::sub($fileName) == 'index') {
176 1
            $this->setType(Type::HOMEPAGE->value);
177 1
        }
178 1
        /*
179 1
         * Set page properties and variables
180 1
         */
181 1
        $this->setFolder($fileRelativePath);
182 1
        $this->setSlug($fileName);
183 1
        $this->setPath($this->getFolder() . '/' . $this->getSlug());
184 1
        $this->setVariables([
185
            'title'    => PrefixSuffix::sub($fileName),
186
            'date'     => (new \DateTime())->setTimestamp($this->file->getMTime()),
187
            'updated'  => (new \DateTime())->setTimestamp($this->file->getMTime()),
188
            'filepath' => $this->file->getRelativePathname(),
189 1
        ]);
190 1
        /*
191 1
         * Set specific variables
192
         */
193 1
        // is file has a prefix?
194 1
        if (PrefixSuffix::hasPrefix($fileName)) {
195
            $prefix = PrefixSuffix::getPrefix($fileName);
196
            if ($prefix !== null) {
197 1
                // prefix is an integer: used for sorting
198 1
                if (is_numeric($prefix)) {
199
                    $this->setVariable('weight', (int) $prefix);
200
                }
201
                // prefix is a valid date?
202
                if (Util\Date::isValid($prefix)) {
203 1
                    $this->setVariable('date', (string) $prefix);
204 1
                }
205
            }
206
        }
207 1
        // is file has a language suffix?
208
        if (PrefixSuffix::hasSuffix($fileName)) {
209 1
            $this->setVariable('language', PrefixSuffix::getSuffix($fileName));
210
        }
211
        // set reference between page's translations, even if it exist in only one language
212
        $this->setVariable('langref', $this->getPath());
213
214
        return $this;
215
    }
216
217
    /**
218
     * Returns file name, with extension.
219
     */
220
    public function getFileName(): ?string
221
    {
222
        if ($this->file === null) {
223
            return null;
224
        }
225
226
        return $this->file->getBasename();
227 1
    }
228
229 1
    /**
230
     * Returns file real path.
231
     */
232
    public function getFilePath(): ?string
233 1
    {
234
        if ($this->file === null) {
235
            return null;
236
        }
237
238
        return $this->file->getRealPath() === false ? null : $this->file->getRealPath();
239 1
    }
240
241 1
    /**
242 1
     * Parse file content.
243 1
     */
244 1
    public function parse(): self
245
    {
246 1
        $parser = new Parser($this->file);
247
        $parsed = $parser->parse();
248
        $this->frontmatter = $parsed->getFrontmatter();
249
        $this->body = $parsed->getBody();
250
251
        return $this;
252 1
    }
253
254 1
    /**
255
     * Get front matter.
256
     */
257
    public function getFrontmatter(): ?string
258
    {
259
        return $this->frontmatter;
260 1
    }
261
262 1
    /**
263
     * Get body as raw.
264
     */
265
    public function getBody(): ?string
266
    {
267
        return $this->body;
268 1
    }
269
270 1
    /**
271
     * Set virtual status.
272 1
     */
273
    public function setVirtual(bool $virtual): self
274
    {
275
        $this->virtual = $virtual;
276
277
        return $this;
278 1
    }
279
280 1
    /**
281
     * Is current page is virtual?
282
     */
283
    public function isVirtual(): bool
284
    {
285
        return $this->virtual;
286 1
    }
287
288 1
    /**
289
     * Set page type.
290 1
     */
291
    public function setType(string $type): self
292
    {
293
        $this->type = Type::from($type);
294
295
        return $this;
296 1
    }
297
298 1
    /**
299
     * Get page type.
300
     */
301
    public function getType(): string
302
    {
303
        return $this->type->value;
304 1
    }
305
306 1
    /**
307
     * Set path without slug.
308 1
     */
309
    public function setFolder(string $folder): self
310
    {
311
        $this->folder = self::slugify($folder);
312
313
        return $this;
314 1
    }
315
316 1
    /**
317
     * Get path without slug.
318
     */
319
    public function getFolder(): ?string
320
    {
321
        return $this->folder;
322 1
    }
323
324 1
    /**
325 1
     * Set slug.
326
     */
327
    public function setSlug(string $slug): self
328 1
    {
329 1
        if (!$this->slug) {
330
            $slug = self::slugify(PrefixSuffix::sub($slug));
331 1
        }
332
        // force slug and update path
333 1
        if ($this->slug && $this->slug != $slug) {
334
            $this->setPath($this->getFolder() . '/' . $slug);
335
        }
336
        $this->slug = $slug;
337
338
        return $this;
339 1
    }
340
341 1
    /**
342
     * Get slug.
343
     */
344
    public function getSlug(): string
345
    {
346
        return $this->slug;
347 1
    }
348
349 1
    /**
350
     * Set path.
351
     */
352 1
    public function setPath(string $path): self
353 1
    {
354
        $path = trim($path, '/');
355 1
356
        // case of homepage
357
        if ($path == 'index') {
358
            $this->path = '';
359 1
360 1
            return $this;
361
        }
362 1
363
        // case of custom sections' index (ie: section/index.md -> section)
364 1
        if (substr($path, -6) == '/index') {
365
            $path = substr($path, 0, \strlen($path) - 6);
366
        }
367 1
        $this->path = $path;
368 1
369
        $lastslash = strrpos($this->path, '/');
370 1
371
        // case of root/top-level pages
372
        if ($lastslash === false) {
373
            $this->slug = $this->path;
374 1
375 1
            return $this;
376
        }
377
378 1
        // case of sections' pages: set section
379 1
        if (!$this->virtual && $this->getSection() === null) {
380
            $this->section = explode('/', $this->path)[0];
381 1
        }
382
        // set/update folder and slug
383
        $this->folder = substr($this->path, 0, $lastslash);
384
        $this->slug = substr($this->path, -(\strlen($this->path) - $lastslash - 1));
385
386
        return $this;
387 1
    }
388
389 1
    /**
390
     * Get path.
391
     */
392
    public function getPath(): ?string
393
    {
394
        return $this->path;
395
    }
396
397
    /**
398
     * @see getPath()
399
     */
400
    public function getPathname(): ?string
401
    {
402
        return $this->getPath();
403 1
    }
404
405 1
    /**
406
     * Set section.
407 1
     */
408
    public function setSection(string $section): self
409
    {
410
        $this->section = $section;
411
412
        return $this;
413 1
    }
414
415 1
    /**
416
     * Get section.
417
     */
418
    public function getSection(): ?string
419
    {
420
        return !empty($this->section) ? $this->section : null;
421
    }
422
423
    /**
424
     * Unset section.
425
     */
426
    public function unSection(): self
427
    {
428
        $this->section = null;
429
430
        return $this;
431 1
    }
432
433 1
    /**
434
     * Set body as HTML.
435 1
     */
436
    public function setBodyHtml(string $html): self
437
    {
438
        $this->html = $html;
439
440
        return $this;
441 1
    }
442
443 1
    /**
444
     * Get body as HTML.
445
     */
446
    public function getBodyHtml(): ?string
447
    {
448
        return $this->html;
449 1
    }
450
451 1
    /**
452
     * @see getBodyHtml()
453
     */
454
    public function getContent(): ?string
455
    {
456
        return $this->getBodyHtml();
457 1
    }
458
459 1
    /**
460
     * Add rendered.
461 1
     */
462
    public function addRendered(array $rendered): self
463
    {
464
        $this->rendered += $rendered;
465
466
        return $this;
467 1
    }
468
469 1
    /**
470
     * Get rendered.
471
     */
472
    public function getRendered(): array
473
    {
474
        return $this->rendered;
475 1
    }
476
477 1
    /**
478
     * Set pages list.
479 1
     */
480
    public function setPages(Collection $pages): self
481
    {
482
        $this->pages = $pages;
483
484
        return $this;
485 1
    }
486
487 1
    /**
488
     * Get pages list.
489
     */
490
    public function getPages(): ?Collection
491
    {
492
        return $this->pages;
493 1
    }
494
495 1
    /**
496
     * Set paginator.
497 1
     */
498
    public function setPaginator(array $paginator): self
499
    {
500
        $this->paginator = $paginator;
501
502
        return $this;
503 1
    }
504
505 1
    /**
506
     * Get paginator.
507
     */
508
    public function getPaginator(): array
509
    {
510
        return $this->paginator;
511
    }
512
513
    /**
514
     * Paginator backward compatibility.
515
     */
516
    public function getPagination(): array
517
    {
518
        return $this->getPaginator();
519 1
    }
520
521 1
    /**
522
     * Set vocabulary terms.
523 1
     */
524
    public function setTerms(\Cecil\Collection\Taxonomy\Vocabulary $terms): self
525
    {
526
        $this->terms = $terms;
527
528
        return $this;
529 1
    }
530
531 1
    /**
532
     * Get vocabulary terms.
533
     */
534
    public function getTerms(): \Cecil\Collection\Taxonomy\Vocabulary
535
    {
536
        return $this->terms;
537
    }
538
539
    /*
540
     * Helpers to set and get variables.
541
     */
542
543 1
    /**
544
     * Set an array as variables.
545 1
     *
546 1
     * @throws RuntimeException
547
     */
548
    public function setVariables(array $variables): self
549 1
    {
550
        foreach ($variables as $key => $value) {
551
            $this->setVariable($key, $value);
552
        }
553
554
        return $this;
555 1
    }
556
557 1
    /**
558
     * Get all variables.
559
     */
560
    public function getVariables(): array
561
    {
562
        return $this->properties;
563
    }
564
565
    /**
566
     * Set a variable.
567
     *
568 1
     * @param string $name  Name of the variable
569
     * @param mixed  $value Value of the variable
570 1
     *
571
     * @throws RuntimeException
572 1
     */
573 1
    public function setVariable(string $name, $value): self
574 1
    {
575
        $this->filterBool($value);
576 1
        switch ($name) {
577
            case 'date':
578
            case 'updated':
579
            case 'lastmod':
580 1
                try {
581 1
                    $date = Util\Date::toDatetime($value);
582
                } catch (\Exception) {
583 1
                    throw new \Exception(\sprintf('The value of "%s" is not a valid date: "%s".', $name, var_export($value, true)));
584
                }
585
                $this->offsetSet($name == 'lastmod' ? 'updated' : $name, $date);
586
                break;
587
588 1
            case 'schedule':
589 1
                /*
590 1
                 * publish: 2012-10-08
591 1
                 * expiry: 2012-10-09
592
                 */
593 1
                $this->offsetSet('published', false);
594
                if (\is_array($value)) {
595
                    if (\array_key_exists('publish', $value) && Util\Date::toDatetime($value['publish']) <= Util\Date::toDatetime('now')) {
596
                        $this->offsetSet('published', true);
597 1
                    }
598 1
                    if (\array_key_exists('expiry', $value) && Util\Date::toDatetime($value['expiry']) >= Util\Date::toDatetime('now')) {
599
                        $this->offsetSet('published', true);
600 1
                    }
601 1
                }
602
                break;
603 1
            case 'draft':
604 1
                // draft: true = published: false
605 1
                if ($value === true) {
606 1
                    $this->offsetSet('published', false);
607 1
                }
608
                break;
609
            case 'path':
610 1
            case 'slug':
611 1
                $slugify = self::slugify((string) $value);
612 1
                if ($value != $slugify) {
613
                    throw new RuntimeException(\sprintf('"%s" variable should be "%s" (not "%s") in "%s".', $name, $slugify, (string) $value, $this->getId()));
614 1
                }
615
                $method = 'set' . ucfirst($name);
616
                $this->$method($value);
617 1
                break;
618
            default:
619
                $this->offsetSet($name, $value);
620
        }
621
622
        return $this;
623
    }
624
625 1
    /**
626
     * Is variable exists?
627 1
     *
628
     * @param string $name Name of the variable
629
     */
630
    public function hasVariable(string $name): bool
631
    {
632
        return $this->offsetExists($name);
633
    }
634
635
    /**
636
     * Get a variable.
637
     *
638 1
     * @param string     $name    Name of the variable
639
     * @param mixed|null $default Default value
640 1
     *
641 1
     * @return mixed|null
642
     */
643
    public function getVariable(string $name, $default = null)
644 1
    {
645
        if ($this->offsetExists($name)) {
646
            return $this->offsetGet($name);
647
        }
648
649
        return $default;
650
    }
651
652 1
    /**
653
     * Unset a variable.
654 1
     *
655 1
     * @param string $name Name of the variable
656
     */
657
    public function unVariable(string $name): self
658 1
    {
659
        if ($this->offsetExists($name)) {
660
            $this->offsetUnset($name);
661
        }
662
663
        return $this;
664 1
    }
665
666 1
    /**
667
     * Set front matter (only) variables.
668 1
     */
669
    public function setFmVariables(array $variables): self
670
    {
671
        $this->fmVariables = $variables;
672
673
        return $this;
674 1
    }
675
676 1
    /**
677
     * Get front matter variables.
678
     */
679
    public function getFmVariables(): array
680
    {
681
        return $this->fmVariables;
682 1
    }
683
684 1
    /**
685 1
     * Creates a page ID from a file (based on path).
686
     */
687 1
    private static function createIdFromFile(SplFileInfo $file): string
688
    {
689 1
        $relativePath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
690
        $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.' . $file->getExtension())));
691 1
        // if file is "README.md", ID is "index"
692 1
        $basename = strtolower($basename) == 'readme' ? 'index' : $basename;
693
        // if file is section's index: "section/index.md", ID is "section"
694
        if (!empty($relativePath) && PrefixSuffix::sub($basename) == 'index') {
695 1
            // case of a localized section's index: "section/index.fr.md", ID is "fr/section"
696
            if (PrefixSuffix::hasSuffix($basename)) {
697
                return PrefixSuffix::getSuffix($basename) . '/' . $relativePath;
698 1
            }
699 1
700
            return $relativePath;
701
        }
702 1
        // localized page
703
        if (PrefixSuffix::hasSuffix($basename)) {
704
            return trim(Util::joinPath(/** @scrutinizer ignore-type */ PrefixSuffix::getSuffix($basename), $relativePath, PrefixSuffix::sub($basename)), '/');
705
        }
706
707
        return trim(Util::joinPath($relativePath, $basename), '/');
708
    }
709
710
    /**
711
     * Cast "boolean" string (or array of strings) to boolean.
712
     *
713
     * @param mixed $value Value to filter
714 1
     *
715
     * @return bool|mixed
716 1
     *
717 1
     * @see strToBool()
718 1
     */
719
    private function filterBool(&$value)
720
    {
721
        \Cecil\Util\Str::strToBool($value);
722
        if (\is_array($value)) {
723
            array_walk_recursive($value, '\Cecil\Util\Str::strToBool');
724
        }
725
    }
726
}
727