Completed
Push — master ( 505196...f680a5 )
by Vladimir
10s
created

FrontMatterObject::getPermalink()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

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