Completed
Push — feature-output-formats ( 98428a...998147 )
by Arnaud
02:19
created

Page::getFolder()   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 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\Util;
14
use Cocur\Slugify\Slugify;
15
use Symfony\Component\Finder\SplFileInfo;
16
17
/**
18
 * Class Page.
19
 */
20
class Page extends Item
21
{
22
    const SLUGIFY_PATTERN = '/(^\/|[^_a-z0-9\/]|-)+/';
23
24
    /**
25
     * @var SplFileInfo|null
26
     */
27
    protected $file;
28
    /**
29
     * @var bool
30
     */
31
    protected $virtual = false;
32
    /**
33
     * @var string
34
     */
35
    protected $type;
36
    /**
37
     * @var string
38
     */
39
    protected $folder;
40
    /**
41
     * @var string
42
     */
43
    protected $slug;
44
    /**
45
     * @var string
46
     */
47
    protected $path;
48
    /**
49
     * @var string
50
     */
51
    protected $section;
52
    /**
53
     * @var string
54
     */
55
    protected $frontmatter;
56
    /**
57
     * @var string
58
     */
59
    protected $body;
60
    /**
61
     * @var string
62
     */
63
    protected $html;
64
65
    /**
66
     * Constructor.
67
     *
68
     * @param SplFileInfo|null $file
69
     */
70
    public function __construct(SplFileInfo $file = null)
71
    {
72
        $this->file = $file;
73
74
        // physical page
75
        if ($this->file instanceof SplFileInfo) {
76
            /*
77
             * File path components
78
             */
79
            // ie: content/Blog/Post 1.md
80
            //             |    |      └─ fileExtension
81
            //             |    └─ fileName
82
            //             └─ filePath
83
            $fileExtension = pathinfo($this->file, PATHINFO_EXTENSION);
84
            $filePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
85
            $fileName = $this->file->getBasename('.'.$fileExtension);
86
            // filePathname = filePath + '/' + fileName
87
            // ie: content/Blog/Post 1.md -> "Blog/Post 1"
88
            // ie: content/index.md -> "index"
89
            // ie: content/Blog/index.md -> "Blog/"
90
            $filePathname = ($filePath ? $filePath.'/' : '')
91
                .($filePath && $fileName == 'index' ? '' : $fileName);
92
            /*
93
             * Set properties
94
             *
95
             * id = path = folder / slug
96
             */
97
            $this->folder = $this->slugify($filePath); // ie: "blog"
98
            $this->slug = $this->slugify(Prefix::subPrefix($fileName)); // ie: "post-1"
99
            $this->path = $this->slugify(Prefix::subPrefix($filePathname)); // ie: "blog/post-1"
100
            $this->id = $this->path;
101
            /*
102
             * Set protected variables
103
             */
104
            $this->setSection(explode('/', $this->folder)[0]); // ie: "blog"
105
            /*
106
             * Set overridable variables
107
             */
108
            $this->setVariable('title', Prefix::subPrefix($fileName)); // ie: "Post 1"
109
            $this->setVariable('date', $this->file->getCTime());
110
            $this->setVariable('updated', $this->file->getMTime());
111
            $this->setVariable('weight', null);
112
            // special case: file has a prefix
113
            if (Prefix::hasPrefix($filePathname)) {
114
                // prefix is a valid date?
115
                if (Util::isValidDate(Prefix::getPrefix($filePathname))) {
116
                    $this->setVariable('date', (string) Prefix::getPrefix($filePathname));
117
                } else {
118
                    // prefix is an integer, use for sorting
119
                    $this->setVariable('weight', (int) Prefix::getPrefix($filePathname));
120
                }
121
            }
122
            // physical file relative path
123
            $this->setVariable('filepath', $this->file->getRelativePathname());
124
125
            parent::__construct($this->id);
126
        } else {
127
            // virtual page
128
            $this->virtual = true;
129
            // default variables
130
            $this->setVariables([
131
                'title'    => 'Page Title',
132
                'date'     => time(),
133
                'updated'  => time(),
134
                'weight'   => null,
135
                'filepath' => null,
136
            ]);
137
138
            parent::__construct();
139
        }
140
        // required
141
        $this->setType(Type::PAGE);
142
        $this->setVariables([
143
            'published'        => true,
144
            'virtual'          => $this->virtual,
145
            'content_template' => 'page.content.twig',
146
        ]);
147
    }
148
149
    /**
150
     * Parse file content.
151
     *
152
     * @return self
153
     */
154
    public function parse(): self
155
    {
156
        $parser = new Parser($this->file);
0 ignored issues
show
Bug introduced by
It seems like $this->file can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
157
        $parsed = $parser->parse();
158
        $this->frontmatter = $parsed->getFrontmatter();
159
        $this->body = $parsed->getBody();
160
161
        return $this;
162
    }
163
164
    /**
165
     * Get frontmatter.
166
     *
167
     * @return string|null
168
     */
169
    public function getFrontmatter(): ?string
170
    {
171
        return $this->frontmatter;
172
    }
173
174
    /**
175
     * Get body as raw.
176
     *
177
     * @return string
178
     */
179
    public function getBody(): ?string
180
    {
181
        return $this->body;
182
    }
183
184
    /**
185
     * Turn a path (string) into a slug (URL).
186
     *
187
     * @param string $path
188
     *
189
     * @return string
190
     */
191
    public static function slugify(string $path): string
192
    {
193
        return Slugify::create([
194
            'regexp' => self::SLUGIFY_PATTERN,
195
        ])->slugify($path);
196
    }
197
198
    /**
199
     * Is current page is virtual?
200
     *
201
     * @return bool
202
     */
203
    public function isVirtual(): bool
204
    {
205
        return $this->virtual;
206
    }
207
208
    /**
209
     * Set page type.
210
     *
211
     * @param string $type
212
     *
213
     * @return self
214
     */
215
    public function setType(string $type): self
216
    {
217
        $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...
218
219
        return $this;
220
    }
221
222
    /**
223
     * Get page type.
224
     *
225
     * @return string|null
226
     */
227
    public function getType(): ?string
228
    {
229
        return $this->type;
230
    }
231
232
    /**
233
     * Set slug.
234
     *
235
     * @param string $slug
236
     *
237
     * @return self
238
     */
239
    public function setSlug(string $slug): self
240
    {
241
        $this->slug = $slug;
242
243
        return $this;
244
    }
245
246
    /**
247
     * Get slug.
248
     *
249
     * @return string|null
250
     */
251
    public function getSlug(): ?string
252
    {
253
        return $this->slug;
254
    }
255
256
    /**
257
     * Set path without slug.
258
     *
259
     * @param $folder
260
     *
261
     * @return self
262
     */
263
    public function setFolder(string $folder): self
264
    {
265
        $this->folder = $folder;
266
267
        return $this;
268
    }
269
270
    /**
271
     * Get path without slug.
272
     *
273
     * @return string|null
274
     */
275
    public function getFolder(): ?string
276
    {
277
        return $this->folder;
278
    }
279
280
    /**
281
     * Set path.
282
     *
283
     * @param string $path
284
     *
285
     * @return self
286
     */
287
    public function setPath(string $path): self
288
    {
289
        $this->path = $path;
290
291
        return $this;
292
    }
293
294
    /**
295
     * Get path.
296
     *
297
     * @return string|null
298
     */
299
    public function getPath(): ?string
300
    {
301
        // special case: homepage
302
        if ($this->path == 'index'
303
            || (\strlen($this->path) >= 6
304
            && substr_compare($this->path, 'index/', 0, 6) == 0)) {
305
            $this->path = '';
306
        }
307
308
        return $this->path;
309
    }
310
311
    /**
312
     * Backward compatibility.
313
     */
314
    public function getPathname()
315
    {
316
        return $this->getPath();
317
    }
318
319
    /**
320
     * Set section.
321
     *
322
     * @param string $section
323
     *
324
     * @return self
325
     */
326
    public function setSection(string $section): self
327
    {
328
        $this->section = $section;
329
330
        return $this;
331
    }
332
333
    /**
334
     * Get section.
335
     *
336
     * @return string|null
337
     */
338
    public function getSection(): ?string
339
    {
340
        if (empty($this->section) && !empty($this->folder)) {
341
            $this->setSection(explode('/', $this->folder)[0]);
342
        }
343
344
        return $this->section;
345
    }
346
347
    /**
348
     * Set body as HTML.
349
     *
350
     * @param string $html
351
     *
352
     * @return self
353
     */
354
    public function setBodyHtml(string $html): self
355
    {
356
        $this->html = $html;
357
358
        return $this;
359
    }
360
361
    /**
362
     * Get body as HTML.
363
     *
364
     * @return string|null
365
     */
366
    public function getBodyHtml(): ?string
367
    {
368
        return $this->html;
369
    }
370
371
    /**
372
     * @see getBodyHtml()
373
     *
374
     * @return string|null
375
     */
376
    public function getContent(): ?string
377
    {
378
        return $this->getBodyHtml();
379
    }
380
381
    /**
382
     * Return output file.
383
     *
384
     * Use cases:
385
     *   - default: path + suffix + extension (ie: blog/post-1/index.html)
386
     *   - subpath: path + subpath + suffix + extension (ie: blog/post-1/amp/index.html)
387
     *   - ugly: path + extension (ie: 404.html, sitemap.xml, robots.txt)
388
     *   - path only (ie: _redirects)
389
     *
390
     * @param string $format
391
     * @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...
392
     *
393
     * @return string
394
     */
395
    public function getOutputFile(string $format, Config $config = null): string
396
    {
397
        $path = $this->getPath();
398
        $subpath = '';
399
        $suffix = '/index';
400
        $extension = 'html';
401
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
402
403
        // site config
404
        if ($config) {
405
            $subpath = $config->get(sprintf('site.output.formats.%s.subpath', $format));
406
            $suffix = $config->get(sprintf('site.output.formats.%s.suffix', $format));
407
            $extension = $config->get(sprintf('site.output.formats.%s.extension', $format));
408
        }
409
        // if ugly URL: not suffix
410
        if ($uglyurl) {
411
            $suffix = '';
412
        }
413
        // format strings
414
        if ($subpath) {
415
            $subpath = sprintf('/%s', ltrim($subpath, '/'));
416
        }
417
        if ($suffix) {
418
            $suffix = sprintf('/%s', ltrim($suffix, '/'));
419
        }
420
        if ($extension) {
421
            $extension = sprintf('.%s', $extension);
422
        }
423
        // special case: homepage
424
        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...
425
            $path = 'index';
426
        }
427
428
        return $path.$subpath.$suffix.$extension;
429
    }
430
431
    /**
432
     * Return URL.
433
     *
434
     * @param string $format
435
     * @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...
436
     *
437
     * @return string
438
     */
439
    public function getUrl(string $format = 'html', Config $config = null): string
440
    {
441
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
442
        $output = $this->getOutputFile($format, $config);
443
444
        if (!$uglyurl) {
445
            $output = str_replace('index.html', '', $output);
446
        }
447
448
        return $output;
449
    }
450
451
    /*
452
     * Helper to set and get variables.
453
     */
454
455
    /**
456
     * Set an array as variables.
457
     *
458
     * @param array $variables
459
     *
460
     * @throws \Exception
461
     *
462
     * @return $this
463
     */
464
    public function setVariables($variables)
465
    {
466
        if (!is_array($variables)) {
467
            throw new \Exception('Can\'t set variables: parameter is not an array');
468
        }
469
        foreach ($variables as $key => $value) {
470
            $this->setVariable($key, $value);
471
        }
472
473
        return $this;
474
    }
475
476
    /**
477
     * Get all variables.
478
     *
479
     * @return array
480
     */
481
    public function getVariables(): array
482
    {
483
        return $this->properties;
484
    }
485
486
    /**
487
     * Set a variable.
488
     *
489
     * @param $name
490
     * @param $value
491
     *
492
     * @throws \Exception
493
     *
494
     * @return $this
495
     */
496
    public function setVariable($name, $value)
497
    {
498
        if (is_bool($value)) {
499
            $value = $value ?: 0;
500
        }
501
        switch ($name) {
502
            case 'date':
503
                try {
504
                    if ($value instanceof \DateTime) {
505
                        $date = $value;
506
                    } else {
507
                        // timestamp
508
                        if (is_numeric($value)) {
509
                            $date = (new \DateTime())->setTimestamp($value);
510
                        } else {
511
                            // ie: 2019-01-01
512
                            if (is_string($value)) {
513
                                $date = new \DateTime($value);
514
                            }
515
                        }
516
                    }
517
                } catch (\Exception $e) {
518
                    throw new \Exception(sprintf('Expected date string for "date" in "%s": "%s"',
519
                        $this->getId(),
520
                        $value
521
                    ));
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