Passed
Pull Request — master (#1704)
by Arnaud
12:40 queued 05:14
created

Page::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

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