Passed
Pull Request — master (#1704)
by Arnaud
08:34 queued 03:24
created

Page::setParent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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