Completed
Push — feature-output-formats ( a29221...37287d )
by Arnaud
02:15
created

Page   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 568
Duplicated Lines 0 %

Coupling/Cohesion

Components 6
Dependencies 8

Importance

Changes 0
Metric Value
wmc 60
lcom 6
cbo 8
dl 0
loc 568
rs 3.6
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 79 7
A parse() 0 9 1
A getFrontmatter() 0 4 1
A getBody() 0 4 1
A slugify() 0 6 1
A isVirtual() 0 4 1
A setType() 0 6 1
A getType() 0 4 1
A setName() 0 6 1
A getName() 0 4 1
A setPath() 0 6 1
A getPath() 0 4 1
A setPathname() 0 6 1
A getPathname() 0 10 1
A setSection() 0 6 1
A getSection() 0 8 3
A setBodyHtml() 0 6 1
A getBodyHtml() 0 4 1
A getContent() 0 4 1
B getOutputFile() 0 34 9
A getUrl() 0 10 3
A setVariables() 0 11 3
A getVariables() 0 4 1
C setVariable() 0 43 12
A hasVariable() 0 4 1
A getVariable() 0 6 2
A unVariable() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like Page often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Page, and based on these observations, apply Extract Interface, too.

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
            // file relative path
147
            $this->setVariable('filepathname', $this->file->getRelativePathname());
148
149
            parent::__construct($this->id);
150
        } else {
151
            // virtual page
152
            $this->virtual = true;
153
            // default variables
154
            $this->setVariables([
155
                'title'  => 'Default Title',
156
                'date'   => time(),
157
                'weight' => null,
158
            ]);
159
160
            parent::__construct();
161
        }
162
        $this->setType(Type::PAGE);
163
        // required variables
164
        $this->setVariable('virtual', $this->virtual);
165
        $this->setVariable('published', true);
166
        $this->setVariable('content_template', 'page.content.twig');
167
    }
168
169
    /**
170
     * Parse file content.
171
     *
172
     * @return self
173
     */
174
    public function parse(): self
175
    {
176
        $parser = new Parser($this->file);
177
        $parsed = $parser->parse();
178
        $this->frontmatter = $parsed->getFrontmatter();
179
        $this->body = $parsed->getBody();
180
181
        return $this;
182
    }
183
184
    /**
185
     * Get frontmatter.
186
     *
187
     * @return string|null
188
     */
189
    public function getFrontmatter(): ?string
190
    {
191
        return $this->frontmatter;
192
    }
193
194
    /**
195
     * Get body as raw.
196
     *
197
     * @return string
198
     */
199
    public function getBody(): ?string
200
    {
201
        return $this->body;
202
    }
203
204
    /**
205
     * Turn a path (string) into a slug (URL).
206
     *
207
     * @param string $path
208
     *
209
     * @return string
210
     */
211
    public static function slugify(string $path): string
212
    {
213
        return Slugify::create([
214
            'regexp' => self::SLUGIFY_PATTERN,
215
        ])->slugify($path);
216
    }
217
218
    /**
219
     * Is current page is virtual?
220
     *
221
     * @return bool
222
     */
223
    public function isVirtual(): bool
224
    {
225
        return $this->virtual;
226
    }
227
228
    /**
229
     * Set page type.
230
     *
231
     * @param string $type
232
     *
233
     * @return self
234
     */
235
    public function setType(string $type): self
236
    {
237
        $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...
238
239
        return $this;
240
    }
241
242
    /**
243
     * Get page type.
244
     *
245
     * @return string|null
246
     */
247
    public function getType(): ?string
248
    {
249
        return $this->type;
250
    }
251
252
    /**
253
     * Set name.
254
     *
255
     * @param string $name
256
     *
257
     * @return self
258
     */
259
    public function setName(string $name): self
260
    {
261
        $this->name = $name;
262
263
        return $this;
264
    }
265
266
    /**
267
     * Get name.
268
     *
269
     * @return string|null
270
     */
271
    public function getName(): ?string
272
    {
273
        return $this->name;
274
    }
275
276
    /**
277
     * Set path.
278
     *
279
     * @param $path
280
     *
281
     * @return self
282
     */
283
    public function setPath(string $path): self
284
    {
285
        $this->path = $path;
286
287
        return $this;
288
    }
289
290
    /**
291
     * Get path.
292
     *
293
     * @return string
294
     */
295
    public function getPath(): string
296
    {
297
        return $this->path;
298
    }
299
300
    /**
301
     * Set path name.
302
     *
303
     * @param string $pathname
304
     *
305
     * @return self
306
     */
307
    public function setPathname(string $pathname): self
308
    {
309
        $this->pathname = $pathname;
310
311
        return $this;
312
    }
313
314
    /**
315
     * Get path name.
316
     *
317
     * @return string
318
     */
319
    public function getPathname(): string
320
    {
321
        /*if ($this->hasVariable('url')
322
            && $this->pathname != $this->getVariable('url')
323
        ) {
324
            $this->setPathname($this->getVariable('url'));
325
        }*/
326
327
        return $this->pathname;
328
    }
329
330
    /**
331
     * Set section.
332
     *
333
     * @param string $section
334
     *
335
     * @return self
336
     */
337
    public function setSection(string $section): self
338
    {
339
        $this->section = $section;
340
341
        return $this;
342
    }
343
344
    /**
345
     * Get section.
346
     *
347
     * @return string|null
348
     */
349
    public function getSection(): ?string
350
    {
351
        if (empty($this->section) && !empty($this->path)) {
352
            $this->setSection(explode('/', $this->path)[0]);
353
        }
354
355
        return $this->section;
356
    }
357
358
    /**
359
     * Set body as HTML.
360
     *
361
     * @param string $html
362
     *
363
     * @return self
364
     */
365
    public function setBodyHtml(string $html): self
366
    {
367
        $this->html = $html;
368
369
        return $this;
370
    }
371
372
    /**
373
     * Get body as HTML.
374
     *
375
     * @return string|null
376
     */
377
    public function getBodyHtml(): ?string
378
    {
379
        return $this->html;
380
    }
381
382
    /**
383
     * @see getBodyHtml()
384
     *
385
     * @return string|null
386
     */
387
    public function getContent(): ?string
388
    {
389
        return $this->getBodyHtml();
390
    }
391
392
    /**
393
     * Return output file.
394
     *
395
     * Use cases:
396
     *   - default: pathname + suffix + extension (ie: blog/post-1/index.html)
397
     *   - subpath: pathname + subpath + suffix + extension (ie: blog/post-1/amp/index.html)
398
     *   - ugly: pathname + extension (ie: 404.html, sitemap.xml, robots.txt)
399
     *   - pathname only (ie: _redirects)
400
     *
401
     * @param string $format
402
     * @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...
403
     *
404
     * @return string
405
     */
406
    public function getOutputFile(string $format, Config $config = null): string
407
    {
408
        $pathname = $this->getPathname();
409
        $subpath = '';
410
        $suffix = '/index';
411
        $extension = 'html';
412
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
413
414
        // site config
415
        if ($config) {
416
            $subpath = $config->get(sprintf('site.output.formats.%s.subpath', $format));
417
            $suffix = $config->get(sprintf('site.output.formats.%s.suffix', $format));
418
            $extension = $config->get(sprintf('site.output.formats.%s.extension', $format));
419
        }
420
        // if ugly URL: not suffix
421
        if ($uglyurl) {
422
            $suffix = '';
423
        }
424
        // format strings
425
        if ($subpath) {
426
            $subpath = sprintf('/%s', ltrim($subpath, '/'));
427
        }
428
        if ($suffix) {
429
            $suffix = sprintf('/%s', ltrim($suffix, '/'));
430
        }
431
        if ($extension) {
432
            $extension = sprintf('.%s', $extension);
433
        }
434
        if (!$pathname && !$suffix) {
435
            $pathname = 'index'; // home page
436
        }
437
438
        return $pathname.$subpath.$suffix.$extension;
439
    }
440
441
    /**
442
     * Return URL.
443
     *
444
     * @param string $format
445
     * @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...
446
     *
447
     * @return string
448
     */
449
    public function getUrl(string $format = 'html', Config $config = null): string
450
    {
451
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
452
453
        if (!$uglyurl) {
454
            return str_replace('index.html', '', $this->getOutputFile($format, $config));
455
        }
456
457
        return $this->getOutputFile($format, $config);
458
    }
459
460
    /*
461
     * Helper to set and get variables.
462
     */
463
464
    /**
465
     * Set an array as variables.
466
     *
467
     * @param array $variables
468
     *
469
     * @throws \Exception
470
     *
471
     * @return $this
472
     */
473
    public function setVariables($variables)
474
    {
475
        if (!is_array($variables)) {
476
            throw new \Exception('Can\'t set variables: parameter is not an array');
477
        }
478
        foreach ($variables as $key => $value) {
479
            $this->setVariable($key, $value);
480
        }
481
482
        return $this;
483
    }
484
485
    /**
486
     * Get all variables.
487
     *
488
     * @return array
489
     */
490
    public function getVariables(): array
491
    {
492
        return $this->properties;
493
    }
494
495
    /**
496
     * Set a variable.
497
     *
498
     * @param $name
499
     * @param $value
500
     *
501
     * @throws \Exception
502
     *
503
     * @return $this
504
     */
505
    public function setVariable($name, $value)
506
    {
507
        if (is_bool($value)) {
508
            $value = $value ?: 0;
509
        }
510
        switch ($name) {
511
            case 'date':
512
                try {
513
                    if ($value instanceof \DateTime) {
514
                        $date = $value;
515
                    } else {
516
                        // timestamp
517
                        if (is_numeric($value)) {
518
                            $date = (new \DateTime())->setTimestamp($value);
519
                        } else {
520
                            // ie: 2019-01-01
521
                            if (is_string($value)) {
522
                                $date = new \DateTime($value);
523
                            }
524
                        }
525
                    }
526
                } catch (\Exception $e) {
527
                    throw new \Exception(sprintf("Expected date string in page '%s'", $this->getId()));
528
                }
529
                $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...
530
                break;
531
            case 'draft':
532
                if ($value === true) {
533
                    $this->offsetSet('published', false);
534
                }
535
                break;
536
            case 'url':
537
                $slug = self::slugify($value);
538
                if ($value != $slug) {
539
                    throw new \Exception(sprintf("'url' variable should be '%s', not '%s', in page '%s'", $slug, $value, $this->getId()));
540
                }
541
                break;
542
            default:
543
                $this->offsetSet($name, $value);
544
        }
545
546
        return $this;
547
    }
548
549
    /**
550
     * Is variable exist?
551
     *
552
     * @param $name
553
     *
554
     * @return bool
555
     */
556
    public function hasVariable($name)
557
    {
558
        return $this->offsetExists($name);
559
    }
560
561
    /**
562
     * Get a variable.
563
     *
564
     * @param string $name
565
     *
566
     * @return mixed|null
567
     */
568
    public function getVariable($name)
569
    {
570
        if ($this->offsetExists($name)) {
571
            return $this->offsetGet($name);
572
        }
573
    }
574
575
    /**
576
     * Unset a variable.
577
     *
578
     * @param $name
579
     *
580
     * @return $this
581
     */
582
    public function unVariable($name)
583
    {
584
        if ($this->offsetExists($name)) {
585
            $this->offsetUnset($name);
586
        }
587
588
        return $this;
589
    }
590
}
591