Completed
Push — master ( 2bc4b2...8df106 )
by Vladimir
02:25
created

FrontMatterObject::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 2
eloc 7
c 2
b 0
f 1
nc 2
nop 1
dl 0
loc 14
rs 9.4285
ccs 6
cts 6
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 24
    public function __construct ($filePath)
79
    {
80 24
        $this->filePath = $filePath;
81 24
        $this->fs       = new Filesystem();
82
83 24
        if (!$this->fs->exists($filePath))
84 24
        {
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 2
        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 24
    }
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
     * Read the file, and parse its contents
212
     */
213
    final public function refreshFileContent ()
214
    {
215 23
        $rawFileContents = file_get_contents($this->filePath);
216
217 23
        $frontMatter = array();
218 23
        preg_match('/---(.*?)---(.*)/s', $rawFileContents, $frontMatter);
219
220 23 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...
221 23
        {
222 1
            throw new IOException(sprintf("'%s' is not a valid ContentItem",
223 1
                    $this->fs->getFileName($this->filePath))
224 1
            );
225
        }
226
227 22 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...
228 22
        {
229 1
            throw new IOException(sprintf('A ContentItem (%s) must have a body to render',
230 1
                    $this->fs->getFileName($this->filePath))
231 1
            );
232
        }
233
234 21
        $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...
235 20
        $this->bodyContent = trim($frontMatter[2]);
236
237 20
        $this->frontMatterEvaluated = false;
238 20
        $this->bodyContentEvaluated = false;
239 20
        $this->permalinkEvaluated = false;
240
241 20
        $this->handleSpecialFrontMatter();
242 20
    }
243
244
    /**
245
     * Evaluate an array of data for FrontMatter variables. This function will modify the array in place.
246
     *
247
     * @param  array $yaml An array of data containing FrontMatter variables
248
     *
249
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
250
     */
251
    final protected function evaluateYaml (&$yaml)
252
    {
253 7
        foreach ($yaml as $key => $value)
254
        {
255 7
            if (is_array($yaml[$key]))
256 7
            {
257 1
                $this->evaluateYaml($yaml[$key]);
258 1
            }
259
            else
260
            {
261 7
                $yaml[$key] = $this->evaluateYamlVar($value, $this->frontMatter);
262
            }
263 6
        }
264 6
    }
265
266
    /**
267
     * Evaluate an string for FrontMatter variables and replace them with the corresponding values
268
     *
269
     * @param  string $string The string that will be evaluated
270
     * @param  array  $yaml   The existing front matter from which the variable values will be pulled from
271
     *
272
     * @return string The final string with variables evaluated
273
     *
274
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
275
     */
276
    private function evaluateYamlVar ($string, $yaml)
277
    {
278 7
        $variables = array();
279 7
        $varRegex  = '/(%[a-zA-Z]+)/';
280 7
        $output    = $string;
281
282 7
        preg_match_all($varRegex, $string, $variables);
283
284
        // Default behavior causes $variables[0] is the entire string that was matched. $variables[1] will be each
285
        // matching result individually.
286 7
        foreach ($variables[1] as $variable)
287
        {
288 6
            $yamlVar = substr($variable, 1); // Trim the '%' from the YAML variable name
289
290 6
            if (!array_key_exists($yamlVar, $yaml))
291 6
            {
292 1
                throw new YamlVariableUndefinedException("Yaml variable `$variable` is not defined");
293
            }
294
295 5
            $output = str_replace($variable, $yaml[$yamlVar], $output);
296 6
        }
297
298 6
        return $output;
299
    }
300
301
    /**
302
     * Handle special front matter values that need special treatment or have special meaning to a Content Item
303
     */
304
    private function handleSpecialFrontMatter ()
305
    {
306 20
        if (isset($this->frontMatter['date']))
307 20
        {
308
            try
309
            {
310
                // Coming from a string variable
311 3
                $itemDate = new \DateTime($this->frontMatter['date']);
312
            }
313 3
            catch (\Exception $e)
314
            {
315
                // YAML has parsed them to Epoch time
316 1
                $itemDate = \DateTime::createFromFormat('U', $this->frontMatter['date']);
317
            }
318
319 3
            if (!$itemDate === false)
320 3
            {
321 2
                $this->frontMatter['year']  = $itemDate->format('Y');
322 2
                $this->frontMatter['month'] = $itemDate->format('m');
323 2
                $this->frontMatter['day']   = $itemDate->format('d');
324 2
            }
325 3
        }
326 20
    }
327
328
    /**
329
     * Get the permalink based off the location of where the file is relative to the website. This permalink is to be
330
     * used as a fallback in the case that a permalink is not explicitly specified in the Front Matter.
331
     *
332
     * @return string
333
     */
334
    private function getPathPermalink ()
335
    {
336
        // Remove the protocol of the path, if there is one and prepend a '/' to the beginning
337 3
        $cleanPath = preg_replace('/[\w|\d]+:\/\//', '', $this->filePath);
338 3
        $cleanPath = ltrim($cleanPath, DIRECTORY_SEPARATOR);
339
340
        // Check the first folder and see if it's a data folder (starts with an underscore) intended for stakx
341 3
        $folders = explode('/', $cleanPath);
342
343 3
        if (substr($folders[0], 0, 1) === '_')
344 3
        {
345 1
            array_shift($folders);
346 1
        }
347
348 3
        $cleanPath = implode(DIRECTORY_SEPARATOR, $folders);
349
350 3
        return $cleanPath;
351
    }
352
353
    /**
354
     * Sanitize a permalink to remove unsupported characters or multiple '/' and replace spaces with hyphens
355
     *
356
     * @param  string $permalink A permalink
357
     *
358
     * @return string $permalink The sanitized permalink
359
     */
360
    private function sanitizePermalink ($permalink)
361
    {
362
        // Remove multiple '/' together
363 7
        $permalink = preg_replace('/\/+/', '/', $permalink);
364
365
        // Replace all spaces with hyphens
366 7
        $permalink = str_replace(' ', '-', $permalink);
367
368
        // Remove all disallowed characters
369 7
        $permalink = preg_replace('/[^0-9a-zA-Z-_\/\.]/', '', $permalink);
370
371
        // Handle unnecessary extensions
372 7
        $extensionsToStrip = array('twig');
373
374 7
        if (in_array($this->fs->getExtension($permalink), $extensionsToStrip))
375 7
        {
376 3
            $permalink = $this->fs->removeExtension($permalink);
377 3
        }
378
379
        // Remove a special './' combination from the beginning of a path
380 7
        if (substr($permalink, 0, 2) === './')
381 7
        {
382 1
            $permalink = substr($permalink, 2);
383 1
        }
384
385 7
        return $permalink;
386
    }
387
}