Completed
Push — feature-output-formats ( 77af1f...1f46d0 )
by Arnaud
02:22
created

Page::getPathname()   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\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 geSlug(): ?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
        return $this->path;
305
    }
306
307
    /**
308
     * Backward compatibility.
309
     */
310
    public function getPathname()
311
    {
312
        return $this->getPath();
313
    }
314
315
    /**
316
     * Set section.
317
     *
318
     * @param string $section
319
     *
320
     * @return self
321
     */
322
    public function setSection(string $section): self
323
    {
324
        $this->section = $section;
325
326
        return $this;
327
    }
328
329
    /**
330
     * Get section.
331
     *
332
     * @return string|null
333
     */
334
    public function getSection(): ?string
335
    {
336
        if (empty($this->section) && !empty($this->rootpath)) {
337
            $this->setSection(explode('/', $this->rootpath)[0]);
338
        }
339
340
        return $this->section;
341
    }
342
343
    /**
344
     * Set body as HTML.
345
     *
346
     * @param string $html
347
     *
348
     * @return self
349
     */
350
    public function setBodyHtml(string $html): self
351
    {
352
        $this->html = $html;
353
354
        return $this;
355
    }
356
357
    /**
358
     * Get body as HTML.
359
     *
360
     * @return string|null
361
     */
362
    public function getBodyHtml(): ?string
363
    {
364
        return $this->html;
365
    }
366
367
    /**
368
     * @see getBodyHtml()
369
     *
370
     * @return string|null
371
     */
372
    public function getContent(): ?string
373
    {
374
        return $this->getBodyHtml();
375
    }
376
377
    /**
378
     * Return output file.
379
     *
380
     * Use cases:
381
     *   - default: path + suffix + extension (ie: blog/post-1/index.html)
382
     *   - subpath: path + subpath + suffix + extension (ie: blog/post-1/amp/index.html)
383
     *   - ugly: path + extension (ie: 404.html, sitemap.xml, robots.txt)
384
     *   - path only (ie: _redirects)
385
     *
386
     * @param string $format
387
     * @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...
388
     *
389
     * @return string
390
     */
391
    public function getOutputFile(string $format, Config $config = null): string
392
    {
393
        $path = $this->getPath();
394
        $subpath = '';
395
        $suffix = '/index';
396
        $extension = 'html';
397
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
398
399
        // site config
400
        if ($config) {
401
            $subpath = $config->get(sprintf('site.output.formats.%s.subpath', $format));
402
            $suffix = $config->get(sprintf('site.output.formats.%s.suffix', $format));
403
            $extension = $config->get(sprintf('site.output.formats.%s.extension', $format));
404
        }
405
        // if ugly URL: not suffix
406
        if ($uglyurl) {
407
            $suffix = '';
408
        }
409
        // format strings
410
        if ($subpath) {
411
            $subpath = sprintf('/%s', ltrim($subpath, '/'));
412
        }
413
        if ($suffix) {
414
            $suffix = sprintf('/%s', ltrim($suffix, '/'));
415
        }
416
        if ($extension) {
417
            $extension = sprintf('.%s', $extension);
418
        }
419
        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...
420
            $path = 'index'; // home page
421
        }
422
423
        return $path.$subpath.$suffix.$extension;
424
    }
425
426
    /**
427
     * Return URL.
428
     *
429
     * @param string $format
430
     * @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...
431
     *
432
     * @return string
433
     */
434
    public function getUrl(string $format = 'html', Config $config = null): string
435
    {
436
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
437
        $output = $this->getOutputFile($format, $config);
438
439
        if (!$uglyurl) {
440
            $output = str_replace('index.html', '', $output);
441
        }
442
443
        return $output;
444
    }
445
446
    /*
447
     * Helper to set and get variables.
448
     */
449
450
    /**
451
     * Set an array as variables.
452
     *
453
     * @param array $variables
454
     *
455
     * @throws \Exception
456
     *
457
     * @return $this
458
     */
459
    public function setVariables($variables)
460
    {
461
        if (!is_array($variables)) {
462
            throw new \Exception('Can\'t set variables: parameter is not an array');
463
        }
464
        foreach ($variables as $key => $value) {
465
            $this->setVariable($key, $value);
466
        }
467
468
        return $this;
469
    }
470
471
    /**
472
     * Get all variables.
473
     *
474
     * @return array
475
     */
476
    public function getVariables(): array
477
    {
478
        return $this->properties;
479
    }
480
481
    /**
482
     * Set a variable.
483
     *
484
     * @param $name
485
     * @param $value
486
     *
487
     * @throws \Exception
488
     *
489
     * @return $this
490
     */
491
    public function setVariable($name, $value)
492
    {
493
        if (is_bool($value)) {
494
            $value = $value ?: 0;
495
        }
496
        switch ($name) {
497
            case 'date':
498
                try {
499
                    if ($value instanceof \DateTime) {
500
                        $date = $value;
501
                    } else {
502
                        // timestamp
503
                        if (is_numeric($value)) {
504
                            $date = (new \DateTime())->setTimestamp($value);
505
                        } else {
506
                            // ie: 2019-01-01
507
                            if (is_string($value)) {
508
                                $date = new \DateTime($value);
509
                            }
510
                        }
511
                    }
512
                } catch (\Exception $e) {
513
                    throw new \Exception(sprintf("Expected date string in page '%s'", $this->getId()));
514
                }
515
                $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...
516
                break;
517
            case 'draft':
518
                if ($value === true) {
519
                    $this->offsetSet('published', false);
520
                }
521
                break;
522
            case 'url':
523
                $slug = self::slugify($value);
524
                if ($value != $slug) {
525
                    throw new \Exception(sprintf(
526
                        "'url' variable should be '%s', not '%s', in page '%s'",
527
                        $slug,
528
                        $value,
529
                        $this->getId()
530
                    ));
531
                }
532
                break;
533
            default:
534
                $this->offsetSet($name, $value);
535
        }
536
537
        return $this;
538
    }
539
540
    /**
541
     * Is variable exist?
542
     *
543
     * @param $name
544
     *
545
     * @return bool
546
     */
547
    public function hasVariable($name)
548
    {
549
        return $this->offsetExists($name);
550
    }
551
552
    /**
553
     * Get a variable.
554
     *
555
     * @param string $name
556
     *
557
     * @return mixed|null
558
     */
559
    public function getVariable($name)
560
    {
561
        if ($this->offsetExists($name)) {
562
            return $this->offsetGet($name);
563
        }
564
    }
565
566
    /**
567
     * Unset a variable.
568
     *
569
     * @param $name
570
     *
571
     * @return $this
572
     */
573
    public function unVariable($name)
574
    {
575
        if ($this->offsetExists($name)) {
576
            $this->offsetUnset($name);
577
        }
578
579
        return $this;
580
    }
581
}
582