Completed
Push — master ( 9fd6c2...8d053c )
by Vladimir
02:02
created

FrontMatterObject   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 484
Duplicated Lines 2.48 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 97.87%

Importance

Changes 0
Metric Value
dl 12
loc 484
ccs 138
cts 141
cp 0.9787
rs 8.3396
c 0
b 0
f 0
wmc 44
lcom 1
cbo 5

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 2
A __get() 0 4 2
A __isset() 0 4 2
getContent() 0 1 ?
A evaluateFrontMatter() 0 9 2
B getPermalink() 0 26 5
A getRedirects() 0 9 2
A getTargetFile() 0 12 2
A getName() 0 4 1
A getFilePath() 0 4 1
A getRelativeFilePath() 0 4 1
B refreshFileContent() 12 32 3
A hasTwigDependency() 0 4 1
A evaluateYaml() 0 14 3
B evaluateYamlVar() 0 24 3
B handleSpecialFrontMatter() 0 23 4
A findTwigDataDependencies() 0 9 1
A getPathPermalink() 0 18 2
B sanitizePermalink() 0 30 3
A getFrontMatter() 0 14 4

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\System\Filesystem;
6
use allejo\stakx\Exception\YamlVariableUndefinedException;
7
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
8
use Symfony\Component\Filesystem\Exception\IOException;
9
use Symfony\Component\Yaml\Yaml;
10
11
abstract class FrontMatterObject
12
{
13
    /**
14
     * An array to keep track of collection or data dependencies used inside of a Twig template
15
     *
16
     * $dataDependencies['collections'] = array()
17
     * $dataDependencies['data'] = array()
18
     *
19
     * @var array
20
     */
21
    protected $dataDependencies;
22
23
    /**
24
     * A list of Front Matter values that should not be returned directly from the $frontMatter array. Values listed
25
     * here have dedicated functions that handle those Front Matter values and the respective functions should be called
26
     * instead.
27
     *
28
     * @var string[]
29
     */
30
    protected $frontMatterBlacklist;
31
32
    /**
33
     * Set to true if the front matter has already been evaluated with variable interpolation
34
     *
35
     * @var bool
36
     */
37
    protected $frontMatterEvaluated;
38
39
    /**
40
     * An array containing the Yaml of the file
41
     *
42
     * @var array
43
     */
44
    protected $frontMatter;
45
46
    /**
47
     * Set to true if the body has already been parsed as markdown or any other format
48
     *
49
     * @var bool
50
     */
51
    protected $bodyContentEvaluated;
52
53
    /**
54
     * Only the body of the file, i.e. the content
55
     *
56
     * @var string
57
     */
58
    protected $bodyContent;
59
60
    /**
61
     * The extension of the file
62
     *
63
     * @var string
64
     */
65
    protected $extension;
66
67
    /**
68
     * The original file path to the ContentItem
69
     *
70
     * @var string
71
     */
72
    protected $filePath;
73
74
    /**
75
     * A filesystem object
76
     *
77
     * @var Filesystem
78
     */
79
    protected $fs;
80
81
    /**
82
     * The permalink for this object
83
     *
84
     * @var string
85
     */
86
    private $permalink;
87
88
    /**
89
     * A list URLs that will redirect to this object
90
     *
91
     * @var string[]
92
     */
93
    private $redirects;
94
95
    /**
96
     * ContentItem constructor.
97
     *
98
     * @param string $filePath The path to the file that will be parsed into a ContentItem
99
     *
100
     * @throws FileNotFoundException The given file path does not exist
101
     * @throws IOException           The file was not a valid ContentItem. This would meam there was no front matter or
102
     *                               no body
103
     */
104 29
    public function __construct ($filePath)
105
    {
106 29
        $this->frontMatterBlacklist = array('permalink', 'redirects');
107
108 29
        $this->filePath  = $filePath;
109 29
        $this->fs        = new Filesystem();
110
111 29
        if (!$this->fs->exists($filePath))
112 29
        {
113 1
            throw new FileNotFoundException("The following file could not be found: ${filePath}");
114
        }
115
116
        $this->extension = strtolower($this->fs->getExtension($filePath));
117
118
        $this->refreshFileContent();
119
    }
120
121
    /**
122
     * The magic getter returns values from the front matter in order to make these values accessible to Twig templates
123
     * in a simple fashion
124
     *
125
     * @param  string $name The key in the front matter
126
     *
127
     * @return mixed|null
128
     */
129
    public function __get ($name)
130
    {
131 3
        return (array_key_exists($name, $this->frontMatter) ? $this->frontMatter[$name] : null);
132
    }
133
134
    /**
135
     * The magic getter returns true if the value exists in the Front Matter. This is used in conjunction with the __get
136
     * function
137
     *
138
     * @param  string $name The name of the Front Matter value being looked for
139
     *
140
     * @return bool
141
     */
142
    public function __isset ($name)
143
    {
144 1
        return (!in_array($name, $this->frontMatterBlacklist)) && array_key_exists($name, $this->frontMatter);
145
    }
146
147
    /**
148
     * Return the body of the Content Item
149
     *
150
     * @return string
151
     */
152
    abstract public function getContent ();
153
154
    /**
155
     * @param array|null $variables An array of YAML variables to use in evaluating the `$permalink` value
156
     */
157
    final public function evaluateFrontMatter ($variables = null)
158
    {
159 2
        if (!is_null($variables))
160 2
        {
161 2
            $this->frontMatter = array_merge($this->frontMatter, $variables);
162 2
            $this->handleSpecialFrontMatter();
163 2
            $this->evaluateYaml($this->frontMatter);
164 2
        }
165 2
    }
166
167
    /**
168
     * Get the Front Matter of a ContentItem as an array
169
     *
170
     * @param  bool $evaluateYaml When set to true, the YAML will be evaluated for variables
171
     *
172
     * @return array
173
     */
174
    final public function getFrontMatter ($evaluateYaml = true)
175
    {
176 6
        if (is_null($this->frontMatter))
177 6
        {
178 1
            $this->frontMatter = array();
179 1
        }
180 5
        else if (!$this->frontMatterEvaluated && $evaluateYaml)
181 5
        {
182 5
            $this->evaluateYaml($this->frontMatter);
183 4
            $this->frontMatterEvaluated = true;
184 4
        }
185
186 5
        return $this->frontMatter;
187
    }
188
189
    /**
190
     * Get the permalink of this Content Item
191
     *
192
     * @return string
193
     */
194
    final public function getPermalink ()
195
    {
196 8
        if (!is_null($this->permalink))
197 8
        {
198 1
            return $this->permalink;
199
        }
200
201 8
        $permalink = (is_array($this->frontMatter) && array_key_exists('permalink', $this->frontMatter)) ?
202 8
            $this->frontMatter['permalink'] : $this->getPathPermalink();
203
204 8
        if (is_array($permalink))
205 8
        {
206 1
            $this->permalink = $permalink[0];
207 1
            array_shift($permalink);
208 1
            $this->redirects = $permalink;
209 1
        }
210
        else
211
        {
212 7
            $this->permalink = $permalink;
213 7
            $this->redirects = array();
214
        }
215
216 8
        $this->permalink = $this->sanitizePermalink($this->permalink);
217
218 8
        return $this->permalink;
219
    }
220
221
    /**
222
     * Get an array of URLs that will redirect to
223
     *
224
     * @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...
225
     */
226
    final public function getRedirects ()
227
    {
228 1
        if (is_null($this->redirects))
229 1
        {
230
            $this->getPermalink();
231
        }
232
233 1
        return $this->redirects;
234
    }
235
236
    /**
237
     * Get the destination of where this Content Item would be written to when the website is compiled
238
     *
239
     * @return string
240
     */
241
    final public function getTargetFile ()
242
    {
243 6
        $permalink  = $this->getPermalink();
244 6
        $extension  = $this->fs->getExtension($permalink);
245
246 6
        if (empty($extension))
247 6
        {
248 2
            $permalink = rtrim($permalink, '/') . '/index.html';
249 2
        }
250
251 6
        return ltrim($permalink, '/');
252
    }
253
254
    /**
255
     * Get the name of the item, which is just the file name without the extension
256
     *
257
     * @return string
258
     */
259
    final public function getName ()
260
    {
261 4
        return $this->fs->getBaseName($this->filePath);
262
    }
263
264
    /**
265
     * Get the original file path
266
     *
267
     * @return string
268
     */
269
    final public function getFilePath ()
270
    {
271 1
        return $this->filePath;
272
    }
273
274
    /**
275
     * Get the relative path to this file relative to the root of the Stakx website
276
     *
277
     * @return string
278
     */
279
    final public function getRelativeFilePath ()
280
    {
281 4
        return $this->fs->getRelativePath($this->filePath);
282
    }
283
284
    /**
285
     * Read the file, and parse its contents
286
     */
287
    final public function refreshFileContent ()
288
    {
289 28
        $rawFileContents = file_get_contents($this->filePath);
290
291 28
        $frontMatter = array();
292 28
        preg_match('/---(.*?)---(.*)/s', $rawFileContents, $frontMatter);
293
294 28 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...
295 28
        {
296 1
            throw new IOException(sprintf("'%s' is not a valid ContentItem",
297 1
                    $this->fs->getFileName($this->filePath))
298 1
            );
299
        }
300
301 27 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...
302 27
        {
303 1
            throw new IOException(sprintf('A ContentItem (%s) must have a body to render',
304 1
                    $this->fs->getFileName($this->filePath))
305 1
            );
306
        }
307
308 26
        $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...
309 25
        $this->bodyContent = trim($frontMatter[2]);
310
311 25
        $this->frontMatterEvaluated = false;
312 25
        $this->bodyContentEvaluated = false;
313 25
        $this->permalink = null;
314
315 25
        $this->handleSpecialFrontMatter();
316 25
        $this->findTwigDataDependencies('collections');
317 25
        $this->findTwigDataDependencies('data');
318 25
    }
319
320
    /**
321
     * Check whether this object has a reference to a collection or data item
322
     *
323
     * @param  string $namespace 'collections' or 'data'
324
     * @param  string $needle
325
     *
326
     * @return bool
327
     */
328
    final public function hasTwigDependency ($namespace, $needle)
329
    {
330
        return (in_array($needle, $this->dataDependencies[$namespace]));
331
    }
332
333
    /**
334
     * Evaluate an array of data for FrontMatter variables. This function will modify the array in place.
335
     *
336
     * @param  array $yaml An array of data containing FrontMatter variables
337
     *
338
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
339
     */
340
    final protected function evaluateYaml (&$yaml)
341
    {
342 7
        foreach ($yaml as $key => $value)
343
        {
344 7
            if (is_array($yaml[$key]))
345 7
            {
346 1
                $this->evaluateYaml($yaml[$key]);
347 1
            }
348
            else
349
            {
350 7
                $yaml[$key] = $this->evaluateYamlVar($value, $this->frontMatter);
351
            }
352 6
        }
353 6
    }
354
355
    /**
356
     * Evaluate an string for FrontMatter variables and replace them with the corresponding values
357
     *
358
     * @param  string $string The string that will be evaluated
359
     * @param  array  $yaml   The existing front matter from which the variable values will be pulled from
360
     *
361
     * @return string The final string with variables evaluated
362
     *
363
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
364
     */
365
    private function evaluateYamlVar ($string, $yaml)
366
    {
367 7
        $variables = array();
368 7
        $varRegex  = '/((?<!\\\\)%[a-zA-Z]+)/';
369 7
        $output    = $string;
370
371 7
        preg_match_all($varRegex, $string, $variables);
372
373
        // Default behavior causes $variables[0] is the entire string that was matched. $variables[1] will be each
374
        // matching result individually.
375 7
        foreach ($variables[1] as $variable)
376
        {
377 6
            $yamlVar = substr($variable, 1); // Trim the '%' from the YAML variable name
378
379 6
            if (!array_key_exists($yamlVar, $yaml))
380 6
            {
381 1
                throw new YamlVariableUndefinedException("Yaml variable `$variable` is not defined");
382
            }
383
384 5
            $output = str_replace($variable, $yaml[$yamlVar], $output);
385 6
        }
386
387 6
        return $output;
388
    }
389
390
    /**
391
     * Handle special front matter values that need special treatment or have special meaning to a Content Item
392
     */
393
    private function handleSpecialFrontMatter ()
394
    {
395 25
        if (isset($this->frontMatter['date']))
396 25
        {
397
            try
398
            {
399
                // Coming from a string variable
400 3
                $itemDate = new \DateTime($this->frontMatter['date']);
401
            }
402 3
            catch (\Exception $e)
403
            {
404
                // YAML has parsed them to Epoch time
405 1
                $itemDate = \DateTime::createFromFormat('U', $this->frontMatter['date']);
406
            }
407
408 3
            if (!$itemDate === false)
409 3
            {
410 2
                $this->frontMatter['year']  = $itemDate->format('Y');
411 2
                $this->frontMatter['month'] = $itemDate->format('m');
412 2
                $this->frontMatter['day']   = $itemDate->format('d');
413 2
            }
414 3
        }
415 25
    }
416
417
    /**
418
     * Get all of the references to either DataItems or ContentItems inside of given string
419
     *
420
     * @param string $filter 'collections' or 'data'
421
     */
422
    private function findTwigDataDependencies ($filter)
423
    {
424 25
        $regex = '/{[{%](?:.+)?(?:' . $filter . ')(?:\.|\[\')(\w+)(?:\'\])?.+[%}]}/';
425 25
        $results = array();
426
427 25
        preg_match_all($regex, $this->bodyContent, $results);
428
429 25
        $this->dataDependencies[$filter] = array_unique($results[1]);
430 25
    }
431
432
    /**
433
     * Get the permalink based off the location of where the file is relative to the website. This permalink is to be
434
     * used as a fallback in the case that a permalink is not explicitly specified in the Front Matter.
435
     *
436
     * @return string
437
     */
438
    private function getPathPermalink ()
439
    {
440
        // Remove the protocol of the path, if there is one and prepend a '/' to the beginning
441 3
        $cleanPath = preg_replace('/[\w|\d]+:\/\//', '', $this->filePath);
442 3
        $cleanPath = ltrim($cleanPath, DIRECTORY_SEPARATOR);
443
444
        // Check the first folder and see if it's a data folder (starts with an underscore) intended for stakx
445 3
        $folders = explode('/', $cleanPath);
446
447 3
        if (substr($folders[0], 0, 1) === '_')
448 3
        {
449 1
            array_shift($folders);
450 1
        }
451
452 3
        $cleanPath = implode(DIRECTORY_SEPARATOR, $folders);
453
454 3
        return $cleanPath;
455
    }
456
457
    /**
458
     * Sanitize a permalink to remove unsupported characters or multiple '/' and replace spaces with hyphens
459
     *
460
     * @param  string $permalink A permalink
461
     *
462
     * @return string $permalink The sanitized permalink
463
     */
464
    private function sanitizePermalink ($permalink)
465
    {
466
        // Remove multiple '/' together
467 8
        $permalink = preg_replace('/\/+/', '/', $permalink);
468
469
        // Replace all spaces with hyphens
470 8
        $permalink = str_replace(' ', '-', $permalink);
471
472
        // Remove all disallowed characters
473 8
        $permalink = preg_replace('/[^0-9a-zA-Z-_\/\.]/', '', $permalink);
474
475
        // Handle unnecessary extensions
476 8
        $extensionsToStrip = array('twig');
477
478 8
        if (in_array($this->fs->getExtension($permalink), $extensionsToStrip))
479 8
        {
480 3
            $permalink = $this->fs->removeExtension($permalink);
481 3
        }
482
483
        // Remove a special './' combination from the beginning of a path
484 8
        if (substr($permalink, 0, 2) === './')
485 8
        {
486 1
            $permalink = substr($permalink, 2);
487 1
        }
488
489
        // Convert permalinks to lower case
490 8
        $permalink = mb_strtolower($permalink, 'UTF-8');
491
492 8
        return $permalink;
493
    }
494
}