Issues (9)

Branch: analysis-k22jOP

src/Collection/Page/Page.php (1 issue)

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

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
716
    }
717
718
    /**
719
     * Set front matter (only) variables.
720
     */
721
    public function setFmVariables(array $variables): self
722
    {
723
        $this->fmVariables = $variables;
724
725
        return $this;
726
    }
727
728
    /**
729
     * Get front matter variables.
730
     */
731
    public function getFmVariables(): array
732
    {
733
        return $this->fmVariables;
734
    }
735
736
    /**
737
     * Creates a page ID from a file (based on path).
738
     */
739
    private static function createIdFromFile(SplFileInfo $file): string
740
    {
741
        $relativePath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
742
        $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.' . $file->getExtension())));
743
        // if file is "README.md", ID is "index"
744
        $basename = strtolower($basename) == 'readme' ? 'index' : $basename;
745
        // if file is section's index: "section/index.md", ID is "section"
746
        if (!empty($relativePath) && PrefixSuffix::sub($basename) == 'index') {
747
            // case of a localized section's index: "section/index.fr.md", ID is "fr/section"
748
            if (PrefixSuffix::hasSuffix($basename)) {
749
                return PrefixSuffix::getSuffix($basename) . '/' . $relativePath;
750
            }
751
752
            return $relativePath;
753
        }
754
        // localized page
755
        if (PrefixSuffix::hasSuffix($basename)) {
756
            return trim(Util::joinPath(/** @scrutinizer ignore-type */ PrefixSuffix::getSuffix($basename), $relativePath, PrefixSuffix::sub($basename)), '/');
757
        }
758
759
        return trim(Util::joinPath($relativePath, $basename), '/');
760
    }
761
762
    /**
763
     * Cast "boolean" string (or array of strings) to boolean.
764
     *
765
     * @param mixed $value Value to filter
766
     *
767
     * @return bool|mixed
768
     *
769
     * @see strToBool()
770
     */
771
    private function filterBool(&$value)
772
    {
773
        \Cecil\Util\Str::strToBool($value);
774
        if (\is_array($value)) {
775
            array_walk_recursive($value, '\Cecil\Util\Str::strToBool');
776
        }
777
    }
778
}
779