Completed
Pull Request — master (#11)
by Vladimir
02:26
created

FrontMatterObject::getTargetFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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