Completed
Pull Request — master (#48)
by Vladimir
04:43
created

FrontMatterDocument::setFrontMatter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 6
cp 0.8333
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 2.0185
1
<?php
2
3
/**
4
 * @copyright 2017 Vladimir Jimenez
5
 * @license   https://github.com/allejo/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx\FrontMatter;
9
10
use allejo\stakx\Document\JailedDocumentInterface;
11
use allejo\stakx\Document\PermalinkDocument;
12
use allejo\stakx\Exception\FileAwareException;
13
use allejo\stakx\Exception\InvalidSyntaxException;
14
use allejo\stakx\FrontMatter\Exception\YamlVariableUndefinedException;
15
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
16
use Symfony\Component\Filesystem\Exception\IOException;
17
use Symfony\Component\Yaml\Exception\ParseException;
18
use Symfony\Component\Yaml\Yaml;
19
20
abstract class FrontMatterDocument extends PermalinkDocument implements
0 ignored issues
show
Coding Style introduced by
FrontMatterDocument does not seem to conform to the naming convention (^Abstract|Factory$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
21
    \ArrayAccess,
22
    JailedDocumentInterface,
23
    WritableDocumentInterface
24
{
25
    const TEMPLATE = "---\n%s\n---\n\n%s";
26
27
    /**
28
     * The names of FrontMatter keys that are specially defined for all Documents
29
     *
30
     * @var array
31
     */
32
    public static $specialFrontMatterKeys = array(
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $specialFrontMatterKeys exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
33
        'filename', 'basename'
34
    );
35
36
    protected static $whiteListFunctions = array(
37
        'getPermalink', 'getRedirects', 'getTargetFile', 'getName', 'getFilePath', 'getRelativeFilePath', 'getContent',
38
        'getExtension', 'getFrontMatter'
39
    );
40
41
    /**
42
     * An array to keep track of collection or data dependencies used inside of a Twig template.
43
     *
44
     * $dataDependencies['collections'] = array()
45
     * $dataDependencies['data'] = array()
46
     *
47
     * @var array
48
     */
49
    protected $dataDependencies;
50
51
    /**
52
     * FrontMatter values that can be injected or set after the file has been parsed. Values in this array will take
53
     * precedence over values in $frontMatter.
54
     *
55
     * @var array
56
     */
57
    protected $writableFrontMatter;
58
59
    /**
60
     * A list of Front Matter values that should not be returned directly from the $frontMatter array. Values listed
61
     * here have dedicated functions that handle those Front Matter values and the respective functions should be called
62
     * instead.
63
     *
64
     * @var string[]
65
     */
66
    protected $frontMatterBlacklist;
67
68
    /**
69
     * Set to true if the front matter has already been evaluated with variable interpolation.
70
     *
71
     * @var bool
72
     */
73
    protected $frontMatterEvaluated;
74
75
    /**
76
     * @var Parser
77
     */
78
    protected $frontMatterParser;
79
80
    /**
81
     * An array containing the Yaml of the file.
82
     *
83
     * @var array
84
     */
85
    protected $frontMatter;
86
87
    /**
88
     * Set to true if the body has already been parsed as markdown or any other format.
89
     *
90
     * @var bool
91
     */
92
    protected $bodyContentEvaluated;
93
94
    /**
95
     * Only the body of the file, i.e. the content.
96
     *
97
     * @var string
98
     */
99
    protected $bodyContent;
100
101
    /**
102
     * The number of lines that Twig template errors should offset.
103
     *
104
     * @var int
105
     */
106
    private $lineOffset;
107
108
    /**
109
     * ContentItem constructor.
110
     *
111
     * @param string $filePath The path to the file that will be parsed into a ContentItem
112
     *
113
     * @throws FileNotFoundException The given file path does not exist
114
     * @throws IOException           The file was not a valid ContentItem. This would meam there was no front matter or
115
     *                               no body
116
     */
117 117
    public function __construct($filePath)
118
    {
119 117
        $this->frontMatterBlacklist = array('permalink', 'redirects');
120 117
        $this->writableFrontMatter = array();
121
122 117
        parent::__construct($filePath);
123 105
    }
124
125
    /**
126
     * Return the body of the Content Item.
127
     *
128
     * @return string
129
     */
130
    abstract public function getContent();
131
132
    /**
133
     * The number of lines that are taken up by FrontMatter and white space.
134
     *
135
     * @return int
136
     */
137
    final public function getLineOffset()
138
    {
139
        return $this->lineOffset;
140
    }
141
142
    /**
143
     * Get the name of the item, which is just the filename without the extension.
144
     *
145
     * @return string
146
     */
147 62
    final public function getName()
148
    {
149 62
        return $this->getBaseName();
150
    }
151
152
    /**
153
     * Check whether this object has a reference to a collection or data item.
154
     *
155
     * @param string $namespace 'collections' or 'data'
156
     * @param string $needle
157
     *
158
     * @return bool
159
     */
160
    final public function hasTwigDependency($namespace, $needle)
161
    {
162
        return in_array($needle, $this->dataDependencies[$namespace]);
163
    }
164
165
    /**
166
     * Read the file, and parse its contents.
167
     */
168 116
    final public function refreshFileContent()
169
    {
170
        // This function can be called after the initial object was created and the file may have been deleted since the
171
        // creation of the object.
172 116 View Code Duplication
        if (!$this->fs->exists($this->filePath))
0 ignored issues
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...
173 116
        {
174 1
            throw new FileNotFoundException(null, 0, null, $this->filePath);
175
        }
176
177
        // $fileStructure[1] is the YAML
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
178
        // $fileStructure[2] is the amount of new lines after the closing `---` and the beginning of content
179
        // $fileStructure[3] is the body of the document
180 116
        $fileStructure = array();
181
182 116
        $rawFileContents = file_get_contents($this->filePath);
183 116
        preg_match('/---\R(.*?\R)?---(\s+)(.*)/s', $rawFileContents, $fileStructure);
184
185 116
        if (count($fileStructure) != 4)
186 116
        {
187 9
            throw new InvalidSyntaxException('Invalid FrontMatter file', 0, null, $this->getRelativeFilePath());
188
        }
189
190 107
        if (empty(trim($fileStructure[3])))
191 107
        {
192 1
            throw new InvalidSyntaxException('FrontMatter files must have a body to render', 0, null, $this->getRelativeFilePath());
193
        }
194
195
        // The hard coded 1 is the offset used to count the new line used after the first `---` that is not caught in the regex
196 106
        $this->lineOffset = substr_count($fileStructure[1], "\n") + substr_count($fileStructure[2], "\n") + 1;
197 106
        $this->bodyContent = $fileStructure[3];
198
199 106
        if (!empty(trim($fileStructure[1])))
200 106
        {
201 89
            $this->frontMatter = Yaml::parse($fileStructure[1], Yaml::PARSE_DATETIME);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Symfony\Component\Yaml\...l\Yaml::PARSE_DATETIME) 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...
202
203 89
            if (!empty($this->frontMatter) && !is_array($this->frontMatter))
204 89
            {
205 1
                throw new ParseException('The evaluated FrontMatter should be an array');
206
            }
207 88
        }
208
        else
209
        {
210 20
            $this->frontMatter = array();
211
        }
212
213 105
        $this->frontMatterEvaluated = false;
214 105
        $this->bodyContentEvaluated = false;
215 105
        $this->permalink = null;
216
217 105
        $this->findTwigDataDependencies('collections');
218 105
        $this->findTwigDataDependencies('data');
219 105
    }
220
221
    /**
222
     * Get all of the references to either DataItems or ContentItems inside of given string.
223
     *
224
     * @param string $filter 'collections' or 'data'
225
     */
226 105
    private function findTwigDataDependencies($filter)
227
    {
228 105
        $regex = '/{[{%](?:.+)?(?:' . $filter . ')(?:\.|\[\')(\w+)(?:\'\])?.+[%}]}/';
229 105
        $results = array();
230
231 105
        preg_match_all($regex, $this->bodyContent, $results);
232
233 105
        $this->dataDependencies[$filter] = array_unique($results[1]);
234 105
    }
235
236
    //
237
    // Permalink and redirect functionality
238
    //
239
240 39
    final protected function buildPermalink()
241
    {
242 39
        if (!is_null($this->permalink))
243 39
        {
244 8
            return;
245
        }
246
247 37
        if (!is_null($this->frontMatterParser) && $this->frontMatterParser->hasExpansion())
248 37
        {
249
            throw new \Exception('The permalink for this item has not been set');
250
        }
251
252 37
        $permalink = (is_array($this->frontMatter) && isset($this->frontMatter['permalink'])) ?
253 37
            $this->frontMatter['permalink'] : $this->getPathPermalink();
254
255 37 View Code Duplication
        if (is_array($permalink))
0 ignored issues
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...
256 37
        {
257 19
            $this->permalink = $permalink[0];
258 19
            array_shift($permalink);
259 19
            $this->redirects = $permalink;
260 19
        }
261
        else
262
        {
263 24
            $this->permalink = $permalink;
264 24
            $this->redirects = array();
265
        }
266 37
    }
267
268
    //
269
    // WritableFrontMatter Implementation
270
    //
271
272
    /**
273
     * {@inheritdoc}
274
     */
275 7
    final public function evaluateFrontMatter($variables = null)
276
    {
277 7
        if (!is_null($variables))
278 7
        {
279 7
            $this->frontMatter = array_merge($this->frontMatter, $variables);
280 7
            $this->evaluateYaml($this->frontMatter);
281 7
        }
282 7
    }
283
284
    /**
285
     * {@inheritdoc}
286
     */
287 29
    final public function getFrontMatter($evaluateYaml = true)
288
    {
289 29
        if (is_null($this->frontMatter))
290 29
        {
291
            $this->frontMatter = array();
292
        }
293 29
        elseif (!$this->frontMatterEvaluated && $evaluateYaml)
294
        {
295 23
            $this->evaluateYaml($this->frontMatter);
296 22
        }
297
298 28
        return $this->frontMatter;
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304 2
    final public function hasExpandedFrontMatter()
305
    {
306 2
        return !is_null($this->frontMatterParser) && $this->frontMatterParser->hasExpansion();
307
    }
308
309
    /**
310
     * {@inheritdoc.
311
     */
312
    final public function appendFrontMatter(array $frontMatter)
313
    {
314
        foreach ($frontMatter as $key => $value)
315
        {
316
            $this->writableFrontMatter[$key] = $value;
317
        }
318
    }
319
320
    /**
321
     * {@inheritdoc.
322
     */
323
    final public function deleteFrontMatter($key)
324
    {
325
        if (!isset($this->writableFrontMatter[$key]))
326
        {
327
            return;
328
        }
329
330
        unset($this->writableFrontMatter[$key]);
331
    }
332
333
    /**
334
     * {@inheritdoc.
335
     */
336 2
    final public function setFrontMatter(array $frontMatter)
337
    {
338 2
        if (!is_array($frontMatter))
339 2
        {
340
            throw new \InvalidArgumentException('An array is required for setting the writable FrontMatter');
341
        }
342
343 2
        $this->writableFrontMatter = $frontMatter;
344 2
    }
345
346
    /**
347
     * Evaluate an array of data for FrontMatter variables. This function will modify the array in place.
348
     *
349
     * @param array $yaml An array of data containing FrontMatter variables
350
     *
351
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
352
     */
353 30
    private function evaluateYaml(&$yaml)
354
    {
355
        try
356
        {
357 30
            $this->frontMatterParser = new Parser($yaml, array(
358 30
                'filename' => $this->getFileName(),
359 30
                'basename' => $this->getName(),
360 30
            ));
361 29
            $this->frontMatterEvaluated = true;
362
        }
363 30
        catch (\Exception $e)
364
        {
365 1
            throw FileAwareException::castException($e, $this->getRelativeFilePath());
366
        }
367 29
    }
368
369
    //
370
    // ArrayAccess Implementation
371
    //
372
373
    /**
374
     * {@inheritdoc}
375
     */
376
    public function offsetSet($offset, $value)
377
    {
378
        if (is_null($offset))
379
        {
380
            throw new \InvalidArgumentException('$offset cannot be null');
381
        }
382
383
        $this->writableFrontMatter[$offset] = $value;
384
    }
385
386
    /**
387
     * {@inheritdoc}
388
     */
389 31
    public function offsetExists($offset)
390
    {
391 31
        if (isset($this->writableFrontMatter[$offset]) || isset($this->frontMatter[$offset]))
392 31
        {
393 30
            return true;
394
        }
395
396 14
        $fxnCall = 'get' . ucfirst($offset);
397
398 14
        return method_exists($this, $fxnCall) && in_array($fxnCall, static::$whiteListFunctions);
399
    }
400
401
    /**
402
     * {@inheritdoc}
403
     */
404
    public function offsetUnset($offset)
405
    {
406
        unset($this->writableFrontMatter[$offset]);
407
    }
408
409
    /**
410
     * {@inheritdoc}
411
     */
412 48
    public function offsetGet($offset)
413
    {
414 48
        $fxnCall = 'get' . ucfirst($offset);
415
416 48
        if (in_array($fxnCall, self::$whiteListFunctions) && method_exists($this, $fxnCall))
417 48
        {
418 6
            return call_user_func_array(array($this, $fxnCall), array());
419
        }
420
421 42
        if (isset($this->writableFrontMatter[$offset]))
422 42
        {
423
            return $this->writableFrontMatter[$offset];
424
        }
425
426 42
        if (isset($this->frontMatter[$offset]))
427 42
        {
428 41
            return $this->frontMatter[$offset];
429
        }
430
431 5
        return null;
432
    }
433
}
434