Completed
Push — feature-output-formats ( 62e575...6f55bd )
by Arnaud
02:00
created

Page::getOutputFile()   B

Complexity

Conditions 9
Paths 128

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

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