Completed
Push — feature-output-formats ( 53282f...b09369 )
by Arnaud
02:20
created

Page::getSlug()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
namespace Cecil\Collection\Page;
10
11
use Cecil\Collection\Item;
12
use Cecil\Config;
13
use Cecil\Page\Parser;
14
use Cecil\Page\Prefix;
15
use Cecil\Page\Type;
16
use Cecil\Util;
17
use Cocur\Slugify\Slugify;
18
use Symfony\Component\Finder\SplFileInfo;
19
20
/**
21
 * Class Page.
22
 */
23
class Page extends Item
24
{
25
    const SLUGIFY_PATTERN = '/(^\/|[^_a-z0-9\/]|-)+/';
26
27
    /**
28
     * @var SplFileInfo
29
     */
30
    protected $file;
31
    /**
32
     * @var bool
33
     */
34
    protected $virtual = false;
35
    /**
36
     * @var string
37
     */
38
    protected $type;
39
    /**
40
     * @var string
41
     */
42
    protected $rootpath;
43
    /**
44
     * @var string
45
     */
46
    protected $path;
47
    /**
48
     * @var string
49
     */
50
    protected $slug;
51
    /**
52
     * @var string
53
     */
54
    protected $section;
55
    /**
56
     * @var string
57
     */
58
    protected $frontmatter;
59
    /**
60
     * @var string
61
     */
62
    protected $body;
63
    /**
64
     * @var string
65
     */
66
    protected $html;
67
68
    /**
69
     * Constructor.
70
     *
71
     * @param SplFileInfo|null $file
72
     */
73
    public function __construct(SplFileInfo $file = null)
74
    {
75
        $this->file = $file;
76
77
        // physical page
78
        if ($this->file instanceof SplFileInfo) {
79
            /*
80
             * File path components
81
             */
82
            // ie: content/Blog/Post 1.md
83
            //             |    |      └─ fileExtension
84
            //             |    └─ fileName
85
            //             └─ filePath
86
            $fileExtension = pathinfo($this->file, PATHINFO_EXTENSION);
87
            $filePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
88
            $fileName = $this->file->getBasename('.'.$fileExtension);
89
            // filePathname = filePath + '/' + fileName
90
            // ie: content/Blog/Post 1.md -> "Blog/Post 1"
91
            // ie: content/index.md -> "index"
92
            // ie: content/Blog/index.md -> "Blog/"
93
            $filePathname = ($filePath ? $filePath.'/' : '')
94
                .($filePath && $fileName == 'index' ? '' : $fileName);
95
            /*
96
             * Set properties
97
             *
98
             * id = path = rootpath / slug
99
             */
100
            $this->rootpath = $this->slugify($filePath); // ie: "blog"
101
            $this->slug = $this->slugify(Prefix::subPrefix($fileName)); // ie: "post-1"
102
            $this->path = $this->slugify(Prefix::subPrefix($filePathname)); // ie: "blog/post-1"
103
            $this->id = $this->path;
104
            /*
105
             * Set protected variables
106
             */
107
            $this->setSection(explode('/', $this->rootpath)[0]); // ie: "blog"
108
            /*
109
             * Set overridable variables
110
             */
111
            $this->setVariable('title', Prefix::subPrefix($fileName)); // ie: "Post 1"
112
            $this->setVariable('date', $this->file->getCTime());
113
            $this->setVariable('updated', $this->file->getMTime());
114
            $this->setVariable('weight', null);
115
            // special case: file has a prefix
116
            if (Prefix::hasPrefix($filePathname)) {
117
                // prefix is a valid date?
118
                if (Util::isValidDate(Prefix::getPrefix($filePathname))) {
119
                    $this->setVariable('date', (string) Prefix::getPrefix($filePathname));
120
                } else {
121
                    // prefix is an integer, use for sorting
122
                    $this->setVariable('weight', (int) Prefix::getPrefix($filePathname));
123
                }
124
            }
125
            // physical file relative path
126
            $this->setVariable('filepath', $this->file->getRelativePathname());
127
128
            parent::__construct($this->id);
129
        } else {
130
            // virtual page
131
            $this->virtual = true;
132
            // default variables
133
            $this->setVariables([
134
                'title'    => 'Page Title',
135
                'date'     => time(),
136
                'updated'  => time(),
137
                'weight'   => null,
138
                'filepath' => null,
139
            ]);
140
141
            parent::__construct();
142
        }
143
        // required
144
        $this->setType(Type::PAGE);
145
        $this->setVariables([
146
            'published'        => true,
147
            'virtual'          => $this->virtual,
148
            'content_template' => 'page.content.twig',
149
        ]);
150
    }
151
152
    /**
153
     * Parse file content.
154
     *
155
     * @return self
156
     */
157
    public function parse(): self
158
    {
159
        $parser = new Parser($this->file);
160
        $parsed = $parser->parse();
161
        $this->frontmatter = $parsed->getFrontmatter();
162
        $this->body = $parsed->getBody();
163
164
        return $this;
165
    }
166
167
    /**
168
     * Get frontmatter.
169
     *
170
     * @return string|null
171
     */
172
    public function getFrontmatter(): ?string
173
    {
174
        return $this->frontmatter;
175
    }
176
177
    /**
178
     * Get body as raw.
179
     *
180
     * @return string
181
     */
182
    public function getBody(): ?string
183
    {
184
        return $this->body;
185
    }
186
187
    /**
188
     * Turn a path (string) into a slug (URL).
189
     *
190
     * @param string $path
191
     *
192
     * @return string
193
     */
194
    public static function slugify(string $path): string
195
    {
196
        return Slugify::create([
197
            'regexp' => self::SLUGIFY_PATTERN,
198
        ])->slugify($path);
199
    }
200
201
    /**
202
     * Is current page is virtual?
203
     *
204
     * @return bool
205
     */
206
    public function isVirtual(): bool
207
    {
208
        return $this->virtual;
209
    }
210
211
    /**
212
     * Set page type.
213
     *
214
     * @param string $type
215
     *
216
     * @return self
217
     */
218
    public function setType(string $type): self
219
    {
220
        $this->type = new Type($type);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Cecil\Page\Type($type) of type object<Cecil\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...
221
222
        return $this;
223
    }
224
225
    /**
226
     * Get page type.
227
     *
228
     * @return string|null
229
     */
230
    public function getType(): ?string
231
    {
232
        return $this->type;
233
    }
234
235
    /**
236
     * Set slug.
237
     *
238
     * @param string $slug
239
     *
240
     * @return self
241
     */
242
    public function setSlug(string $slug): self
243
    {
244
        $this->slug = $slug;
245
246
        return $this;
247
    }
248
249
    /**
250
     * Get slug.
251
     *
252
     * @return string|null
253
     */
254
    public function getSlug(): ?string
255
    {
256
        return $this->slug;
257
    }
258
259
    /**
260
     * Set relative path.
261
     *
262
     * @param $rootpath
263
     *
264
     * @return self
265
     */
266
    public function setRootPath(string $rootpath): self
267
    {
268
        $this->rootpath = $rootpath;
269
270
        return $this;
271
    }
272
273
    /**
274
     * Get relative path.
275
     *
276
     * @return string|null
277
     */
278
    public function getRootPath(): ?string
279
    {
280
        return $this->rootpath;
281
    }
282
283
    /**
284
     * Set path.
285
     *
286
     * @param string $path
287
     *
288
     * @return self
289
     */
290
    public function setPath(string $path): self
291
    {
292
        $this->path = $path;
293
294
        return $this;
295
    }
296
297
    /**
298
     * Get path.
299
     *
300
     * @return string|null
301
     */
302
    public function getPath(): ?string
303
    {
304
        // special case: homepage
305
        if ($this->path == 'index'
306
            || (\strlen($this->path) >= 6
307
            && substr_compare($this->path, 'index/', 0, 6) == 0)) {
308
            $this->path = '';
309
        }
310
311
        return $this->path;
312
    }
313
314
    /**
315
     * Backward compatibility.
316
     */
317
    public function getPathname()
318
    {
319
        return $this->getPath();
320
    }
321
322
    /**
323
     * Set section.
324
     *
325
     * @param string $section
326
     *
327
     * @return self
328
     */
329
    public function setSection(string $section): self
330
    {
331
        $this->section = $section;
332
333
        return $this;
334
    }
335
336
    /**
337
     * Get section.
338
     *
339
     * @return string|null
340
     */
341
    public function getSection(): ?string
342
    {
343
        if (empty($this->section) && !empty($this->rootpath)) {
344
            $this->setSection(explode('/', $this->rootpath)[0]);
345
        }
346
347
        return $this->section;
348
    }
349
350
    /**
351
     * Set body as HTML.
352
     *
353
     * @param string $html
354
     *
355
     * @return self
356
     */
357
    public function setBodyHtml(string $html): self
358
    {
359
        $this->html = $html;
360
361
        return $this;
362
    }
363
364
    /**
365
     * Get body as HTML.
366
     *
367
     * @return string|null
368
     */
369
    public function getBodyHtml(): ?string
370
    {
371
        return $this->html;
372
    }
373
374
    /**
375
     * @see getBodyHtml()
376
     *
377
     * @return string|null
378
     */
379
    public function getContent(): ?string
380
    {
381
        return $this->getBodyHtml();
382
    }
383
384
    /**
385
     * Return output file.
386
     *
387
     * Use cases:
388
     *   - default: path + suffix + extension (ie: blog/post-1/index.html)
389
     *   - subpath: path + subpath + suffix + extension (ie: blog/post-1/amp/index.html)
390
     *   - ugly: path + extension (ie: 404.html, sitemap.xml, robots.txt)
391
     *   - path only (ie: _redirects)
392
     *
393
     * @param string $format
394
     * @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...
395
     *
396
     * @return string
397
     */
398
    public function getOutputFile(string $format, Config $config = null): string
399
    {
400
        $path = $this->getPath();
401
        $subpath = '';
402
        $suffix = '/index';
403
        $extension = 'html';
404
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
405
406
        // site config
407
        if ($config) {
408
            $subpath = $config->get(sprintf('site.output.formats.%s.subpath', $format));
409
            $suffix = $config->get(sprintf('site.output.formats.%s.suffix', $format));
410
            $extension = $config->get(sprintf('site.output.formats.%s.extension', $format));
411
        }
412
        // if ugly URL: not suffix
413
        if ($uglyurl) {
414
            $suffix = '';
415
        }
416
        // format strings
417
        if ($subpath) {
418
            $subpath = sprintf('/%s', ltrim($subpath, '/'));
419
        }
420
        if ($suffix) {
421
            $suffix = sprintf('/%s', ltrim($suffix, '/'));
422
        }
423
        if ($extension) {
424
            $extension = sprintf('.%s', $extension);
425
        }
426
        // special case: homepage
427
        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...
428
            $path = 'index';
429
        }
430
431
        return $path.$subpath.$suffix.$extension;
432
    }
433
434
    /**
435
     * Return URL.
436
     *
437
     * @param string $format
438
     * @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...
439
     *
440
     * @return string
441
     */
442
    public function getUrl(string $format = 'html', Config $config = null): string
443
    {
444
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
445
        $output = $this->getOutputFile($format, $config);
446
447
        if (!$uglyurl) {
448
            $output = str_replace('index.html', '', $output);
449
        }
450
451
        return $output;
452
    }
453
454
    /*
455
     * Helper to set and get variables.
456
     */
457
458
    /**
459
     * Set an array as variables.
460
     *
461
     * @param array $variables
462
     *
463
     * @throws \Exception
464
     *
465
     * @return $this
466
     */
467
    public function setVariables($variables)
468
    {
469
        if (!is_array($variables)) {
470
            throw new \Exception('Can\'t set variables: parameter is not an array');
471
        }
472
        foreach ($variables as $key => $value) {
473
            $this->setVariable($key, $value);
474
        }
475
476
        return $this;
477
    }
478
479
    /**
480
     * Get all variables.
481
     *
482
     * @return array
483
     */
484
    public function getVariables(): array
485
    {
486
        return $this->properties;
487
    }
488
489
    /**
490
     * Set a variable.
491
     *
492
     * @param $name
493
     * @param $value
494
     *
495
     * @throws \Exception
496
     *
497
     * @return $this
498
     */
499
    public function setVariable($name, $value)
500
    {
501
        if (is_bool($value)) {
502
            $value = $value ?: 0;
503
        }
504
        switch ($name) {
505
            case 'date':
506
                try {
507
                    if ($value instanceof \DateTime) {
508
                        $date = $value;
509
                    } else {
510
                        // timestamp
511
                        if (is_numeric($value)) {
512
                            $date = (new \DateTime())->setTimestamp($value);
513
                        } else {
514
                            // ie: 2019-01-01
515
                            if (is_string($value)) {
516
                                $date = new \DateTime($value);
517
                            }
518
                        }
519
                    }
520
                } catch (\Exception $e) {
521
                    throw new \Exception(sprintf('Expected date string for "date" in "%s": "%s"', $this->getId(), $value));
522
                }
523
                $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...
524
                break;
525
            case 'draft':
526
                if ($value === true) {
527
                    $this->offsetSet('published', false);
528
                }
529
                break;
530
            case 'url':
531
                $slug = self::slugify($value);
532
                if ($value != $slug) {
533
                    throw new \Exception(sprintf(
534
                        "'url' variable should be '%s', not '%s', in page '%s'",
535
                        $slug,
536
                        $value,
537
                        $this->getId()
538
                    ));
539
                }
540
                break;
541
            default:
542
                $this->offsetSet($name, $value);
543
        }
544
545
        return $this;
546
    }
547
548
    /**
549
     * Is variable exist?
550
     *
551
     * @param $name
552
     *
553
     * @return bool
554
     */
555
    public function hasVariable($name)
556
    {
557
        return $this->offsetExists($name);
558
    }
559
560
    /**
561
     * Get a variable.
562
     *
563
     * @param string $name
564
     *
565
     * @return mixed|null
566
     */
567
    public function getVariable($name)
568
    {
569
        if ($this->offsetExists($name)) {
570
            return $this->offsetGet($name);
571
        }
572
    }
573
574
    /**
575
     * Unset a variable.
576
     *
577
     * @param $name
578
     *
579
     * @return $this
580
     */
581
    public function unVariable($name)
582
    {
583
        if ($this->offsetExists($name)) {
584
            $this->offsetUnset($name);
585
        }
586
587
        return $this;
588
    }
589
}
590