Completed
Push — master ( 6af8c9...1b8281 )
by Vladimir
02:30
created

FrontMatterObject::__get()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 4
rs 10
ccs 1
cts 1
cp 1
crap 2
1
<?php
2
3
namespace allejo\stakx\Object;
4
5
use allejo\stakx\System\Filesystem;
6
use allejo\stakx\Exception\YamlVariableUndefinedException;
7
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
8
use Symfony\Component\Filesystem\Exception\IOException;
9
use Symfony\Component\Yaml\Yaml;
10
11
abstract class FrontMatterObject
12
{
13
    /**
14
     * Set to true if the permalink has been sanitized
15
     *
16
     * @var bool
17
     */
18
    protected $permalinkEvaluated;
19
20
    /**
21
     * Set to true if the front matter has already been evaluated with variable interpolation
22
     *
23
     * @var bool
24
     */
25
    protected $frontMatterEvaluated;
26
27
    /**
28
     * An array containing the Yaml of the file
29
     *
30
     * @var array
31
     */
32
    protected $frontMatter;
33
34
    /**
35
     * Set to true if the body has already been parsed as markdown or any other format
36
     *
37
     * @var bool
38
     */
39
    protected $bodyContentEvaluated;
40
41
    /**
42
     * Only the body of the file, i.e. the content
43
     *
44
     * @var string
45
     */
46
    protected $bodyContent;
47
48
    /**
49
     * The extension of the file
50
     *
51
     * @var string
52
     */
53
    protected $extension;
54
55
    /**
56
     * The original file path to the ContentItem
57
     *
58
     * @var string
59
     */
60
    protected $filePath;
61
62
    /**
63
     * A filesystem object
64
     *
65
     * @var Filesystem
66
     */
67
    protected $fs;
68
69
    /**
70
     * ContentItem constructor.
71
     *
72
     * @param string $filePath The path to the file that will be parsed into a ContentItem
73
     *
74
     * @throws FileNotFoundException The given file path does not exist
75
     * @throws IOException           The file was not a valid ContentItem. This would meam there was no front matter or
76
     *                               no body
77
     */
78 30
    public function __construct ($filePath)
79
    {
80 30
        $this->filePath = $filePath;
81 30
        $this->fs       = new Filesystem();
82
83 30
        if (!$this->fs->exists($filePath))
84 30
        {
85 1
            throw new FileNotFoundException("The following file could not be found: ${filePath}");
86
        }
87
88
        $this->extension = strtolower($this->fs->getExtension($filePath));
89
90
        $this->refreshFileContent();
91
    }
92
93
    /**
94
     * The magic getter returns values from the front matter in order to make these values accessible to Twig templates
95
     * in a simple fashion
96
     *
97
     * @param  string $name The key in the front matter
98
     *
99
     * @return mixed|null
100
     */
101
    public function __get ($name)
102
    {
103 3
        return (array_key_exists($name, $this->frontMatter) ? $this->frontMatter[$name] : null);
104
    }
105
106
    /**
107
     * The magic getter returns true if the value exists in the Front Matter. This is used in conjunction with the __get
108
     * function
109
     *
110
     * @param  string $name The name of the Front Matter value being looked for
111
     *
112
     * @return bool
113
     */
114
    public function __isset ($name)
115
    {
116 1
        return array_key_exists($name, $this->frontMatter);
117 30
    }
118
119
    /**
120
     * Return the body of the Content Item
121
     *
122
     * @return string
123
     */
124
    abstract public function getContent ();
125
126
    /**
127
     * @param array|null $variables An array of YAML variables to use in evaluating the `$permalink` value
128
     */
129
    final public function evaluateFrontMatter ($variables = null)
130
    {
131 2
        if (!is_null($variables))
132 2
        {
133 2
            $this->frontMatter = array_merge($this->frontMatter, $variables);
134 2
            $this->handleSpecialFrontMatter();
135 2
            $this->evaluateYaml($this->frontMatter);
136 2
        }
137 2
    }
138
139
    /**
140
     * Get the Front Matter of a ContentItem as an array
141
     *
142
     * @param  bool $evaluateYaml When set to true, the YAML will be evaluated for variables
143
     *
144
     * @return array
145
     */
146
    final public function getFrontMatter ($evaluateYaml = true)
147
    {
148 6
        if ($this->frontMatter === null)
149 6
        {
150 1
            $this->frontMatter = array();
151 1
        }
152 5
        else if (!$this->frontMatterEvaluated && $evaluateYaml && !empty($evaluateYaml))
153 5
        {
154 5
            $this->evaluateYaml($this->frontMatter);
155 4
            $this->frontMatterEvaluated = true;
156 4
        }
157
158 5
        return $this->frontMatter;
159
    }
160
161
    /**
162
     * Get the permalink of this Content Item
163
     *
164
     * @return string
165
     */
166
    final public function getPermalink ()
167
    {
168 7
        if ($this->permalinkEvaluated)
169 7
        {
170 5
            return $this->frontMatter['permalink'];
171
        }
172
173 7
        $permalink = (is_array($this->frontMatter) && array_key_exists('permalink', $this->frontMatter)) ?
174 7
            $this->frontMatter['permalink'] : $this->getPathPermalink();
175
176 7
        $this->frontMatter['permalink'] = $this->sanitizePermalink($permalink);
177 7
        $this->permalinkEvaluated = true;
178
179 7
        return $this->frontMatter['permalink'];
180
    }
181
182
    /**
183
     * Get the destination of where this Content Item would be written to when the website is compiled
184
     *
185
     * @return string
186
     */
187
    final public function getTargetFile ()
188
    {
189 5
        $extension  = $this->fs->getExtension($this->getPermalink());
190 5
        $targetFile = $this->getPermalink();
191
192 5
        if (empty($extension))
193 5
        {
194 1
            $targetFile = rtrim($this->getPermalink(), '/') . '/index.html';
195 1
        }
196
197 5
        return ltrim($targetFile, '/');
198
    }
199
200
    /**
201
     * Get the original file path
202
     *
203
     * @return string
204
     */
205
    final public function getFilePath ()
206
    {
207 1
        return $this->filePath;
208
    }
209
210
    /**
211
     * Get the relative path to this file relative to the root of the Stakx website
212
     *
213
     * @return string
214
     */
215
    final public function getRelativeFilePath ()
216
    {
217
        return $this->fs->getRelativePath($this->filePath);
218
    }
219
220
    /**
221
     * Read the file, and parse its contents
222
     */
223
    final public function refreshFileContent ()
224
    {
225 29
        $rawFileContents = file_get_contents($this->filePath);
226
227 29
        $frontMatter = array();
228 29
        preg_match('/---(.*?)---(.*)/s', $rawFileContents, $frontMatter);
229
230 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...
231 29
        {
232 1
            throw new IOException(sprintf("'%s' is not a valid ContentItem",
233 1
                    $this->fs->getFileName($this->filePath))
234 1
            );
235
        }
236
237 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...
238 28
        {
239 1
            throw new IOException(sprintf('A ContentItem (%s) must have a body to render',
240 1
                    $this->fs->getFileName($this->filePath))
241 1
            );
242
        }
243
244 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...
245 26
        $this->bodyContent = trim($frontMatter[2]);
246
247 26
        $this->frontMatterEvaluated = false;
248 26
        $this->bodyContentEvaluated = false;
249 26
        $this->permalinkEvaluated = false;
250
251 26
        $this->handleSpecialFrontMatter();
252 26
    }
253
254
    /**
255
     * Evaluate an array of data for FrontMatter variables. This function will modify the array in place.
256
     *
257
     * @param  array $yaml An array of data containing FrontMatter variables
258
     *
259
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
260
     */
261
    final protected function evaluateYaml (&$yaml)
262
    {
263 7
        foreach ($yaml as $key => $value)
264
        {
265 7
            if (is_array($yaml[$key]))
266 7
            {
267 1
                $this->evaluateYaml($yaml[$key]);
268 1
            }
269
            else
270
            {
271 7
                $yaml[$key] = $this->evaluateYamlVar($value, $this->frontMatter);
272
            }
273 6
        }
274 6
    }
275
276
    /**
277
     * Evaluate an string for FrontMatter variables and replace them with the corresponding values
278
     *
279
     * @param  string $string The string that will be evaluated
280
     * @param  array  $yaml   The existing front matter from which the variable values will be pulled from
281
     *
282
     * @return string The final string with variables evaluated
283
     *
284
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
285
     */
286
    private function evaluateYamlVar ($string, $yaml)
287
    {
288 7
        $variables = array();
289 7
        $varRegex  = '/(%[a-zA-Z]+)/';
290 7
        $output    = $string;
291
292 7
        preg_match_all($varRegex, $string, $variables);
293
294
        // Default behavior causes $variables[0] is the entire string that was matched. $variables[1] will be each
295
        // matching result individually.
296 7
        foreach ($variables[1] as $variable)
297
        {
298 6
            $yamlVar = substr($variable, 1); // Trim the '%' from the YAML variable name
299
300 6
            if (!array_key_exists($yamlVar, $yaml))
301 6
            {
302 1
                throw new YamlVariableUndefinedException("Yaml variable `$variable` is not defined");
303
            }
304
305 5
            $output = str_replace($variable, $yaml[$yamlVar], $output);
306 6
        }
307
308 6
        return $output;
309
    }
310
311
    /**
312
     * Handle special front matter values that need special treatment or have special meaning to a Content Item
313
     */
314
    private function handleSpecialFrontMatter ()
315
    {
316 26
        if (isset($this->frontMatter['date']))
317 26
        {
318
            try
319
            {
320
                // Coming from a string variable
321 3
                $itemDate = new \DateTime($this->frontMatter['date']);
322
            }
323 3
            catch (\Exception $e)
324
            {
325
                // YAML has parsed them to Epoch time
326 1
                $itemDate = \DateTime::createFromFormat('U', $this->frontMatter['date']);
327
            }
328
329 3
            if (!$itemDate === false)
330 3
            {
331 2
                $this->frontMatter['year']  = $itemDate->format('Y');
332 2
                $this->frontMatter['month'] = $itemDate->format('m');
333 2
                $this->frontMatter['day']   = $itemDate->format('d');
334 2
            }
335 3
        }
336 26
    }
337
338
    /**
339
     * Get the permalink based off the location of where the file is relative to the website. This permalink is to be
340
     * used as a fallback in the case that a permalink is not explicitly specified in the Front Matter.
341
     *
342
     * @return string
343
     */
344
    private function getPathPermalink ()
345
    {
346
        // Remove the protocol of the path, if there is one and prepend a '/' to the beginning
347 3
        $cleanPath = preg_replace('/[\w|\d]+:\/\//', '', $this->filePath);
348 3
        $cleanPath = ltrim($cleanPath, DIRECTORY_SEPARATOR);
349
350
        // Check the first folder and see if it's a data folder (starts with an underscore) intended for stakx
351 3
        $folders = explode('/', $cleanPath);
352
353 3
        if (substr($folders[0], 0, 1) === '_')
354 3
        {
355 1
            array_shift($folders);
356 1
        }
357
358 3
        $cleanPath = implode(DIRECTORY_SEPARATOR, $folders);
359
360 3
        return $cleanPath;
361
    }
362
363
    /**
364
     * Sanitize a permalink to remove unsupported characters or multiple '/' and replace spaces with hyphens
365
     *
366
     * @param  string $permalink A permalink
367
     *
368
     * @return string $permalink The sanitized permalink
369
     */
370
    private function sanitizePermalink ($permalink)
371
    {
372
        // Remove multiple '/' together
373 7
        $permalink = preg_replace('/\/+/', '/', $permalink);
374
375
        // Replace all spaces with hyphens
376 7
        $permalink = str_replace(' ', '-', $permalink);
377
378
        // Remove all disallowed characters
379 7
        $permalink = preg_replace('/[^0-9a-zA-Z-_\/\.]/', '', $permalink);
380
381
        // Handle unnecessary extensions
382 7
        $extensionsToStrip = array('twig');
383
384 7
        if (in_array($this->fs->getExtension($permalink), $extensionsToStrip))
385 7
        {
386 3
            $permalink = $this->fs->removeExtension($permalink);
387 3
        }
388
389
        // Remove a special './' combination from the beginning of a path
390 7
        if (substr($permalink, 0, 2) === './')
391 7
        {
392 1
            $permalink = substr($permalink, 2);
393 1
        }
394
395
        // Convert permalinks to lower case
396 7
        $permalink = mb_strtolower($permalink, 'UTF-8');
397
398 7
        return $permalink;
399
    }
400
}