Completed
Push — master ( 599098...2eed8d )
by Vladimir
03:27
created

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