Completed
Push — master ( 40f944...ef99f8 )
by Vladimir
02:11
created

FrontMatterObject   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 545
Duplicated Lines 2.2 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 90.73%

Importance

Changes 0
Metric Value
dl 12
loc 545
ccs 137
cts 151
cp 0.9073
rs 7.4757
c 0
b 0
f 0
wmc 53
lcom 1
cbo 5

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 2
A __get() 0 9 3
A __isset() 0 4 1
A isMagicGet() 0 7 3
getContent() 0 1 ?
A evaluateFrontMatter() 0 9 2
A getFrontMatter() 0 14 4
C getPermalink() 0 31 7
A getRedirects() 0 9 2
A getTargetFile() 0 13 2
A getName() 0 4 1
A getFilePath() 0 4 1
A getRelativeFilePath() 0 4 1
A hasExpandedFrontMatter() 0 4 2
B refreshFileContent() 12 32 3
A hasTwigDependency() 0 4 1
A appendFrontMatter() 0 7 2
A deleteFrontMatter() 0 6 2
A setFrontMatter() 0 9 2
A evaluateYaml() 0 4 1
B handleSpecialFrontMatter() 0 23 4
A findTwigDataDependencies() 0 9 1
B getPathPermalink() 0 24 3
B sanitizePermalink() 0 30 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like FrontMatterObject 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 FrontMatterObject, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace allejo\stakx\Object;
4
5
use allejo\stakx\FrontMatter\FrontMatterParser;
6
use allejo\stakx\FrontMatter\YamlVariableUndefinedException;
7
use allejo\stakx\System\Filesystem;
8
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
9
use Symfony\Component\Filesystem\Exception\IOException;
10
use Symfony\Component\Yaml\Yaml;
11
12
abstract class FrontMatterObject implements Jailable
13
{
14
    protected static $whiteListFunctions = array(
15
        'getPermalink', 'getRedirects', 'getTargetFile', 'getName', 'getFilePath', 'getRelativeFilePath', 'getContent'
16
    );
17
18
    /**
19
     * An array to keep track of collection or data dependencies used inside of a Twig template
20
     *
21
     * $dataDependencies['collections'] = array()
22
     * $dataDependencies['data'] = array()
23
     *
24
     * @var array
25
     */
26
    protected $dataDependencies;
27
28
    /**
29
     * FrontMatter values that can be injected or set after the file has been parsed. Values in this array will take
30
     * precedence over values in $frontMatter
31
     *
32
     * @var array
33
     */
34
    protected $writableFrontMatter;
35
36
    /**
37
     * A list of Front Matter values that should not be returned directly from the $frontMatter array. Values listed
38
     * here have dedicated functions that handle those Front Matter values and the respective functions should be called
39
     * instead.
40
     *
41
     * @var string[]
42
     */
43
    protected $frontMatterBlacklist;
44
45
    /**
46
     * Set to true if the front matter has already been evaluated with variable interpolation
47
     *
48
     * @var bool
49
     */
50
    protected $frontMatterEvaluated;
51
52
    /**
53
     * @var FrontMatterParser
54
     */
55
    protected $frontMatterParser;
56
57
    /**
58
     * An array containing the Yaml of the file
59
     *
60
     * @var array
61
     */
62
    protected $frontMatter;
63
64
    /**
65
     * Set to true if the body has already been parsed as markdown or any other format
66
     *
67
     * @var bool
68
     */
69
    protected $bodyContentEvaluated;
70
71
    /**
72
     * Only the body of the file, i.e. the content
73
     *
74
     * @var string
75
     */
76
    protected $bodyContent;
77
78
    /**
79
     * The extension of the file
80
     *
81
     * @var string
82
     */
83
    protected $extension;
84
85
    /**
86
     * The original file path to the ContentItem
87
     *
88
     * @var string
89
     */
90
    protected $filePath;
91
92
    /**
93
     * The permalink for this object
94
     *
95
     * @var string
96
     */
97
    protected $permalink;
98
99
    /**
100
     * A filesystem object
101
     *
102
     * @var Filesystem
103
     */
104
    protected $fs;
105
106
    /**
107
     * A list URLs that will redirect to this object
108
     *
109
     * @var string[]
110
     */
111
    private $redirects;
112
113
    /**
114
     * ContentItem constructor.
115
     *
116
     * @param string $filePath The path to the file that will be parsed into a ContentItem
117
     *
118
     * @throws FileNotFoundException The given file path does not exist
119
     * @throws IOException           The file was not a valid ContentItem. This would meam there was no front matter or
120
     *                               no body
121
     */
122 30
    public function __construct ($filePath)
123
    {
124 30
        $this->frontMatterBlacklist = array('permalink', 'redirects');
125 30
        $this->writableFrontMatter = array();
126
127 30
        $this->filePath  = $filePath;
128 30
        $this->fs        = new Filesystem();
129
130 30
        if (!$this->fs->exists($filePath))
131 30
        {
132 1
            throw new FileNotFoundException("The following file could not be found: ${filePath}");
133
        }
134
135
        $this->extension = strtolower($this->fs->getExtension($filePath));
136
137
        $this->refreshFileContent();
138
    }
139
140
    /**
141
     * The magic getter returns values from the front matter in order to make these values accessible to Twig templates
142
     * in a simple fashion
143
     *
144
     * @param  string $name The key in the front matter
145
     *
146
     * @return mixed|null
147
     */
148
    public function __get ($name)
149
    {
150 4
        if (isset($this->writableFrontMatter[$name]))
151 4
        {
152 1
            return $this->writableFrontMatter[$name];
153
        }
154
155 3
        return (isset($this->frontMatter[$name]) ? $this->frontMatter[$name] : null);
156
    }
157
158
    /**
159
     * The magic getter returns true if the value exists in the Front Matter. This is used in conjunction with the __get
160
     * function
161
     *
162
     * @param  string $name The name of the Front Matter value being looked for
163
     *
164
     * @return bool
165
     */
166
    public function __isset ($name)
167
    {
168 1
        return $this->isMagicGet($name);
169
    }
170
171
    /**
172
     * Check if a specific value is defined in the Front Matter
173
     *
174
     * @param  string $name
175
     *
176
     * @return bool
177
     */
178
    public function isMagicGet ($name)
179
    {
180
        return (
181 2
            !in_array($name, $this->frontMatterBlacklist)) &&
182 2
            (isset($this->frontMatter[$name]) || isset($this->writableFrontMatter[$name])
183 2
        );
184
    }
185
186
    /**
187
     * Return the body of the Content Item
188
     *
189
     * @return string
190
     */
191
    abstract public function getContent ();
192
193
    /**
194
     * @param array|null $variables An array of YAML variables to use in evaluating the `$permalink` value
195
     */
196
    final public function evaluateFrontMatter ($variables = null)
197
    {
198 2
        if (!is_null($variables))
199 2
        {
200 2
            $this->frontMatter = array_merge($this->frontMatter, $variables);
201 2
            $this->handleSpecialFrontMatter();
202 2
            $this->evaluateYaml($this->frontMatter);
203 2
        }
204 2
    }
205
206
    /**
207
     * Get the Front Matter of a ContentItem as an array
208
     *
209
     * @param  bool $evaluateYaml When set to true, the YAML will be evaluated for variables
210
     *
211
     * @return array
212
     */
213
    final public function getFrontMatter ($evaluateYaml = true)
214
    {
215 7
        if (is_null($this->frontMatter))
216 7
        {
217 1
            $this->frontMatter = array();
218 1
        }
219 6
        else if (!$this->frontMatterEvaluated && $evaluateYaml)
220 6
        {
221 6
            $this->evaluateYaml($this->frontMatter);
222 5
            $this->frontMatterEvaluated = true;
223 5
        }
224
225 6
        return $this->frontMatter;
226
    }
227
228
    /**
229
     * Get the permalink of this Content Item
230
     *
231
     * @return string
232
     * @throws \Exception
233
     */
234
    final public function getPermalink ()
235
    {
236 9
        if (!is_null($this->permalink))
237 9
        {
238 2
            return $this->permalink;
239
        }
240
241 8
        if (!is_null($this->frontMatterParser) && $this->frontMatterParser->hasExpansion())
242 8
        {
243
            throw new \Exception('The permalink for this item has not been set');
244
        }
245
246 8
        $permalink = (is_array($this->frontMatter) && isset($this->frontMatter['permalink'])) ?
247 8
            $this->frontMatter['permalink'] : $this->getPathPermalink();
248
249 8
        if (is_array($permalink))
250 8
        {
251 3
            $this->permalink = $permalink[0];
252 3
            array_shift($permalink);
253 3
            $this->redirects = $permalink;
254 3
        }
255
        else
256
        {
257 5
            $this->permalink = $permalink;
258 5
            $this->redirects = array();
259
        }
260
261 8
        $this->permalink = $this->sanitizePermalink($this->permalink);
262
263 8
        return $this->permalink;
264
    }
265
266
    /**
267
     * Get an array of URLs that will redirect to
268
     *
269
     * @return string[]
0 ignored issues
show
Documentation introduced by
Should the return type not be null|string[]?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
270
     */
271
    final public function getRedirects ()
272
    {
273 1
        if (is_null($this->redirects))
274 1
        {
275
            $this->getPermalink();
276
        }
277
278 1
        return $this->redirects;
279
    }
280
281
    /**
282
     * Get the destination of where this Content Item would be written to when the website is compiled
283
     *
284
     * @return string
285
     */
286
    final public function getTargetFile ()
287
    {
288 7
        $permalink = $this->getPermalink();
289 7
        $extension = $this->fs->getExtension($permalink);
290 7
        $permalink = str_replace('/', DIRECTORY_SEPARATOR, $permalink);
291
292 7
        if (empty($extension))
293 7
        {
294 3
            $permalink = rtrim($permalink, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'index.html';
295 3
        }
296
297 7
        return ltrim($permalink, DIRECTORY_SEPARATOR);
298
    }
299
300
    /**
301
     * Get the name of the item, which is just the file name without the extension
302
     *
303
     * @return string
304
     */
305
    final public function getName ()
306
    {
307 4
        return $this->fs->getBaseName($this->filePath);
308
    }
309
310
    /**
311
     * Get the original file path
312
     *
313
     * @return string
314
     */
315
    final public function getFilePath ()
316
    {
317 1
        return $this->filePath;
318
    }
319
320
    /**
321
     * Get the relative path to this file relative to the root of the Stakx website
322
     *
323
     * @return string
324
     */
325
    final public function getRelativeFilePath ()
326
    {
327 8
        return $this->fs->getRelativePath($this->filePath);
328
    }
329
330
    /**
331
     * Returns true when the evaluated Front Matter has expanded values embeded
332
     *
333
     * @return bool
334
     */
335
    final public function hasExpandedFrontMatter ()
336
    {
337 1
        return (!is_null($this->frontMatterParser) && $this->frontMatterParser->hasExpansion());
338
    }
339
340
    /**
341
     * Read the file, and parse its contents
342
     */
343
    final public function refreshFileContent ()
344
    {
345 29
        $rawFileContents = file_get_contents($this->filePath);
346
347 29
        $frontMatter = array();
348 29
        preg_match('/---(.*?)---(.*)/s', $rawFileContents, $frontMatter);
349
350 29 View Code Duplication
        if (count($frontMatter) != 3)
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
351 29
        {
352 1
            throw new IOException(sprintf("'%s' is not a valid ContentItem",
353 1
                    $this->fs->getFileName($this->filePath))
354 1
            );
355
        }
356
357 28 View Code Duplication
        if (empty(trim($frontMatter[2])))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
358 28
        {
359 1
            throw new IOException(sprintf('A ContentItem (%s) must have a body to render',
360 1
                    $this->fs->getFileName($this->filePath))
361 1
            );
362
        }
363
364 27
        $this->frontMatter = Yaml::parse($frontMatter[1]);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Symfony\Component\Yaml\...:parse($frontMatter[1]) can also be of type string or object<stdClass>. However, the property $frontMatter is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
365 26
        $this->bodyContent = trim($frontMatter[2]);
366
367 26
        $this->frontMatterEvaluated = false;
368 26
        $this->bodyContentEvaluated = false;
369 26
        $this->permalink = null;
370
371 26
        $this->handleSpecialFrontMatter();
372 26
        $this->findTwigDataDependencies('collections');
373 26
        $this->findTwigDataDependencies('data');
374 26
    }
375
376
    /**
377
     * Check whether this object has a reference to a collection or data item
378
     *
379
     * @param  string $namespace 'collections' or 'data'
380
     * @param  string $needle
381
     *
382
     * @return bool
383
     */
384
    final public function hasTwigDependency ($namespace, $needle)
385
    {
386
        return (in_array($needle, $this->dataDependencies[$namespace]));
387
    }
388
389
    /**
390
     * Append a custom FrontMatter value
391
     *
392
     * @param array $frontMatter
393
     */
394
    final public function appendFrontMatter (array $frontMatter)
395
    {
396
        foreach ($frontMatter as $key => $value)
397
        {
398
            $this->writableFrontMatter[$key] = $value;
399
        }
400
    }
401
402
    /**
403
     * Delete a custom FrontMatter value
404
     *
405
     * This will not delete a FrontMatter value parsed from the file
406
     *
407
     * @param string $key
408
     */
409
    final public function deleteFrontMatter ($key)
410
    {
411
        if (!isset($this->writableFrontMatter[$key])) { return; }
412
413
        unset($this->writableFrontMatter[$key]);
414
    }
415
416
    /**
417
     * Set custom FrontMatter values
418
     *
419
     * These custom values are temporary and will take precedence over Front Matter evaluated from the file but is only
420
     * available to Twig templates
421
     *
422
     * @param array $frontMatter
423
     */
424
    final public function setFrontMatter (array $frontMatter)
425
    {
426 1
        if (!is_array($frontMatter))
427 1
        {
428
            throw new \InvalidArgumentException('An array is required for setting the writable FrontMatter');
429
        }
430
431 1
        $this->writableFrontMatter = $frontMatter;
432 1
    }
433
434
    /**
435
     * Evaluate an array of data for FrontMatter variables. This function will modify the array in place.
436
     *
437
     * @param  array $yaml An array of data containing FrontMatter variables
438
     *
439
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
440
     */
441
    final protected function evaluateYaml (&$yaml)
442
    {
443 8
        $this->frontMatterParser = new FrontMatterParser($yaml);
444 7
    }
445
446
    /**
447
     * Handle special front matter values that need special treatment or have special meaning to a Content Item
448
     */
449
    private function handleSpecialFrontMatter ()
450
    {
451 26
        if (isset($this->frontMatter['date']))
452 26
        {
453
            try
454
            {
455
                // Coming from a string variable
456 3
                $itemDate = new \DateTime($this->frontMatter['date']);
457
            }
458 3
            catch (\Exception $e)
459
            {
460
                // YAML has parsed them to Epoch time
461 1
                $itemDate = \DateTime::createFromFormat('U', $this->frontMatter['date']);
462
            }
463
464 3
            if (!$itemDate === false)
465 3
            {
466 2
                $this->frontMatter['year']  = $itemDate->format('Y');
467 2
                $this->frontMatter['month'] = $itemDate->format('m');
468 2
                $this->frontMatter['day']   = $itemDate->format('d');
469 2
            }
470 3
        }
471 26
    }
472
473
    /**
474
     * Get all of the references to either DataItems or ContentItems inside of given string
475
     *
476
     * @param string $filter 'collections' or 'data'
477
     */
478
    private function findTwigDataDependencies ($filter)
479
    {
480 26
        $regex = '/{[{%](?:.+)?(?:' . $filter . ')(?:\.|\[\')(\w+)(?:\'\])?.+[%}]}/';
481 26
        $results = array();
482
483 26
        preg_match_all($regex, $this->bodyContent, $results);
484
485 26
        $this->dataDependencies[$filter] = array_unique($results[1]);
486 26
    }
487
488
    /**
489
     * Get the permalink based off the location of where the file is relative to the website. This permalink is to be
490
     * used as a fallback in the case that a permalink is not explicitly specified in the Front Matter.
491
     *
492
     * @return string
493
     */
494
    private function getPathPermalink ()
495
    {
496
        // Remove the protocol of the path, if there is one and prepend a '/' to the beginning
497 3
        $cleanPath = preg_replace('/[\w|\d]+:\/\//', '', $this->getRelativeFilePath());
498 3
        $cleanPath = ltrim($cleanPath, DIRECTORY_SEPARATOR);
499
500
        // Handle vfs:// paths by replacing their forward slashes with the OS appropriate directory separator
501 3
        if (DIRECTORY_SEPARATOR !== '/')
502 3
        {
503
            $cleanPath = str_replace('/', DIRECTORY_SEPARATOR, $cleanPath);
504
        }
505
506
        // Check the first folder and see if it's a data folder (starts with an underscore) intended for stakx
507 3
        $folders = explode(DIRECTORY_SEPARATOR, $cleanPath);
508
509 3
        if (substr($folders[0], 0, 1) === '_')
510 3
        {
511 1
            array_shift($folders);
512 1
        }
513
514 3
        $cleanPath = implode(DIRECTORY_SEPARATOR, $folders);
515
516 3
        return $cleanPath;
517
    }
518
519
    /**
520
     * Sanitize a permalink to remove unsupported characters or multiple '/' and replace spaces with hyphens
521
     *
522
     * @param  string $permalink A permalink
523
     *
524
     * @return string $permalink The sanitized permalink
525
     */
526
    private function sanitizePermalink ($permalink)
527
    {
528
        // Remove multiple '/' together
529 8
        $permalink = preg_replace('/\/+/', '/', $permalink);
530
531
        // Replace all spaces with hyphens
532 8
        $permalink = str_replace(' ', '-', $permalink);
533
534
        // Remove all disallowed characters
535 8
        $permalink = preg_replace('/[^0-9a-zA-Z-_\/\\\.]/', '', $permalink);
536
537
        // Handle unnecessary extensions
538 8
        $extensionsToStrip = array('twig');
539
540 8
        if (in_array($this->fs->getExtension($permalink), $extensionsToStrip))
541 8
        {
542 3
            $permalink = $this->fs->removeExtension($permalink);
543 3
        }
544
545
        // Remove a special './' combination from the beginning of a path
546 8
        if (substr($permalink, 0, 2) === '.' . DIRECTORY_SEPARATOR)
547 8
        {
548 1
            $permalink = substr($permalink, 2);
549 1
        }
550
551
        // Convert permalinks to lower case
552 8
        $permalink = mb_strtolower($permalink, 'UTF-8');
553
554 8
        return $permalink;
555
    }
556
}