Passed
Pull Request — master (#1704)
by Arnaud
11:53 queued 04:45
created

Page::getAncestors()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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