Completed
Push — feature-output-formats ( 4981c5...233774 )
by Arnaud
02:38
created

Page::setFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
/*
3
 * Copyright (c) Arnaud Ligny <[email protected]>
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
declare(strict_types=1);
10
11
namespace Cecil\Collection\Page;
12
13
use Cecil\Collection\Item;
14
use Cecil\Config;
15
use Cecil\Util;
16
use Cocur\Slugify\Slugify;
17
use Symfony\Component\Finder\SplFileInfo;
18
19
/**
20
 * Class Page.
21
 */
22
class Page extends Item
23
{
24
    const SLUGIFY_PATTERN = '/(^\/|[^_a-z0-9\/]|-)+/';
25
26
    /**
27
     * @var SplFileInfo
28
     */
29
    protected $file;
30
    /**
31
     * @var bool
32
     */
33
    protected $virtual;
34
    /**
35
     * @var string
36
     */
37
    protected $type;
38
    /**
39
     * @var string
40
     */
41
    protected $folder;
42
    /**
43
     * @var string
44
     */
45
    protected $slug;
46
    /**
47
     * @var string
48
     */
49
    protected $path;
50
    /**
51
     * @var string
52
     */
53
    protected $section;
54
    /**
55
     * @var string
56
     */
57
    protected $frontmatter;
58
    /**
59
     * @var string
60
     */
61
    protected $body;
62
    /**
63
     * @var string
64
     */
65
    protected $html;
66
67
    /**
68
     * Constructor.
69
     *
70
     * @param string $id
71
     */
72
    public function __construct(string $id)
73
    {
74
        // set file ?
75
76
        // physical page
77
        if ($id instanceof SplFileInfo) {
78
            $this->file = $id;
79
80
            var_dump($this->file);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($this->file); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
81
            /*
82
             * File path components
83
             */
84
            $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
85
            $fileExtension = $this->file->getExtension();
86
            $fileName = $this->file->getBasename('.'.$fileExtension);
87
            /*
88
             * Set protected variables
89
             */
90
            $this->virtual = false;
91
            $this->setFolder($fileRelativePath); // ie: "blog"
92
            $this->setSlug($fileName); // ie: "post-1"
93
            $this->setPath($this->getFolder().'/'.$this->getSlug()); // ie: "blog/post-1"
94
            $this->setId($this->getPath());
95
            $this->setSection(explode('/', $this->folder)[0]); // ie: "blog"
96
            /*
97
             * Set default variables
98
             */
99
            $this->setVariable('title', Prefix::subPrefix($fileName));
100
            $this->setVariable('date', $this->file->getCTime());
101
            $this->setVariable('updated', $this->file->getMTime());
102
            $this->setVariable('weight', null);
103
            // special case: file has a prefix
104
            if (Prefix::hasPrefix($fileName)) {
105
                $prefix = Prefix::getPrefix($fileName);
106
                // prefix is a valid date?
107
                if (Util::isValidDate($prefix)) {
108
                    $this->setVariable('date', (string) $prefix);
109
                } else {
110
                    // prefix is an integer, use for sorting
111
                    $this->setVariable('weight', (int) $prefix);
112
                }
113
            }
114
            // physical file relative path
115
            $this->setVariable('filepath', $fileRelativePath);
116
117
            //parent::__construct($this->getPath());
118
        } else {
119
            $this->virtual = true;
120
            $this->setId($id);
121
            // default variables
122
            $this->setVariables([
123
                'title'    => 'Page Title',
124
                'date'     => time(),
125
                'updated'  => time(),
126
                'weight'   => null,
127
                'filepath' => null,
128
            ]);
129
130
            //parent::__construct($id);
131
        }
132
        // required
133
        $this->setType(Type::PAGE);
134
        $this->setVariables([
135
            'published'        => true,
136
            'virtual'          => $this->virtual,
137
            'content_template' => 'page.content.twig',
138
        ]);
139
    }
140
141
    /**
142
     * Create ID from file.
143
     *
144
     * @param SplFileInfo $file
145
     *
146
     * @return string
147
     */
148
    public function createId(SplFileInfo $file): string
149
    {
150
        return trim($this->slugify(Prefix::subPrefix(basename($file->getRelativePathname(), $file->getFileExtension()))), '/');
0 ignored issues
show
Bug introduced by
The method getFileExtension() does not seem to exist on object<Symfony\Component\Finder\SplFileInfo>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
151
    }
152
153
    /**
154
     * Set file.
155
     *
156
     * @param SplFileInfo $file
157
     *
158
     * @return self
0 ignored issues
show
Documentation introduced by
Should the return type not be \self?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
159
     */
160
    public function setFile(SplFileInfo $file): self
161
    {
162
        $this->file = $file;
163
    }
164
165
    /**
166
     * Parse file content.
167
     *
168
     * @return self
169
     */
170
    public function parse(): self
171
    {
172
        $parser = new Parser($this->file);
173
        $parsed = $parser->parse();
174
        $this->frontmatter = $parsed->getFrontmatter();
175
        $this->body = $parsed->getBody();
176
177
        return $this;
178
    }
179
180
    /**
181
     * Get frontmatter.
182
     *
183
     * @return string|null
184
     */
185
    public function getFrontmatter(): ?string
186
    {
187
        return $this->frontmatter;
188
    }
189
190
    /**
191
     * Get body as raw.
192
     *
193
     * @return string
194
     */
195
    public function getBody(): ?string
196
    {
197
        return $this->body;
198
    }
199
200
    /**
201
     * Turn a path (string) into a slug (URL).
202
     *
203
     * @param string $path
204
     *
205
     * @return string
206
     */
207
    public static function slugify(string $path): string
208
    {
209
        return Slugify::create([
210
            'regexp' => self::SLUGIFY_PATTERN,
211
        ])->slugify($path);
212
    }
213
214
    /**
215
     * Is current page is virtual?
216
     *
217
     * @return bool
218
     */
219
    public function isVirtual(): bool
220
    {
221
        return $this->virtual;
222
    }
223
224
    /**
225
     * Set page type.
226
     *
227
     * @param string $type
228
     *
229
     * @return self
230
     */
231
    public function setType(string $type): self
232
    {
233
        $this->type = new Type($type);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Cecil\Collection\Page\Type($type) of type object<Cecil\Collection\Page\Type> is incompatible with the declared type string of property $type.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
234
235
        return $this;
236
    }
237
238
    /**
239
     * Get page type.
240
     *
241
     * @return string
242
     */
243
    public function getType(): string
244
    {
245
        return (string) $this->type;
246
    }
247
248
    /**
249
     * Set path without slug.
250
     *
251
     * @param string $folder
252
     *
253
     * @return self
254
     */
255
    public function setFolder(string $folder): self
256
    {
257
        $this->folder = $this->slugify($folder);
258
259
        return $this;
260
    }
261
262
    /**
263
     * Get path without slug.
264
     *
265
     * @return string|null
266
     */
267
    public function getFolder(): ?string
268
    {
269
        return $this->folder;
270
    }
271
272
    /**
273
     * Set slug.
274
     *
275
     * @param string $slug
276
     *
277
     * @return self
278
     */
279
    public function setSlug(string $slug): self
280
    {
281
        $this->slug = $this->slugify(Prefix::subPrefix($slug));
282
283
        return $this;
284
    }
285
286
    /**
287
     * Get slug.
288
     *
289
     * @return string
290
     */
291
    public function getSlug(): string
292
    {
293
        // custom slug, from front matter
294
        if ($this->getVariable('slug')) {
295
            $this->setSlug($this->getVariable('slug'));
296
        }
297
298
        return $this->slug;
299
    }
300
301
    /**
302
     * Set path.
303
     *
304
     * @param string $path
305
     *
306
     * @return self
307
     */
308
    public function setPath(string $path): self
309
    {
310
        // DEBUG
311
        if ($this->getId()) {
312
            echo '['.$this->getId().'] '.$path.' => '.trim($this->slugify(Prefix::subPrefix($path)), '/')."\n";
313
        } else {
314
            echo '[    ] '.$path.' => '.trim($this->slugify(Prefix::subPrefix($path)), '/')."\n";
315
        }
316
317
        if ($path) {
318
            $this->path = $this->slugify(Prefix::subPrefix($path));
319
        }
320
        // default path
321
        if (!$path) {
322
            // Use cases:
323
            // - "blog/post-1" => "blog/post-1"
324
            // - "blog/index" => "blog"
325
            // - "projet/projet-a" => "projet/projet-a"
326
            // - "404" => "404"
327
            $this->path = rtrim(($this->folder ? $this->folder.'/' : '')
328
                .($this->folder && $this->slug == 'index' ? '' : $this->slug), '/');
329
        }
330
331
        return $this;
332
    }
333
334
    /**
335
     * Get path.
336
     *
337
     * @return string|null
338
     */
339
    public function getPath(): ?string
340
    {
341
        // special case: homepage
342
        if ($this->path == 'index'
343
            || (\strlen($this->path) >= 6 && \substr_compare($this->path, 'index/', 0, 6) == 0))
344
        {
345
            $this->path = '';
346
        }
347
348
        return $this->path;
349
    }
350
351
    /**
352
     * @see getPath()
353
     *
354
     * @return string|null
355
     */
356
    public function getPathname(): ?string
357
    {
358
        return $this->getPath();
359
    }
360
361
    /**
362
     * Set section.
363
     *
364
     * @param string $section
365
     *
366
     * @return self
367
     */
368
    public function setSection(string $section): self
369
    {
370
        $this->section = $section;
371
372
        return $this;
373
    }
374
375
    /**
376
     * Get section.
377
     *
378
     * @return string|null
379
     */
380
    public function getSection(): ?string
381
    {
382
        if (empty($this->section) && !empty($this->folder)) {
383
            $this->setSection(explode('/', $this->folder)[0]);
384
        }
385
386
        return $this->section;
387
    }
388
389
    /**
390
     * Set body as HTML.
391
     *
392
     * @param string $html
393
     *
394
     * @return self
395
     */
396
    public function setBodyHtml(string $html): self
397
    {
398
        $this->html = $html;
399
400
        return $this;
401
    }
402
403
    /**
404
     * Get body as HTML.
405
     *
406
     * @return string|null
407
     */
408
    public function getBodyHtml(): ?string
409
    {
410
        return $this->html;
411
    }
412
413
    /**
414
     * @see getBodyHtml()
415
     *
416
     * @return string|null
417
     */
418
    public function getContent(): ?string
419
    {
420
        return $this->getBodyHtml();
421
    }
422
423
    /**
424
     * Return output file.
425
     *
426
     * Use cases:
427
     *   - default: path + suffix + extension (ie: blog/post-1/index.html)
428
     *   - subpath: path + subpath + suffix + extension (ie: blog/post-1/amp/index.html)
429
     *   - ugly: path + extension (ie: 404.html, sitemap.xml, robots.txt)
430
     *   - path only (ie: _redirects)
431
     *
432
     * @param string $format
433
     * @param Config $config
0 ignored issues
show
Documentation introduced by
Should the type for parameter $config not be null|Config?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
434
     *
435
     * @return string
436
     */
437
    public function getOutputFile(string $format, Config $config = null): string
438
    {
439
        $path = $this->getPath();
440
        $subpath = '';
441
        $suffix = '/index';
442
        $extension = 'html';
443
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
444
445
        // site config
446
        if ($config) {
447
            $subpath = $config->get(sprintf('site.output.formats.%s.subpath', $format));
448
            $suffix = $config->get(sprintf('site.output.formats.%s.suffix', $format));
449
            $extension = $config->get(sprintf('site.output.formats.%s.extension', $format));
450
        }
451
        // if ugly URL: not suffix
452
        if ($uglyurl) {
453
            $suffix = '';
454
        }
455
        // format strings
456
        if ($subpath) {
457
            $subpath = sprintf('/%s', ltrim($subpath, '/'));
458
        }
459
        if ($suffix) {
460
            $suffix = sprintf('/%s', ltrim($suffix, '/'));
461
        }
462
        if ($extension) {
463
            $extension = sprintf('.%s', $extension);
464
        }
465
        // special case: homepage
466
        if (!$path && !$suffix) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
467
            $path = 'index';
468
        }
469
470
        return $path.$subpath.$suffix.$extension;
471
    }
472
473
    /**
474
     * Return URL.
475
     *
476
     * @param string $format
477
     * @param Config $config
0 ignored issues
show
Documentation introduced by
Should the type for parameter $config not be null|Config?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
478
     *
479
     * @return string
480
     */
481
    public function getUrl(string $format = 'html', Config $config = null): string
482
    {
483
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
484
        $output = $this->getOutputFile($format, $config);
485
486
        if (!$uglyurl) {
487
            $output = str_replace('index.html', '', $output);
488
        }
489
490
        return $output;
491
    }
492
493
    /*
494
     * Helper to set and get variables.
495
     */
496
497
    /**
498
     * Set an array as variables.
499
     *
500
     * @param array $variables
501
     *
502
     * @throws \Exception
503
     *
504
     * @return $this
505
     */
506
    public function setVariables($variables)
507
    {
508
        if (!is_array($variables)) {
509
            throw new \Exception('Can\'t set variables: parameter is not an array');
510
        }
511
        foreach ($variables as $key => $value) {
512
            $this->setVariable($key, $value);
513
        }
514
515
        return $this;
516
    }
517
518
    /**
519
     * Get all variables.
520
     *
521
     * @return array
522
     */
523
    public function getVariables(): array
524
    {
525
        return $this->properties;
526
    }
527
528
    /**
529
     * Set a variable.
530
     *
531
     * @param $name
532
     * @param $value
533
     *
534
     * @throws \Exception
535
     *
536
     * @return $this
537
     */
538
    public function setVariable($name, $value)
539
    {
540
        if (is_bool($value)) {
541
            $value = $value ?: 0;
542
        }
543
        switch ($name) {
544
            case 'date':
545
                try {
546
                    if ($value instanceof \DateTime) {
547
                        $date = $value;
548
                    } else {
549
                        // timestamp
550
                        if (is_numeric($value)) {
551
                            $date = (new \DateTime())->setTimestamp($value);
552
                        } else {
553
                            // ie: 2019-01-01
554
                            if (is_string($value)) {
555
                                $date = new \DateTime($value);
556
                            }
557
                        }
558
                    }
559
                } catch (\Exception $e) {
560
                    throw new \Exception(sprintf(
561
                        'Expected date string for "date" in "%s": "%s"',
562
                        $this->getId(),
563
                        $value
564
                    ));
565
                }
566
                $this->offsetSet('date', $date);
0 ignored issues
show
Bug introduced by
The variable $date does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
567
                break;
568
            case 'draft':
569
                if ($value === true) {
570
                    $this->offsetSet('published', false);
571
                }
572
                break;
573
            case 'path':
574
            case 'slug':
575
                $slugify = self::slugify($value);
576
                if ($value != $slugify) {
577
                    throw new \Exception(sprintf(
578
                        '"%s" variable should be "%s" (not "%s") in "%s"',
579
                        $name,
580
                        $slugify,
581
                        $value,
582
                        $this->getId()
583
                    ));
584
                }
585
                $methodName = 'set'.\ucfirst($name);
586
                $this->$methodName($value);
587
                break;
588
            default:
589
                $this->offsetSet($name, $value);
590
        }
591
592
        return $this;
593
    }
594
595
    /**
596
     * Is variable exist?
597
     *
598
     * @param string $name
599
     *
600
     * @return bool
601
     */
602
    public function hasVariable(string $name): bool
603
    {
604
        return $this->offsetExists($name);
605
    }
606
607
    /**
608
     * Get a variable.
609
     *
610
     * @param string $name
611
     *
612
     * @return mixed|null
613
     */
614
    public function getVariable(string $name)
615
    {
616
        if ($this->offsetExists($name)) {
617
            return $this->offsetGet($name);
618
        }
619
    }
620
621
    /**
622
     * Unset a variable.
623
     *
624
     * @param string $name
625
     *
626
     * @return $this
627
     */
628
    public function unVariable(string $name): self
629
    {
630
        if ($this->offsetExists($name)) {
631
            $this->offsetUnset($name);
632
        }
633
634
        return $this;
635
    }
636
}
637