Completed
Push — master ( 323d53...359eec )
by Vladimir
02:11
created

FrontMatterObject::refreshFileContent()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 33
Code Lines 19

Duplication

Lines 12
Ratio 36.36 %

Code Coverage

Tests 20
CRAP Score 3.0198

Importance

Changes 0
Metric Value
cc 3
eloc 19
nc 3
nop 0
dl 12
loc 33
ccs 20
cts 23
cp 0.8696
crap 3.0198
rs 8.8571
c 0
b 0
f 0
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\Finder\SplFileInfo;
11
use Symfony\Component\Yaml\Yaml;
12
13
abstract class FrontMatterObject implements Jailable
14
{
15
    protected static $whiteListFunctions = array(
16
        'getPermalink', 'getRedirects', 'getTargetFile', 'getName', 'getFilePath', 'getRelativeFilePath', 'getContent'
17
    );
18
19
    /**
20
     * An array to keep track of collection or data dependencies used inside of a Twig template
21
     *
22
     * $dataDependencies['collections'] = array()
23
     * $dataDependencies['data'] = array()
24
     *
25
     * @var array
26
     */
27
    protected $dataDependencies;
28
29
    /**
30
     * FrontMatter values that can be injected or set after the file has been parsed. Values in this array will take
31
     * precedence over values in $frontMatter
32
     *
33
     * @var array
34
     */
35
    protected $writableFrontMatter;
36
37
    /**
38
     * A list of Front Matter values that should not be returned directly from the $frontMatter array. Values listed
39
     * here have dedicated functions that handle those Front Matter values and the respective functions should be called
40
     * instead.
41
     *
42
     * @var string[]
43
     */
44
    protected $frontMatterBlacklist;
45
46
    /**
47
     * Set to true if the front matter has already been evaluated with variable interpolation
48
     *
49
     * @var bool
50
     */
51
    protected $frontMatterEvaluated;
52
53
    /**
54
     * @var FrontMatterParser
55
     */
56
    protected $frontMatterParser;
57
58
    /**
59
     * An array containing the Yaml of the file
60
     *
61
     * @var array
62
     */
63
    protected $frontMatter;
64
65
    /**
66
     * Set to true if the body has already been parsed as markdown or any other format
67
     *
68
     * @var bool
69
     */
70
    protected $bodyContentEvaluated;
71
72
    /**
73
     * Only the body of the file, i.e. the content
74
     *
75
     * @var string
76
     */
77
    protected $bodyContent;
78
79
    /**
80
     * The extension of the file
81
     *
82
     * @var string
83
     */
84
    protected $extension;
85
86
    /**
87
     * The original file path to the ContentItem
88
     *
89
     * @var SplFileInfo
90
     */
91
    protected $filePath;
92
93
    /**
94
     * The permalink for this object
95
     *
96
     * @var string
97
     */
98
    protected $permalink;
99
100
    /**
101
     * A filesystem object
102
     *
103
     * @var Filesystem
104
     */
105
    protected $fs;
106
107
    /**
108
     * A list URLs that will redirect to this object
109
     *
110
     * @var string[]
111
     */
112
    private $redirects;
113
114
    /**
115
     * The number of lines that Twig template errors should offset
116
     *
117
     * @var int
118
     */
119
    private $lineOffset;
120
121
    /**
122
     * ContentItem constructor.
123
     *
124
     * @param string $filePath The path to the file that will be parsed into a ContentItem
125
     *
126
     * @throws FileNotFoundException The given file path does not exist
127
     * @throws IOException           The file was not a valid ContentItem. This would meam there was no front matter or
128
     *                               no body
129
     */
130 30
    public function __construct ($filePath)
131
    {
132 30
        $this->frontMatterBlacklist = array('permalink', 'redirects');
133 30
        $this->writableFrontMatter = array();
134
135 30
        $this->filePath  = $filePath;
0 ignored issues
show
Documentation Bug introduced by
It seems like $filePath of type string is incompatible with the declared type object<Symfony\Component\Finder\SplFileInfo> of property $filePath.

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...
136 30
        $this->fs        = new Filesystem();
137
138 30
        if (!$this->fs->exists($filePath))
139 30
        {
140 1
            throw new FileNotFoundException("The following file could not be found: ${filePath}");
141
        }
142
143
        $this->extension = strtolower($this->fs->getExtension($filePath));
144
145
        $this->refreshFileContent();
146
    }
147
148
    /**
149
     * The magic getter returns values from the front matter in order to make these values accessible to Twig templates
150
     * in a simple fashion
151
     *
152
     * @param  string $name The key in the front matter
153
     *
154
     * @return mixed|null
155
     */
156
    public function __get ($name)
157
    {
158 4
        if (isset($this->writableFrontMatter[$name]))
159 4
        {
160 1
            return $this->writableFrontMatter[$name];
161
        }
162
163 3
        return (isset($this->frontMatter[$name]) ? $this->frontMatter[$name] : null);
164
    }
165
166
    /**
167
     * The magic getter returns true if the value exists in the Front Matter. This is used in conjunction with the __get
168
     * function
169
     *
170
     * @param  string $name The name of the Front Matter value being looked for
171
     *
172
     * @return bool
173
     */
174
    public function __isset ($name)
175
    {
176 1
        return $this->isMagicGet($name);
177
    }
178
179
    /**
180
     * Check if a specific value is defined in the Front Matter
181
     *
182
     * @param  string $name
183
     *
184
     * @return bool
185
     */
186
    public function isMagicGet ($name)
187
    {
188
        return (
189 2
            !in_array($name, $this->frontMatterBlacklist)) &&
190 2
            (isset($this->frontMatter[$name]) || isset($this->writableFrontMatter[$name])
191 2
        );
192
    }
193
194
    /**
195
     * Return the body of the Content Item
196
     *
197
     * @return string
198
     */
199
    abstract public function getContent ();
200
201
    /**
202
     * @param array|null $variables An array of YAML variables to use in evaluating the `$permalink` value
203
     */
204
    final public function evaluateFrontMatter ($variables = null)
205
    {
206 2
        if (!is_null($variables))
207 2
        {
208 2
            $this->frontMatter = array_merge($this->frontMatter, $variables);
209 2
            $this->handleSpecialFrontMatter();
210 2
            $this->evaluateYaml($this->frontMatter);
211 2
        }
212 2
    }
213
214
    /**
215
     * Get the Front Matter of a ContentItem as an array
216
     *
217
     * @param  bool $evaluateYaml When set to true, the YAML will be evaluated for variables
218
     *
219
     * @return array
220
     */
221
    final public function getFrontMatter ($evaluateYaml = true)
222
    {
223 7
        if (is_null($this->frontMatter))
224 7
        {
225 1
            $this->frontMatter = array();
226 1
        }
227 6
        else if (!$this->frontMatterEvaluated && $evaluateYaml)
228 6
        {
229 6
            $this->evaluateYaml($this->frontMatter);
230 5
            $this->frontMatterEvaluated = true;
231 5
        }
232
233 6
        return $this->frontMatter;
234
    }
235
236
    /**
237
     * Get the permalink of this Content Item
238
     *
239
     * @return string
240
     * @throws \Exception
241
     */
242
    final public function getPermalink ()
243
    {
244 9
        if (!is_null($this->permalink))
245 9
        {
246 2
            return $this->permalink;
247
        }
248
249 8
        if (!is_null($this->frontMatterParser) && $this->frontMatterParser->hasExpansion())
250 8
        {
251
            throw new \Exception('The permalink for this item has not been set');
252
        }
253
254 8
        $permalink = (is_array($this->frontMatter) && isset($this->frontMatter['permalink'])) ?
255 8
            $this->frontMatter['permalink'] : $this->getPathPermalink();
256
257 8
        if (is_array($permalink))
258 8
        {
259 3
            $this->permalink = $permalink[0];
260 3
            array_shift($permalink);
261 3
            $this->redirects = $permalink;
262 3
        }
263
        else
264
        {
265 5
            $this->permalink = $permalink;
266 5
            $this->redirects = array();
267
        }
268
269 8
        $this->permalink = $this->sanitizePermalink($this->permalink);
270
271 8
        return $this->permalink;
272
    }
273
274
    /**
275
     * Get an array of URLs that will redirect to
276
     *
277
     * @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...
278
     */
279
    final public function getRedirects ()
280
    {
281 1
        if (is_null($this->redirects))
282 1
        {
283
            $this->getPermalink();
284
        }
285
286 1
        return $this->redirects;
287
    }
288
289
    /**
290
     * Get the destination of where this Content Item would be written to when the website is compiled
291
     *
292
     * @return string
293
     */
294
    final public function getTargetFile ()
295
    {
296 7
        $permalink = $this->getPermalink();
297 7
        $extension = $this->fs->getExtension($permalink);
298 7
        $permalink = str_replace('/', DIRECTORY_SEPARATOR, $permalink);
299
300 7
        if (empty($extension))
301 7
        {
302 3
            $permalink = rtrim($permalink, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'index.html';
303 3
        }
304
305 7
        return ltrim($permalink, DIRECTORY_SEPARATOR);
306
    }
307
308
    /**
309
     * Get the name of the item, which is just the file name without the extension
310
     *
311
     * @return string
312
     */
313
    final public function getName ()
314
    {
315 4
        return $this->fs->getBaseName($this->filePath);
316
    }
317
318
    /**
319
     * Get the original file path
320
     *
321
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be SplFileInfo?

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...
322
     */
323
    final public function getFilePath ()
324
    {
325 1
        return $this->filePath;
326
    }
327
328
    /**
329
     * Get the relative path to this file relative to the root of the Stakx website
330
     *
331
     * @return string
332
     */
333
    final public function getRelativeFilePath ()
334
    {
335 8
        if ($this->filePath instanceof SplFileInfo)
336 8
        {
337 5
            return $this->filePath->getRelativePathname();
338
        }
339
340
        // TODO ensure that we always get SplFileInfo objects, even when handling VFS documents
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
341 3
        return $this->fs->getRelativePath($this->filePath);
342
    }
343
344
    /**
345
     * The number of lines that are taken up by FrontMatter and white space
346
     *
347
     * @return int
348
     */
349
    final public function getLineOffset ()
350
    {
351
        return $this->lineOffset;
352
    }
353
354
    /**
355
     * Returns true when the evaluated Front Matter has expanded values embeded
356
     *
357
     * @return bool
358
     */
359
    final public function hasExpandedFrontMatter ()
360
    {
361 1
        return (!is_null($this->frontMatterParser) && $this->frontMatterParser->hasExpansion());
362
    }
363
364
    /**
365
     * Read the file, and parse its contents
366
     */
367
    final public function refreshFileContent ()
368
    {
369 29
        $rawFileContents = file_get_contents($this->filePath);
370
371 29
        $frontMatter = array();
372 29
        preg_match('/---(.*?)---(\n(?:[\s|\n]+)?)(.*)/s', $rawFileContents, $frontMatter);
373
374 29 View Code Duplication
        if (count($frontMatter) != 4)
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...
375 29
        {
376 2
            throw new IOException(sprintf("'%s' is not a valid ContentItem",
377 2
                    $this->fs->getFileName($this->filePath))
378 2
            );
379
        }
380
381 27 View Code Duplication
        if (empty(trim($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...
382 27
        {
383
            throw new IOException(sprintf('A ContentItem (%s) must have a body to render',
384
                    $this->fs->getFileName($this->filePath))
385
            );
386
        }
387
388 27
        $this->lineOffset  = substr_count($frontMatter[1], "\n") + substr_count($frontMatter[2], "\n");
389 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...
390 26
        $this->bodyContent = $frontMatter[3];
391
392 26
        $this->frontMatterEvaluated = false;
393 26
        $this->bodyContentEvaluated = false;
394 26
        $this->permalink = null;
395
396 26
        $this->handleSpecialFrontMatter();
397 26
        $this->findTwigDataDependencies('collections');
398 26
        $this->findTwigDataDependencies('data');
399 26
    }
400
401
    /**
402
     * Check whether this object has a reference to a collection or data item
403
     *
404
     * @param  string $namespace 'collections' or 'data'
405
     * @param  string $needle
406
     *
407
     * @return bool
408
     */
409
    final public function hasTwigDependency ($namespace, $needle)
410
    {
411
        return (in_array($needle, $this->dataDependencies[$namespace]));
412
    }
413
414
    /**
415
     * Append a custom FrontMatter value
416
     *
417
     * @param array $frontMatter
418
     */
419
    final public function appendFrontMatter (array $frontMatter)
420
    {
421
        foreach ($frontMatter as $key => $value)
422
        {
423
            $this->writableFrontMatter[$key] = $value;
424
        }
425
    }
426
427
    /**
428
     * Delete a custom FrontMatter value
429
     *
430
     * This will not delete a FrontMatter value parsed from the file
431
     *
432
     * @param string $key
433
     */
434
    final public function deleteFrontMatter ($key)
435
    {
436
        if (!isset($this->writableFrontMatter[$key])) { return; }
437
438
        unset($this->writableFrontMatter[$key]);
439
    }
440
441
    /**
442
     * Set custom FrontMatter values
443
     *
444
     * These custom values are temporary and will take precedence over Front Matter evaluated from the file but is only
445
     * available to Twig templates
446
     *
447
     * @param array $frontMatter
448
     */
449
    final public function setFrontMatter (array $frontMatter)
450
    {
451 1
        if (!is_array($frontMatter))
452 1
        {
453
            throw new \InvalidArgumentException('An array is required for setting the writable FrontMatter');
454
        }
455
456 1
        $this->writableFrontMatter = $frontMatter;
457 1
    }
458
459
    /**
460
     * Evaluate an array of data for FrontMatter variables. This function will modify the array in place.
461
     *
462
     * @param  array $yaml An array of data containing FrontMatter variables
463
     *
464
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
465
     */
466
    final protected function evaluateYaml (&$yaml)
467
    {
468 8
        $this->frontMatterParser = new FrontMatterParser($yaml);
469 7
    }
470
471
    /**
472
     * Handle special front matter values that need special treatment or have special meaning to a Content Item
473
     */
474
    private function handleSpecialFrontMatter ()
475
    {
476 26
        if (isset($this->frontMatter['date']))
477 26
        {
478
            try
479
            {
480
                // Coming from a string variable
481 3
                $itemDate = new \DateTime($this->frontMatter['date']);
482
            }
483 3
            catch (\Exception $e)
484
            {
485
                // YAML has parsed them to Epoch time
486 1
                $itemDate = \DateTime::createFromFormat('U', $this->frontMatter['date']);
487
            }
488
489 3
            if (!$itemDate === false)
490 3
            {
491 2
                $this->frontMatter['year']  = $itemDate->format('Y');
492 2
                $this->frontMatter['month'] = $itemDate->format('m');
493 2
                $this->frontMatter['day']   = $itemDate->format('d');
494 2
            }
495 3
        }
496 26
    }
497
498
    /**
499
     * Get all of the references to either DataItems or ContentItems inside of given string
500
     *
501
     * @param string $filter 'collections' or 'data'
502
     */
503
    private function findTwigDataDependencies ($filter)
504
    {
505 26
        $regex = '/{[{%](?:.+)?(?:' . $filter . ')(?:\.|\[\')(\w+)(?:\'\])?.+[%}]}/';
506 26
        $results = array();
507
508 26
        preg_match_all($regex, $this->bodyContent, $results);
509
510 26
        $this->dataDependencies[$filter] = array_unique($results[1]);
511 26
    }
512
513
    /**
514
     * Get the permalink based off the location of where the file is relative to the website. This permalink is to be
515
     * used as a fallback in the case that a permalink is not explicitly specified in the Front Matter.
516
     *
517
     * @return string
518
     */
519
    private function getPathPermalink ()
520
    {
521
        // Remove the protocol of the path, if there is one and prepend a '/' to the beginning
522 3
        $cleanPath = preg_replace('/[\w|\d]+:\/\//', '', $this->getRelativeFilePath());
523 3
        $cleanPath = ltrim($cleanPath, DIRECTORY_SEPARATOR);
524
525
        // Handle vfs:// paths by replacing their forward slashes with the OS appropriate directory separator
526 3
        if (DIRECTORY_SEPARATOR !== '/')
527 3
        {
528
            $cleanPath = str_replace('/', DIRECTORY_SEPARATOR, $cleanPath);
529
        }
530
531
        // Check the first folder and see if it's a data folder (starts with an underscore) intended for stakx
532 3
        $folders = explode(DIRECTORY_SEPARATOR, $cleanPath);
533
534 3
        if (substr($folders[0], 0, 1) === '_')
535 3
        {
536 1
            array_shift($folders);
537 1
        }
538
539 3
        $cleanPath = implode(DIRECTORY_SEPARATOR, $folders);
540
541 3
        return $cleanPath;
542
    }
543
544
    /**
545
     * Sanitize a permalink to remove unsupported characters or multiple '/' and replace spaces with hyphens
546
     *
547
     * @param  string $permalink A permalink
548
     *
549
     * @return string $permalink The sanitized permalink
550
     */
551
    private function sanitizePermalink ($permalink)
552
    {
553
        // Remove multiple '/' together
554 8
        $permalink = preg_replace('/\/+/', '/', $permalink);
555
556
        // Replace all spaces with hyphens
557 8
        $permalink = str_replace(' ', '-', $permalink);
558
559
        // Remove all disallowed characters
560 8
        $permalink = preg_replace('/[^0-9a-zA-Z-_\/\\\.]/', '', $permalink);
561
562
        // Handle unnecessary extensions
563 8
        $extensionsToStrip = array('twig');
564
565 8
        if (in_array($this->fs->getExtension($permalink), $extensionsToStrip))
566 8
        {
567 3
            $permalink = $this->fs->removeExtension($permalink);
568 3
        }
569
570
        // Remove a special './' combination from the beginning of a path
571 8
        if (substr($permalink, 0, 2) === '.' . DIRECTORY_SEPARATOR)
572 8
        {
573 1
            $permalink = substr($permalink, 2);
574 1
        }
575
576
        // Convert permalinks to lower case
577 8
        $permalink = mb_strtolower($permalink, 'UTF-8');
578
579 8
        return $permalink;
580
    }
581
}