Passed
Push — feat/cast-bool ( 0e044f...27f82c )
by Arnaud
03:56
created

Page::setVariables()   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
 * This file is part of the Cecil/Cecil package.
4
 *
5
 * Copyright (c) Arnaud Ligny <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace Cecil\Collection\Page;
14
15
use Cecil\Collection\Item;
16
use Cecil\Exception\RuntimeException;
17
use Cecil\Util;
18
use Cocur\Slugify\Slugify;
19
use Symfony\Component\Finder\SplFileInfo;
20
21
/**
22
 * Class Page.
23
 */
24
class Page extends Item
25
{
26
    const SLUGIFY_PATTERN = '/(^\/|[^._a-z0-9\/]|-)+/'; // should be '/^\/|[^_a-z0-9\/]+/'
27
28
    /** @var bool True if page is not created from a Markdown file. */
29
    protected $virtual;
30
31
    /** @var SplFileInfo */
32
    protected $file;
33
34
    /** @var string Homepage, Page, Section, etc. */
35
    protected $type;
36
37
    /** @var string */
38
    protected $folder;
39
40
    /** @var string */
41
    protected $slug;
42
43
    /** @var string folder + slug. */
44
    protected $path;
45
46
    /** @var string */
47
    protected $section;
48
49
    /** @var string */
50
    protected $frontmatter;
51
52
    /** @var string Body before conversion. */
53
    protected $body;
54
55
    /** @var array Front matter before conversion. */
56
    protected $fmVariables = [];
57
58
    /** @var string Body after Markdown conversion. */
59
    protected $html;
60
61
    /** @var Slugify */
62
    private static $slugifier;
63
64 1
    public function __construct(string $id)
65
    {
66 1
        parent::__construct($id);
67 1
        $this->setVirtual(true);
68 1
        $this->setType(Type::PAGE);
69
        // default variables
70 1
        $this->setVariables([
71 1
            'title'            => 'Page Title',
72 1
            'date'             => new \DateTime(),
73 1
            'updated'          => new \DateTime(),
74
            'weight'           => null,
75
            'filepath'         => null,
76
            'published'        => true,
77 1
            'content_template' => 'page.content.twig',
78
        ]);
79 1
    }
80
81
    /**
82
     * Turns a path (string) into a slug (URI).
83
     */
84 1
    public static function slugify(string $path): string
85
    {
86 1
        if (!self::$slugifier instanceof Slugify) {
87 1
            self::$slugifier = Slugify::create(['regexp' => self::SLUGIFY_PATTERN]);
88
        }
89
90 1
        return self::$slugifier->slugify($path);
91
    }
92
93
    /**
94
     * Creates the ID from the file path.
95
     */
96 1
    public static function createId(SplFileInfo $file): string
97
    {
98 1
        $relativepath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
99 1
        $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.'.$file->getExtension())));
100
        // case of "README" -> index
101 1
        $basename = (string) str_ireplace('readme', 'index', $basename);
102
        // case of section's index: "section/index" -> "section"
103 1
        if (!empty($relativepath) && PrefixSuffix::sub($basename) == 'index') {
104
            // case of a localized section
105 1
            if (PrefixSuffix::hasSuffix($basename)) {
106
                return $relativepath.'.'.PrefixSuffix::getSuffix($basename);
107
            }
108
109 1
            return $relativepath;
110
        }
111
112 1
        return trim(Util::joinPath($relativepath, $basename), '/');
113
    }
114
115
    /**
116
     * Returns the ID of a page without language suffix.
117
     */
118 1
    public function getIdWithoutLang(): string
119
    {
120 1
        return PrefixSuffix::sub($this->getId());
121
    }
122
123
    /**
124
     * Set file.
125
     */
126 1
    public function setFile(SplFileInfo $file): self
127
    {
128 1
        $this->setVirtual(false);
129 1
        $this->file = $file;
130
131
        /*
132
         * File path components
133
         */
134 1
        $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
135 1
        $fileExtension = $this->file->getExtension();
136 1
        $fileName = $this->file->getBasename('.'.$fileExtension);
137
        // case of "README" -> "index"
138 1
        $fileName = (string) str_ireplace('readme', 'index', $fileName);
139
        // case of "index" = home page
140 1
        if (empty($this->file->getRelativePath()) && PrefixSuffix::sub($fileName) == 'index') {
141 1
            $this->setType(Type::HOMEPAGE);
142
        }
143
        /*
144
         * Set protected variables
145
         */
146 1
        $this->setFolder($fileRelativePath); // ie: "blog"
147 1
        $this->setSlug($fileName); // ie: "post-1"
148 1
        $this->setPath($this->getFolder().'/'.$this->getSlug()); // ie: "blog/post-1"
149
        /*
150
         * Set default variables
151
         */
152 1
        $this->setVariables([
153 1
            'title'    => PrefixSuffix::sub($fileName),
154 1
            'date'     => (new \DateTime())->setTimestamp($this->file->getCTime()),
155 1
            'updated'  => (new \DateTime())->setTimestamp($this->file->getMTime()),
156 1
            'filepath' => $this->file->getRelativePathname(),
157
        ]);
158
        /*
159
         * Set specific variables
160
         */
161
        // is file has a prefix?
162 1
        if (PrefixSuffix::hasPrefix($fileName)) {
163 1
            $prefix = PrefixSuffix::getPrefix($fileName);
164 1
            if ($prefix !== null) {
165
                // prefix is a valid date?
166 1
                if (Util\Date::isDateValid($prefix)) {
167 1
                    $this->setVariable('date', (string) $prefix);
168
                } else {
169
                    // prefix is an integer: used for sorting
170 1
                    $this->setVariable('weight', (int) $prefix);
171
                }
172
            }
173
        }
174
        // is file has a language suffix?
175 1
        if (PrefixSuffix::hasSuffix($fileName)) {
176 1
            $this->setVariable('language', PrefixSuffix::getSuffix($fileName));
177
        }
178
        // set reference between translations
179 1
        $this->setVariable('langref', $this->getPath());
180
181 1
        return $this;
182
    }
183
184
    /**
185
     * Returns file real path.
186
     */
187 1
    public function getFilePath(): ?string
188
    {
189 1
        return $this->file->getRealPath() === false ? null : $this->file->getRealPath();
190
    }
191
192
    /**
193
     * Parse file content.
194
     */
195 1
    public function parse(): self
196
    {
197 1
        $parser = new Parser($this->file);
198 1
        $parsed = $parser->parse();
199 1
        $this->frontmatter = $parsed->getFrontmatter();
200 1
        $this->body = $parsed->getBody();
201
202 1
        return $this;
203
    }
204
205
    /**
206
     * Get frontmatter.
207
     */
208 1
    public function getFrontmatter(): ?string
209
    {
210 1
        return $this->frontmatter;
211
    }
212
213
    /**
214
     * Get body as raw.
215
     */
216 1
    public function getBody(): ?string
217
    {
218 1
        return $this->body;
219
    }
220
221
    /**
222
     * Set virtual status.
223
     */
224 1
    public function setVirtual(bool $virtual): self
225
    {
226 1
        $this->virtual = $virtual;
227
228 1
        return $this;
229
    }
230
231
    /**
232
     * Is current page is virtual?
233
     */
234 1
    public function isVirtual(): bool
235
    {
236 1
        return $this->virtual;
237
    }
238
239
    /**
240
     * Set page type.
241
     */
242 1
    public function setType(string $type): self
243
    {
244 1
        $this->type = new Type($type);
245
246 1
        return $this;
247
    }
248
249
    /**
250
     * Get page type.
251
     */
252 1
    public function getType(): string
253
    {
254 1
        return (string) $this->type;
255
    }
256
257
    /**
258
     * Set path without slug.
259
     */
260 1
    public function setFolder(string $folder): self
261
    {
262 1
        $this->folder = self::slugify($folder);
263
264 1
        return $this;
265
    }
266
267
    /**
268
     * Get path without slug.
269
     */
270 1
    public function getFolder(): ?string
271
    {
272 1
        return $this->folder;
273
    }
274
275
    /**
276
     * Set slug.
277
     */
278 1
    public function setSlug(string $slug): self
279
    {
280 1
        if (!$this->slug) {
281 1
            $slug = self::slugify(PrefixSuffix::sub($slug));
282
        }
283
        // force slug and update path
284 1
        if ($this->slug && $this->slug != $slug) {
285 1
            $this->setPath($this->getFolder().'/'.$slug);
286
        }
287 1
        $this->slug = $slug;
288
289 1
        return $this;
290
    }
291
292
    /**
293
     * Get slug.
294
     */
295 1
    public function getSlug(): string
296
    {
297 1
        return $this->slug;
298
    }
299
300
    /**
301
     * Set path.
302
     */
303 1
    public function setPath(string $path): self
304
    {
305 1
        $path = self::slugify(PrefixSuffix::sub($path));
306
307
        // case of homepage
308 1
        if ($path == 'index') {
309 1
            $this->path = '';
310
311 1
            return $this;
312
        }
313
314
        // case of custom sections' index (ie: content/section/index.md)
315 1
        if (substr($path, -6) == '/index') {
316 1
            $path = substr($path, 0, strlen($path) - 6);
317
        }
318 1
        $this->path = $path;
319
320
        // case of root pages
321 1
        $lastslash = strrpos($this->path, '/');
322 1
        if ($lastslash === false) {
323 1
            $this->slug = $this->path;
324
325 1
            return $this;
326
        }
327
328 1
        if (!$this->virtual && $this->getSection() === null) {
329 1
            $this->section = explode('/', $this->path)[0];
330
        }
331 1
        $this->folder = substr($this->path, 0, $lastslash);
332 1
        $this->slug = substr($this->path, -(strlen($this->path) - $lastslash - 1));
333
334 1
        return $this;
335
    }
336
337
    /**
338
     * Get path.
339
     */
340 1
    public function getPath(): ?string
341
    {
342 1
        return $this->path;
343
    }
344
345
    /**
346
     * @see getPath()
347
     */
348
    public function getPathname(): ?string
349
    {
350
        return $this->getPath();
351
    }
352
353
    /**
354
     * Set section.
355
     */
356 1
    public function setSection(string $section): self
357
    {
358 1
        $this->section = $section;
359
360 1
        return $this;
361
    }
362
363
    /**
364
     * Get section.
365
     */
366 1
    public function getSection(): ?string
367
    {
368 1
        return !empty($this->section) ? $this->section : null;
369
    }
370
371
    /**
372
     * Set body as HTML.
373
     */
374 1
    public function setBodyHtml(string $html): self
375
    {
376 1
        $this->html = $html;
377
378 1
        return $this;
379
    }
380
381
    /**
382
     * Get body as HTML.
383
     */
384 1
    public function getBodyHtml(): ?string
385
    {
386 1
        return $this->html;
387
    }
388
389
    /**
390
     * @see getBodyHtml()
391
     */
392 1
    public function getContent(): ?string
393
    {
394 1
        return $this->getBodyHtml();
395
    }
396
397
    /*
398
     * Helpers to set and get variables.
399
     */
400
401
    /**
402
     * Set an array as variables.
403
     *
404
     * @throws RuntimeException
405
     */
406 1
    public function setVariables(array $variables): self
407
    {
408 1
        foreach ($variables as $key => $value) {
409 1
            $this->setVariable($key, $value);
410
        }
411
412 1
        return $this;
413
    }
414
415
    /**
416
     * Get all variables.
417
     */
418 1
    public function getVariables(): array
419
    {
420 1
        return $this->properties;
421
    }
422
423
    /**
424
     * Set a variable.
425
     *
426
     * @param string $name
427
     * @param mixed  $value
428
     *
429
     * @throws RuntimeException
430
     */
431 1
    public function setVariable(string $name, $value): self
432
    {
433
        // cast some strings to boolean
434 1
        $this->filterBool($value);
435
        if (is_array($value)) {
436 1
            array_walk_recursive($value, [$this, 'filterBool']);
437
        }
438
        // behavior for specific named variables
439
        switch ($name) {
440 1
            /**
441 1
             * date: 2012-10-08.
442 1
             */
443 1
            case 'date':
444 1
                try {
445
                    $date = Util\Date::dateToDatetime($value);
446 1
                } catch (\Exception $e) {
447 1
                    throw new RuntimeException(\sprintf('Expected date format for "date" in "%s" must be "YYYY-MM-DD" instead of "%s"', $this->getId(), (string) $value));
448 1
                }
449 1
                $this->offsetSet('date', $date);
450 1
                break;
451
            /**
452
             * schedule:
453
             *   publish: 2012-10-08
454
             *   expiry: 2012-10-09.
455 1
             */
456 1
            case 'schedule':
457 1
                $this->offsetSet('published', false);
458
                if (is_array($value)) {
459
                    if (array_key_exists('publish', $value) && Util\Date::dateToDatetime($value['publish']) <= Util\Date::dateToDatetime('now')) {
460 1
                        $this->offsetSet('published', true);
461 1
                    }
462
                    if (array_key_exists('expiry', $value) && Util\Date::dateToDatetime($value['expiry']) >= Util\Date::dateToDatetime('now')) {
463 1
                        $this->offsetSet('published', true);
464
                    }
465
                }
466 1
                break;
467
            /**
468
             * draft: true.
469
             */
470
            case 'draft':
471
                if ($value === true) {
472 1
                    $this->offsetSet('published', 0);
473
                }
474 1
                break;
475 1
            /**
476
             * path: about/about
477 1
             * slug: about.
478
             */
479 1
            case 'path':
480
            case 'slug':
481
                $slugify = self::slugify((string) $value);
482
                if ($value != $slugify) {
483
                    throw new RuntimeException(\sprintf('"%s" variable should be "%s" (not "%s") in "%s"', $name, $slugify, (string) $value, $this->getId()));
484
                }
485 1
                /** @see setPath() */
486
                /** @see setSlug() */
487 1
                $method = 'set'.\ucfirst($name);
488
                $this->$method($value);
489
                break;
490
            default:
491
                $this->offsetSet($name, $value);
492
        }
493
494
        return $this;
495 1
    }
496
497 1
    /**
498 1
     * Is variable exists?
499
     */
500 1
    public function hasVariable(string $name): bool
501
    {
502
        return $this->offsetExists($name);
503
    }
504
505 1
    /**
506
     * Get a variable.
507 1
     *
508 1
     * @return mixed|null
509
     */
510
    public function getVariable(string $name)
511 1
    {
512
        if ($this->offsetExists($name)) {
513
            return $this->offsetGet($name);
514
        }
515
    }
516
517 1
    /**
518
     * Unset a variable.
519 1
     */
520
    public function unVariable(string $name): self
521 1
    {
522
        if ($this->offsetExists($name)) {
523
            $this->offsetUnset($name);
524
        }
525
526
        return $this;
527 1
    }
528
529 1
    /**
530
     * Set front matter (only) variables.
531
     */
532
    public function setFmVariables(array $variables): self
533
    {
534
        $this->fmVariables = $variables;
535
536
        return $this;
537
    }
538
539
    /**
540
     * Get front matter variables.
541
     */
542
    public function getFmVariables(): array
543
    {
544
        return $this->fmVariables;
545
    }
546
547
    /**
548
     * Filter 'true', 'false', 'on', 'off', 'yes', 'no' to boolean.
549
     */
550
    private function filterBool(&$value)
551
    {
552
        if (is_string($value)) {
553
            if (in_array($value, ['true', 'on', 'yes'])) {
554
                $value = true;
555
            }
556
            if (in_array($value, ['false', 'off', 'no'])) {
557
                $value = false;
558
            }
559
        }
560
    }
561
}
562