Completed
Pull Request — feature-output-formats (#275)
by Arnaud
69:12 queued 66:47
created

Page::setPath()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 9.1928
c 0
b 0
f 0
cc 5
nc 12
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 bool
28
     */
29
    protected $virtual;
30
    /**
31
     * @var SplFileInfo
32
     */
33
    protected $file;
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
        parent::__construct($id);
75
        $this->setVirtual(true);
76
        $this->setType(Type::PAGE);
77
        // default variables
78
        $this->setVariables([
79
            'title'            => 'Page Title',
80
            'date'             => time(),
81
            'updated'          => time(),
82
            'weight'           => null,
83
            'filepath'         => null,
84
            'published'        => true,
85
            'content_template' => 'page.content.twig',
86
        ]);
87
    }
88
89
    /**
90
     * Turn a path (string) into a slug (URI).
91
     *
92
     * @param string $path
93
     *
94
     * @return string
95
     */
96
    public static function slugify(string $path): string
97
    {
98
        return Slugify::create([
99
            'regexp' => self::SLUGIFY_PATTERN,
100
        ])->slugify($path);
101
    }
102
103
    /**
104
     * Create ID from file.
105
     *
106
     * @param SplFileInfo $file
107
     *
108
     * @return string
109
     */
110
    public static function createId(SplFileInfo $file): string
111
    {
112
        $id = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()).'/'.Prefix::subPrefix($file->getBasename('.'.$file->getExtension())));
113
114
        return $id;
115
    }
116
117
    /**
118
     * Set file.
119
     *
120
     * @param SplFileInfo $file
121
     *
122
     * @return self
123
     */
124
    public function setFile(SplFileInfo $file): self
125
    {
126
        $this->setVirtual(false);
127
        $this->file = $file;
128
129
        /*
130
         * File path components
131
         */
132
        $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
133
        $fileExtension = $this->file->getExtension();
134
        $fileName = $this->file->getBasename('.'.$fileExtension);
135
        /*
136
         * Set protected variables
137
         */
138
        $this->setFolder($fileRelativePath); // ie: "blog"
139
        $this->setSlug($fileName); // ie: "post-1"
140
        $this->setPath($this->getFolder().'/'.$this->getSlug()); // ie: "blog/post-1"
141
        //$this->setSection(explode('/', $this->folder)[0]); // ie: "blog"
142
        /*
143
         * Set default variables
144
         */
145
        $this->setVariables([
146
            'title'    => Prefix::subPrefix($fileName),
147
            'date'     => $this->file->getCTime(),
148
            'updated'  => $this->file->getMTime(),
149
            'filepath' => $this->file->getRelativePathname(),
150
        ]);
151
        // special case: file has a prefix
152
        if (Prefix::hasPrefix($fileName)) {
153
            $prefix = Prefix::getPrefix($fileName);
154
            // prefix is a valid date?
155
            if (Util::isValidDate($prefix)) {
156
                $this->setVariable('date', (string) $prefix);
157
            } else {
158
                // prefix is an integer, use for sorting
159
                $this->setVariable('weight', (int) $prefix);
160
            }
161
        }
162
163
        return $this;
164
    }
165
166
    /**
167
     * Parse file content.
168
     *
169
     * @return self
170
     */
171
    public function parse(): self
172
    {
173
        $parser = new Parser($this->file);
174
        $parsed = $parser->parse();
175
        $this->frontmatter = $parsed->getFrontmatter();
176
        $this->body = $parsed->getBody();
177
178
        return $this;
179
    }
180
181
    /**
182
     * Get frontmatter.
183
     *
184
     * @return string|null
185
     */
186
    public function getFrontmatter(): ?string
187
    {
188
        return $this->frontmatter;
189
    }
190
191
    /**
192
     * Get body as raw.
193
     *
194
     * @return string
195
     */
196
    public function getBody(): ?string
197
    {
198
        return $this->body;
199
    }
200
201
    /**
202
     * Set virtual status.
203
     *
204
     * @param bool $virtual
205
     *
206
     * @return self
207
     */
208
    public function setVirtual(bool $virtual): self
209
    {
210
        $this->virtual = $virtual;
211
212
        return $this;
213
    }
214
215
    /**
216
     * Is current page is virtual?
217
     *
218
     * @return bool
219
     */
220
    public function isVirtual(): bool
221
    {
222
        return $this->virtual;
223
    }
224
225
    /**
226
     * Set page type.
227
     *
228
     * @param string $type
229
     *
230
     * @return self
231
     */
232
    public function setType(string $type): self
233
    {
234
        $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...
235
236
        return $this;
237
    }
238
239
    /**
240
     * Get page type.
241
     *
242
     * @return string
243
     */
244
    public function getType(): string
245
    {
246
        return (string) $this->type;
247
    }
248
249
    /**
250
     * Set path without slug.
251
     *
252
     * @param string $folder
253
     *
254
     * @return self
255
     */
256
    public function setFolder(string $folder): self
257
    {
258
        $this->folder = $this->slugify($folder);
259
260
        return $this;
261
    }
262
263
    /**
264
     * Get path without slug.
265
     *
266
     * @return string|null
267
     */
268
    public function getFolder(): ?string
269
    {
270
        return $this->folder;
271
    }
272
273
    /**
274
     * Set slug.
275
     *
276
     * @param string $slug
277
     *
278
     * @return self
279
     */
280
    public function setSlug(string $slug): self
281
    {
282
        $this->slug = $this->slugify(Prefix::subPrefix($slug));
283
284
        return $this;
285
    }
286
287
    /**
288
     * Get slug.
289
     *
290
     * @return string
291
     */
292
    public function getSlug(): string
293
    {
294
        // custom slug, from front matter
295
        if ($this->getVariable('slug')) {
296
            $this->setSlug($this->getVariable('slug'));
297
        }
298
299
        return $this->slug;
300
    }
301
302
    /**
303
     * Set path.
304
     *
305
     * @param string $path
306
     *
307
     * @return self
308
     */
309
    public function setPath(string $path): self
310
    {
311
        $this->path = $this->slugify(Prefix::subPrefix($path));
312
313
        if ($this->path == 'index') {
314
            $this->path = '';
315
        }
316
        if (substr($this->path, -6) == '/index') {
317
            $this->path = substr($this->path, 0, strlen($this->path) - 6);
318
        }
319
320
        $lastslash = strrpos($this->path, '/');
321
        if ($lastslash === false) {
322
            $this->section = null;
323
            $this->folder = null;
324
            $this->slug = $this->path;
325
        } else {
326
            if (!$this->virtual) {
327
                $this->section = explode('/', $this->path)[0];
328
            }
329
            $this->folder = substr($this->path, 0, $lastslash);
330
            $this->slug = substr($this->path, -(strlen($this->path) - $lastslash - 1));
331
        }
332
333
        return $this;
334
    }
335
336
    /**
337
     * Get path.
338
     *
339
     * @return string|null
340
     */
341
    public function getPath(): ?string
342
    {
343
        return $this->path;
344
    }
345
346
    /**
347
     * @see getPath()
348
     *
349
     * @return string|null
350
     */
351
    public function getPathname(): ?string
352
    {
353
        return $this->getPath();
354
    }
355
356
    /**
357
     * Set section.
358
     *
359
     * @param string $section
360
     *
361
     * @return self
362
     */
363
    public function setSection(string $section): self
364
    {
365
        $this->section = $section;
366
367
        return $this;
368
    }
369
370
    /**
371
     * Get section.
372
     *
373
     * @return string|null
374
     */
375
    public function getSection(): ?string
376
    {
377
        //if (empty($this->section) && !empty($this->folder)) {
378
        //    $this->setSection(explode('/', $this->folder)[0]);
379
        //}
380
381
        return $this->section;
382
    }
383
384
    /**
385
     * Set body as HTML.
386
     *
387
     * @param string $html
388
     *
389
     * @return self
390
     */
391
    public function setBodyHtml(string $html): self
392
    {
393
        $this->html = $html;
394
395
        return $this;
396
    }
397
398
    /**
399
     * Get body as HTML.
400
     *
401
     * @return string|null
402
     */
403
    public function getBodyHtml(): ?string
404
    {
405
        return $this->html;
406
    }
407
408
    /**
409
     * @see getBodyHtml()
410
     *
411
     * @return string|null
412
     */
413
    public function getContent(): ?string
414
    {
415
        return $this->getBodyHtml();
416
    }
417
418
    /**
419
     * Return output file.
420
     *
421
     * Use cases:
422
     *   - default: path + suffix + extension (ie: blog/post-1/index.html)
423
     *   - subpath: path + subpath + suffix + extension (ie: blog/post-1/amp/index.html)
424
     *   - ugly: path + extension (ie: 404.html, sitemap.xml, robots.txt)
425
     *   - path only (ie: _redirects)
426
     *
427
     * @param string $format
428
     * @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...
429
     *
430
     * @return string
431
     */
432
    public function getOutputFile(string $format, Config $config = null): string
433
    {
434
        $path = $this->getPath();
435
        $subpath = '';
436
        $suffix = '/index';
437
        $extension = 'html';
438
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
439
440
        // site config
441
        if ($config) {
442
            $subpath = $config->get(sprintf('site.output.formats.%s.subpath', $format));
443
            $suffix = $config->get(sprintf('site.output.formats.%s.suffix', $format));
444
            $extension = $config->get(sprintf('site.output.formats.%s.extension', $format));
445
        }
446
        // if ugly URL: not suffix
447
        if ($uglyurl) {
448
            $suffix = '';
449
        }
450
        // format strings
451
        if ($subpath) {
452
            $subpath = sprintf('/%s', ltrim($subpath, '/'));
453
        }
454
        if ($suffix) {
455
            $suffix = sprintf('/%s', ltrim($suffix, '/'));
456
        }
457
        if ($extension) {
458
            $extension = sprintf('.%s', $extension);
459
        }
460
        // special case: homepage
461
        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...
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
462
            //$path = 'index';
463
        }
464
465
        return $path.$subpath.$suffix.$extension;
466
    }
467
468
    /**
469
     * Return URL.
470
     *
471
     * @param string $format
472
     * @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...
473
     *
474
     * @return string
475
     */
476
    public function getUrl(string $format = 'html', Config $config = null): string
477
    {
478
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
479
        $output = $this->getOutputFile($format, $config);
480
481
        if (!$uglyurl) {
482
            $output = str_replace('index.html', '', $output);
483
        }
484
485
        return $output;
486
    }
487
488
    /*
489
     * Helper to set and get variables.
490
     */
491
492
    /**
493
     * Set an array as variables.
494
     *
495
     * @param array $variables
496
     *
497
     * @throws \Exception
498
     *
499
     * @return $this
500
     */
501
    public function setVariables($variables)
502
    {
503
        if (!is_array($variables)) {
504
            throw new \Exception('Can\'t set variables: parameter is not an array');
505
        }
506
        foreach ($variables as $key => $value) {
507
            $this->setVariable($key, $value);
508
        }
509
510
        return $this;
511
    }
512
513
    /**
514
     * Get all variables.
515
     *
516
     * @return array
517
     */
518
    public function getVariables(): array
519
    {
520
        return $this->properties;
521
    }
522
523
    /**
524
     * Set a variable.
525
     *
526
     * @param $name
527
     * @param $value
528
     *
529
     * @throws \Exception
530
     *
531
     * @return $this
532
     */
533
    public function setVariable($name, $value)
534
    {
535
        if (is_bool($value)) {
536
            $value = $value ?: 0;
537
        }
538
        switch ($name) {
539
            case 'date':
540
                try {
541
                    if ($value instanceof \DateTime) {
542
                        $date = $value;
543
                    } else {
544
                        // timestamp
545
                        if (is_numeric($value)) {
546
                            $date = (new \DateTime())->setTimestamp($value);
547
                        } else {
548
                            // ie: 2019-01-01
549
                            if (is_string($value)) {
550
                                $date = new \DateTime($value);
551
                            }
552
                        }
553
                    }
554
                } catch (\Exception $e) {
555
                    throw new \Exception(sprintf(
556
                        'Expected date string for "date" in "%s": "%s"',
557
                        $this->getId(),
558
                        $value
559
                    ));
560
                }
561
                $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...
562
                break;
563
            case 'draft':
564
                if ($value === true) {
565
                    $this->offsetSet('published', false);
566
                }
567
                break;
568
            case 'path':
569
            case 'slug':
570
                $slugify = self::slugify($value);
571
                if ($value != $slugify) {
572
                    throw new \Exception(sprintf(
573
                        '"%s" variable should be "%s" (not "%s") in "%s"',
574
                        $name,
575
                        $slugify,
576
                        $value,
577
                        $this->getId()
578
                    ));
579
                }
580
                $methodName = 'set'.\ucfirst($name);
581
                $this->$methodName($value);
582
                break;
583
            default:
584
                $this->offsetSet($name, $value);
585
        }
586
587
        return $this;
588
    }
589
590
    /**
591
     * Is variable exist?
592
     *
593
     * @param string $name
594
     *
595
     * @return bool
596
     */
597
    public function hasVariable(string $name): bool
598
    {
599
        return $this->offsetExists($name);
600
    }
601
602
    /**
603
     * Get a variable.
604
     *
605
     * @param string $name
606
     *
607
     * @return mixed|null
608
     */
609
    public function getVariable(string $name)
610
    {
611
        if ($this->offsetExists($name)) {
612
            return $this->offsetGet($name);
613
        }
614
    }
615
616
    /**
617
     * Unset a variable.
618
     *
619
     * @param string $name
620
     *
621
     * @return $this
622
     */
623
    public function unVariable(string $name): self
624
    {
625
        if ($this->offsetExists($name)) {
626
            $this->offsetUnset($name);
627
        }
628
629
        return $this;
630
    }
631
}
632