Completed
Push — master ( 808f8c...952fde )
by Vladimir
11s
created

FrontMatterDocument::readContents()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4.0119

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 10
cts 11
cp 0.9091
rs 8.7972
c 0
b 0
f 0
cc 4
eloc 11
nc 4
nop 1
crap 4.0119
1
<?php
2
3
/**
4
 * @copyright 2018 Vladimir Jimenez
5
 * @license   https://github.com/stakx-io/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx\Document;
9
10
use allejo\stakx\Exception\FileAwareException;
11
use allejo\stakx\Exception\InvalidSyntaxException;
12
use allejo\stakx\FrontMatter\Exception\YamlVariableUndefinedException;
13
use allejo\stakx\FrontMatter\FrontMatterParser;
14
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
15
use Symfony\Component\Yaml\Exception\ParseException;
16
use Symfony\Component\Yaml\Yaml;
17
18
abstract class FrontMatterDocument extends ReadableDocument implements \IteratorAggregate, \ArrayAccess
19
{
20
    const TEMPLATE = "---\n%s\n---\n\n%s";
21
22
    /** @var array Functions that are white listed and can be called from templates. */
23
    public static $whiteListedFunctions = [
24
        'getPermalink', 'getRedirects', 'getTargetFile', 'getContent',
25
        'getFilename', 'getBasename', 'getExtension', 'isDraft',
26
    ];
27
28
    /** @var array FrontMatter keys that will be defined internally and cannot be overridden by users. */
29
    protected $specialFrontMatter = [
30
        'filePath' => null,
31
    ];
32
33
    /** @var bool Whether or not the body content has been evaluated yet. */
34
    protected $bodyContentEvaluated = false;
35
36
    /** @var FrontMatterParser */
37
    protected $frontMatterParser;
38
39
    /** @var array The raw FrontMatter that has not been evaluated. */
40
    protected $rawFrontMatter = [];
41
42
    /** @var array|null FrontMatter that is read from user documents. */
43
    protected $frontMatter = null;
44
45
    /** @var int The number of lines that Twig template errors should offset. */
46
    protected $lineOffset = 0;
47
48
    ///
49
    // Getter functions
50
    ///
51
52
    /**
53
     * {@inheritdoc}
54
     */
55 19
    public function getIterator()
56
    {
57 19
        return new \ArrayIterator($this->frontMatter);
58
    }
59
60
    /**
61
     * Get the number of lines that are taken up by FrontMatter and whitespace.
62
     *
63
     * @return int
64
     */
65
    public function getLineOffset()
66
    {
67
        return $this->lineOffset;
68
    }
69
70
    /**
71
     * Get whether or not this document is a draft.
72
     *
73
     * @return bool
74
     */
75 2
    public function isDraft()
76
    {
77 2
        return isset($this->frontMatter['draft']) && $this->frontMatter['draft'] === true;
78
    }
79
80
    ///
81
    // FrontMatter functionality
82
    ///
83
84
    /**
85
     * {@inheritdoc}
86
     */
87 117
    protected function beforeReadContents()
88
    {
89 117
        if (!$this->file->exists())
90
        {
91 1
            throw new FileNotFoundException(null, 0, null, $this->file->getAbsolutePath());
92
        }
93
94
        // If the "Last Modified" time is equal to what we have on record, then there's no need to read the file again
95 117
        if ($this->metadata['last_modified'] === $this->file->getMTime())
96
        {
97
            return false;
98
        }
99
100 117
        $this->metadata['last_modified'] = $this->file->getMTime();
101
102 117
        return true;
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108 117
    protected function readContents($readNecessary)
109
    {
110 117
        if (!$readNecessary)
111
        {
112
            return [];
113
        }
114
115 117
        $fileStructure = [];
116 117
        $rawFileContents = $this->file->getContents();
117 117
        preg_match('/---\R(.*?\R)?---(\s+)(.*)/s', $rawFileContents, $fileStructure);
118
119 117
        if (count($fileStructure) != 4)
120
        {
121 9
            throw new InvalidSyntaxException('Invalid FrontMatter file', 0, null, $this->getRelativeFilePath());
122
        }
123
124 108
        if (empty(trim($fileStructure[3])))
125
        {
126 1
            throw new InvalidSyntaxException('FrontMatter files must have a body to render', 0, null, $this->getRelativeFilePath());
127
        }
128
129 107
        return $fileStructure;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135 107
    protected function afterReadContents($fileStructure)
136
    {
137
        // The file wasn't modified since our last read, so we can exit out quickly
138 107
        if (empty($fileStructure))
139
        {
140
            return;
141
        }
142
143
        /*
144
         * $fileStructure[1] is the YAML
145
         * $fileStructure[2] is the amount of new lines after the closing `---` and the beginning of content
146
         * $fileStructure[3] is the body of the document
147
         */
148
149
        // The hard coded 1 is the offset used to count the new line used after the first `---` that is not caught in the regex
150 107
        $this->lineOffset = substr_count($fileStructure[1], "\n") + substr_count($fileStructure[2], "\n") + 1;
151
152
        //
153
        // Update the FM of the document, if necessary
154
        //
155
156 107
        $fmHash = md5($fileStructure[1]);
157
158 107
        if ($this->metadata['fm_hash'] !== $fmHash)
159
        {
160 107
            $this->metadata['fm_hash'] = $fmHash;
161
162 107
            if (!empty(trim($fileStructure[1])))
163
            {
164 91
                $this->rawFrontMatter = Yaml::parse($fileStructure[1], Yaml::PARSE_DATETIME);
165
166 91
                if (!empty($this->rawFrontMatter) && !is_array($this->rawFrontMatter))
167
                {
168 91
                    throw new ParseException('The evaluated FrontMatter should be an array');
169
                }
170
            }
171
            else
172
            {
173 19
                $this->rawFrontMatter = [];
174
            }
175
        }
176
177
        //
178
        // Update the body of the document, if necessary
179
        //
180
181 106
        $bodyHash = md5($fileStructure[3]);
182
183 106
        if ($this->metadata['body_sum'] !== $bodyHash)
184
        {
185 106
            $this->metadata['body_sum'] = $bodyHash;
186 106
            $this->bodyContent = $fileStructure[3];
187 106
            $this->bodyContentEvaluated = false;
188
        }
189 106
    }
190
191
    /**
192
     * Get the FrontMatter without evaluating its variables or special functionality.
193
     *
194
     * @return array
195
     */
196 18
    final public function getRawFrontMatter()
197
    {
198 18
        return $this->rawFrontMatter;
199
    }
200
201
    /**
202
     * Get the FrontMatter for this document.
203
     *
204
     * @param bool $evaluateYaml whether or not to evaluate any variables
205
     *
206
     * @return array
207
     */
208 14
    final public function getFrontMatter($evaluateYaml = true)
0 ignored issues
show
Unused Code introduced by
The parameter $evaluateYaml is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
209
    {
210 14
        if ($this->frontMatter === null)
211
        {
212
            throw new \LogicException('FrontMatter has not been evaluated yet, be sure FrontMatterDocument::evaluateFrontMatter() is called before.');
213
        }
214
215 14
        return $this->frontMatter;
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221 91
    final public function evaluateFrontMatter(array $variables = [], array $complexVariables = [])
222
    {
223 91
        $this->frontMatter = array_merge($this->rawFrontMatter, $variables);
224 91
        $this->evaluateYaml($this->frontMatter, $complexVariables);
225 90
    }
226
227
    /**
228
     * Returns true when the evaluated Front Matter has expanded values embedded.
229
     *
230
     * @return bool
231
     */
232 14
    final public function hasExpandedFrontMatter()
233
    {
234 14
        return $this->frontMatterParser !== null && $this->frontMatterParser->hasExpansion();
235
    }
236
237
    /**
238
     * Evaluate an array of data for FrontMatter variables. This function will modify the array in place.
239
     *
240
     * @param array $yaml An array of data containing FrontMatter variables
241
     *
242
     * @see $specialFrontMatter
243
     *
244
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
245
     */
246 91
    private function evaluateYaml(array &$yaml, array $complexVariables = [])
247
    {
248
        try
249
        {
250
            // The second parameter for this parser must match the $specialFrontMatter structure
251 91
            $this->frontMatterParser = new FrontMatterParser($yaml, [
252 91
                'filePath' => $this->getRelativeFilePath(),
253
            ]);
254 91
            $this->frontMatterParser->addComplexVariables($complexVariables);
255 91
            $this->frontMatterParser->parse();
256
        }
257 1
        catch (\Exception $e)
258
        {
259 1
            throw FileAwareException::castException($e, $this->getRelativeFilePath());
260
        }
261 90
    }
262
263
    ///
264
    // ArrayAccess Implementation
265
    ///
266
267
    /**
268
     * {@inheritdoc}
269
     */
270
    public function offsetSet($offset, $value)
271
    {
272
        throw new \LogicException('FrontMatter is read-only.');
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278 46
    public function offsetExists($offset)
279
    {
280 46
        if (isset($this->frontMatter[$offset]) || isset($this->specialFrontMatter[$offset]))
281
        {
282 37
            return true;
283
        }
284
285 23
        $fxnCall = 'get' . ucfirst($offset);
286
287 23
        return method_exists($this, $fxnCall) && in_array($fxnCall, static::$whiteListedFunctions);
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293
    public function offsetUnset($offset)
294
    {
295
        throw new \LogicException('FrontMatter is read-only.');
296
    }
297
298
    /**
299
     * {@inheritdoc}
300
     */
301 47
    public function offsetGet($offset)
302
    {
303 47
        if (isset($this->specialFrontMatter[$offset]))
304
        {
305
            return $this->specialFrontMatter[$offset];
306
        }
307
308 47
        $fxnCall = 'get' . ucfirst($offset);
309
310 47
        if (in_array($fxnCall, self::$whiteListedFunctions) && method_exists($this, $fxnCall))
311
        {
312 6
            return call_user_func_array([$this, $fxnCall], []);
313
        }
314
315 41
        if (isset($this->frontMatter[$offset]))
316
        {
317 40
            return $this->frontMatter[$offset];
318
        }
319
320 2
        return null;
321
    }
322
}
323