Completed
Pull Request — master (#20)
by Vladimir
02:14
created

FrontMatterObject   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 460
Duplicated Lines 2.61 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 96.85%

Importance

Changes 0
Metric Value
dl 12
loc 460
ccs 123
cts 127
cp 0.9685
rs 8.3157
c 0
b 0
f 0
wmc 43
lcom 1
cbo 5

20 Methods

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