Completed
Push — master ( 45fc01...73ed94 )
by Vladimir
02:06
created

FrontMatterObject::appendFrontMatter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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