Completed
Pull Request — master (#45)
by Vladimir
02:32
created

Parser::evaluateBlock()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 15
nc 6
nop 1
dl 0
loc 30
ccs 22
cts 22
cp 1
crap 6
rs 8.439
c 0
b 0
f 0
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\FrontMatter\Exception\YamlUnsupportedVariableException;
11
use allejo\stakx\FrontMatter\Exception\YamlVariableUndefinedException;
12
use allejo\stakx\Utilities\ArrayUtilities;
13
14
class Parser
15
{
16
    /**
17
     * The RegEx used to identify Front Matter variables.
18
     */
19
    const VARIABLE_DEF = '/(?<!\\\\)%([a-zA-Z]+)/';
20
21
    /**
22
     * A list of special fields in the Front Matter that will support expansion.
23
     *
24
     * @var string[]
25
     */
26
    private static $expandableFields = array('permalink');
27
28
    /**
29
     * Whether or not an field was expanded into several values.
30
     *
31
     * Only fields specified in $expandableFields will cause this value to be set to true
32
     *
33
     * @var bool
34
     */
35
    private $expansionUsed;
36
37
    /**
38
     * The current depth of the recursion for evaluating nested arrays in the Front Matter.
39
     *
40
     * @var int
41
     */
42
    private $nestingLevel;
43
44
    /**
45
     * The current hierarchy of the keys that are being evaluated.
46
     *
47
     * Since arrays can be nested, we'll keep track of the keys up until the current depth. This information is used for
48
     * error reporting
49
     *
50
     * @var array
51
     */
52
    private $yamlKeys;
53
54
    /**
55
     * The entire Front Matter block; evaluation will happen in place.
56
     *
57
     * @var array
58
     */
59
    private $frontMatter;
60
61
    /**
62
     * FrontMatterParser constructor.
63
     *
64
     * @param array $rawFrontMatter
65
     */
66 44
    public function __construct(&$rawFrontMatter)
67
    {
68 44
        $this->expansionUsed = false;
69 44
        $this->nestingLevel = 0;
70 44
        $this->yamlKeys = array();
71
72 44
        $this->frontMatter = &$rawFrontMatter;
73
74 44
        $this->handleSpecialFrontMatter();
75 44
        $this->evaluateBlock($this->frontMatter);
76 39
    }
77
78
    /**
79
     * True if any fields were expanded in the Front Matter block.
80
     *
81
     * @return bool
82
     */
83 20
    public function hasExpansion()
84
    {
85 20
        return $this->expansionUsed;
86
    }
87
88
    //
89
    // Special FrontMatter fields
90
    //
91
92
    /**
93
     * Special treatment for some FrontMatter variables.
94
     */
95 44
    private function handleSpecialFrontMatter()
96
    {
97 44
        $this->handleDateField();
98 44
    }
99
100
    /**
101
     * Special treatment for the `date` field in FrontMatter that creates three new variables: year, month, day.
102
     */
103 44
    private function handleDateField()
104
    {
105 44
        if (!isset($this->frontMatter['date']))
106 44
        {
107 37
            return;
108
        }
109
110 7
        $date = &$this->frontMatter['date'];
111 7
        $itemDate = $this->guessDateTime($date);
112
113 7
        if (!$itemDate === false)
114 7
        {
115 7
            $this->frontMatter['date'] = $itemDate->format('U');
116 7
            $this->frontMatter['year'] = $itemDate->format('Y');
117 7
            $this->frontMatter['month'] = $itemDate->format('m');
118 7
            $this->frontMatter['day'] = $itemDate->format('d');
119 7
        }
120 7
    }
121
122
    //
123
    // Evaluation
124
    //
125
126
    /**
127
     * Evaluate an array as Front Matter.
128
     *
129
     * @param array $yaml
130
     */
131 44
    private function evaluateBlock(&$yaml)
132
    {
133 44
        ++$this->nestingLevel;
134
135 44
        foreach ($yaml as $key => &$value)
136
        {
137 43
            $this->yamlKeys[$this->nestingLevel] = $key;
138 43
            $keys = implode('.', $this->yamlKeys);
139
140 43
            if (in_array($key, self::$expandableFields, true))
141 43
            {
142 25
                $value = $this->evaluateExpandableField($keys, $value);
143 24
            }
144 36
            elseif (is_array($value))
145
            {
146 17
                $this->evaluateBlock($value);
147 17
            }
148 36
            elseif (is_string($value))
149
            {
150 35
                $value = $this->evaluateBasicType($keys, $value);
151 32
            }
152 14
            elseif ($value instanceof \DateTime)
153
            {
154 4
                $value = $this->castDateTimeTimezone($value->format('U'));
155 4
            }
156 42
        }
157
158 41
        --$this->nestingLevel;
159 41
        $this->yamlKeys = array();
160 41
    }
161
162
    /**
163
     * Evaluate an expandable field.
164
     *
165
     * @param string $key
166
     * @param string $fmStatement
167
     *
168
     * @return array
169
     */
170 25
    private function evaluateExpandableField($key, $fmStatement)
171
    {
172 25
        if (!is_array($fmStatement))
173 25
        {
174 24
            $fmStatement = array($fmStatement);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $fmStatement. This often makes code more readable.
Loading history...
175 24
        }
176
177 25
        $wip = array();
178
179 25
        foreach ($fmStatement as $statement)
180
        {
181 25
            $value = $this->evaluateBasicType($key, $statement, true);
182
183
            // Only continue expansion if there are Front Matter variables remain in the string, this means there'll be
184
            // Front Matter variables referencing arrays
185 25
            $expandingVars = $this->getFrontMatterVariables($value);
186 25
            if (!empty($expandingVars))
187 25
            {
188 6
                $value = $this->evaluateArrayType($key, $value, $expandingVars);
189 5
            }
190
191 24
            $wip[] = $value;
192 24
        }
193
194 24
        return $wip;
195
    }
196
197
    /**
198
     * Convert a string or an array into an array of ExpandedValue objects created through "value expansion".
199
     *
200
     * @param string $frontMatterKey     The current hierarchy of the Front Matter keys being used
201
     * @param string $expandableValue    The Front Matter value that will be expanded
202
     * @param array  $arrayVariableNames The Front Matter variable names that reference arrays
203
     *
204
     * @throws YamlUnsupportedVariableException If a multidimensional array is given for value expansion
205
     *
206
     * @return array
207
     */
208 6
    private function evaluateArrayType($frontMatterKey, $expandableValue, $arrayVariableNames)
209
    {
210 6
        if (!is_array($expandableValue))
211 6
        {
212 6
            $expandableValue = array($expandableValue);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $expandableValue. This often makes code more readable.
Loading history...
213 6
        }
214
215 6
        $this->expansionUsed = true;
216
217 6
        foreach ($arrayVariableNames as $variable)
218
        {
219 6
            if (ArrayUtilities::is_multidimensional($this->frontMatter[$variable]))
220 6
            {
221 1
                throw new YamlUnsupportedVariableException("Yaml array expansion is not supported with multidimensional arrays with `$variable` for key `$frontMatterKey`");
2 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $variable instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $frontMatterKey instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
222
            }
223
224 5
            $wip = array();
225
226 5
            foreach ($expandableValue as &$statement)
227
            {
228 5
                foreach ($this->frontMatter[$variable] as $value)
229
                {
230 5
                    $evaluatedValue = ($statement instanceof ExpandedValue) ? clone $statement : new ExpandedValue($statement);
231 5
                    $evaluatedValue->setEvaluated(str_replace('%' . $variable, $value, $evaluatedValue->getEvaluated()));
232 5
                    $evaluatedValue->setIterator($variable, $value);
233
234 5
                    $wip[] = $evaluatedValue;
235 5
                }
236 5
            }
237
238 5
            $expandableValue = $wip;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $expandableValue. This often makes code more readable.
Loading history...
239 5
        }
240
241 5
        return $expandableValue;
242
    }
243
244
    /**
245
     * Evaluate an string for FrontMatter variables and replace them with the corresponding values.
246
     *
247
     * @param string $key          The key of the Front Matter value
248
     * @param string $string       The string that will be evaluated
249
     * @param bool   $ignoreArrays When set to true, an exception won't be thrown when an array is found with the
250
     *                             interpolation
251
     *
252
     * @throws YamlUnsupportedVariableException A FrontMatter variable is not an int, float, or string
253
     *
254
     * @return string The final string with variables evaluated
255
     */
256 43
    private function evaluateBasicType($key, $string, $ignoreArrays = false)
257
    {
258 43
        $variables = $this->getFrontMatterVariables($string);
259
260 43
        foreach ($variables as $variable)
261
        {
262 26
            $value = $this->getVariableValue($key, $variable);
263
264 24
            if (is_array($value) || is_bool($value))
265 24
            {
266
                if ($ignoreArrays)
267 8
                {
268 6
                    continue;
269
                }
270
271 2
                throw new YamlUnsupportedVariableException("Yaml variable `$variable` for `$key` is not a supported data type.");
2 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $variable instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $key instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
272
            }
273
274 16
            $string = str_replace('%' . $variable, $value, $string);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $string. This often makes code more readable.
Loading history...
275 40
        }
276
277 40
        return $string;
278
    }
279
280
    //
281
    // Variable management
282
    //
283
284
    /**
285
     * Get an array of FrontMatter variables in the specified string that need to be interpolated.
286
     *
287
     * @param string $string
288
     *
289
     * @return string[]
290
     */
291 43
    private function getFrontMatterVariables($string)
292
    {
293 43
        $variables = array();
294
295 43
        preg_match_all(self::VARIABLE_DEF, $string, $variables);
296
297
        // Default behavior causes $variables[0] is the entire string that was matched. $variables[1] will be each
298
        // matching result individually.
299 43
        return $variables[1];
300
    }
301
302
    /**
303
     * Get the value of a FM variable or throw an exception.
304
     *
305
     * @param string $key
306
     * @param string $varName
307
     *
308
     * @throws YamlVariableUndefinedException
309
     *
310
     * @return mixed
311
     */
312 26
    private function getVariableValue($key, $varName)
313
    {
314 26
        if (!isset($this->frontMatter[$varName]))
315 26
        {
316 2
            throw new YamlVariableUndefinedException("Yaml variable `$varName` is not defined for: $key");
2 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $varName instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $key instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
317
        }
318
319 24
        return $this->frontMatter[$varName];
320
    }
321
322
    //
323
    // Utility functions
324
    //
325
326
    /**
327
     * @param string $epochTime
328
     *
329
     * @return bool|\DateTime
330
     */
331 6
    private function castDateTimeTimezone($epochTime)
332
    {
333 6
        $timezone = new \DateTimeZone(date_default_timezone_get());
334 6
        $value = \DateTime::createFromFormat('U', $epochTime);
335 6
        $value->setTimezone($timezone);
336
337 6
        return $value;
338
    }
339
340
    /**
341
     * @param $guess
342
     *
343
     * @return bool|\DateTime
344
     */
345 7
    private function guessDateTime($guess)
346
    {
347 7
        if ($guess instanceof \DateTime)
348 7
        {
349 1
            return $guess;
350
        }
351 6
        elseif (is_numeric($guess))
352
        {
353 2
            return $this->castDateTimeTimezone($guess);
354
        }
355
356
        try
357
        {
358 4
            return new \DateTime($guess);
359
        }
360
        catch (\Exception $e)
361
        {
362
            return false;
363
        }
364
    }
365
}
366