Completed
Push — master ( 551c23...b14e2a )
by Vladimir
03:04
created

Document::refreshFileContent()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 52
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 52
ccs 28
cts 28
cp 1
rs 7.2396
c 0
b 0
f 0
cc 7
eloc 23
nc 6
nop 0
crap 7

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @copyright 2017 Vladimir Jimenez
5
 * @license   https://github.com/allejo/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx\FrontMatter;
9
10
use allejo\stakx\Document\JailedDocumentInterface;
11
use allejo\stakx\Exception\FileAwareException;
12
use allejo\stakx\Exception\InvalidSyntaxException;
13
use allejo\stakx\FrontMatter\Exception\YamlVariableUndefinedException;
14
use allejo\stakx\System\Filesystem;
15
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
16
use Symfony\Component\Filesystem\Exception\IOException;
17
use Symfony\Component\Finder\SplFileInfo;
18
use Symfony\Component\Yaml\Exception\ParseException;
19
use Symfony\Component\Yaml\Yaml;
20
21
abstract class Document implements WritableDocumentInterface, JailedDocumentInterface, \ArrayAccess
0 ignored issues
show
Coding Style introduced by
Document does not seem to conform to the naming convention (^Abstract|Factory$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
22
{
23
    const TEMPLATE = "---\n%s\n---\n\n%s";
24
25
    protected static $whiteListFunctions = array(
26
        'getPermalink', 'getRedirects', 'getTargetFile', 'getName', 'getFilePath', 'getRelativeFilePath', 'getContent',
27
        'getExtension', 'getFrontMatter'
28
    );
29
30
    /**
31
     * An array to keep track of collection or data dependencies used inside of a Twig template.
32
     *
33
     * $dataDependencies['collections'] = array()
34
     * $dataDependencies['data'] = array()
35
     *
36
     * @var array
37
     */
38
    protected $dataDependencies;
39
40
    /**
41
     * FrontMatter values that can be injected or set after the file has been parsed. Values in this array will take
42
     * precedence over values in $frontMatter.
43
     *
44
     * @var array
45
     */
46
    protected $writableFrontMatter;
47
48
    /**
49
     * A list of Front Matter values that should not be returned directly from the $frontMatter array. Values listed
50
     * here have dedicated functions that handle those Front Matter values and the respective functions should be called
51
     * instead.
52
     *
53
     * @var string[]
54
     */
55
    protected $frontMatterBlacklist;
56
57
    /**
58
     * Set to true if the front matter has already been evaluated with variable interpolation.
59
     *
60
     * @var bool
61
     */
62
    protected $frontMatterEvaluated;
63
64
    /**
65
     * @var Parser
66
     */
67
    protected $frontMatterParser;
68
69
    /**
70
     * An array containing the Yaml of the file.
71
     *
72
     * @var array
73
     */
74
    protected $frontMatter;
75
76
    /**
77
     * Set to true if the body has already been parsed as markdown or any other format.
78
     *
79
     * @var bool
80
     */
81
    protected $bodyContentEvaluated;
82
83
    /**
84
     * Only the body of the file, i.e. the content.
85
     *
86
     * @var string
87
     */
88
    protected $bodyContent;
89
90
    /**
91
     * The permalink for this object.
92
     *
93
     * @var string
94
     */
95
    protected $permalink;
96
97
    /**
98
     * A filesystem object.
99
     *
100
     * @var Filesystem
101
     */
102
    protected $fs;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fs. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
103
104
    /**
105
     * The extension of the file.
106
     *
107
     * @var string
108
     */
109
    private $extension;
110
111
    /**
112
     * The number of lines that Twig template errors should offset.
113
     *
114
     * @var int
115
     */
116
    private $lineOffset;
117
118
    /**
119
     * A list URLs that will redirect to this object.
120
     *
121
     * @var string[]
122
     */
123
    private $redirects;
124
125
    /**
126
     * The original file path to the ContentItem.
127
     *
128
     * @var SplFileInfo
129
     */
130
    private $filePath;
131
132
    /**
133
     * ContentItem constructor.
134
     *
135
     * @param string $filePath The path to the file that will be parsed into a ContentItem
136
     *
137
     * @throws FileNotFoundException The given file path does not exist
138
     * @throws IOException           The file was not a valid ContentItem. This would meam there was no front matter or
139
     *                               no body
140
     */
141 116
    public function __construct($filePath)
142
    {
143 116
        $this->frontMatterBlacklist = array('permalink', 'redirects');
144 116
        $this->writableFrontMatter = array();
145
146 116
        $this->filePath = $filePath;
0 ignored issues
show
Documentation Bug introduced by
It seems like $filePath of type string is incompatible with the declared type object<Symfony\Component\Finder\SplFileInfo> of property $filePath.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
147 116
        $this->fs = new Filesystem();
148
149 116
        if (!$this->fs->exists($filePath))
150 116
        {
151 1
            throw new FileNotFoundException("The following file could not be found: ${filePath}");
152
        }
153
154
        $this->extension = strtolower($this->fs->getExtension($filePath));
155
156
        $this->refreshFileContent();
157
    }
158
159
    /**
160
     * Return the body of the Content Item.
161
     *
162
     * @return string
163
     */
164
    abstract public function getContent();
165
166
    /**
167
     * Get the extension of the current file.
168
     *
169
     * @return string
170
     */
171
    final public function getExtension()
172
    {
173 7
        return $this->extension;
174
    }
175
176
    /**
177
     * Get the original file path.
178
     *
179
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be SplFileInfo?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
180
     */
181
    final public function getFilePath()
182
    {
183 19
        return $this->filePath;
184
    }
185
186
    /**
187
     * The number of lines that are taken up by FrontMatter and white space.
188
     *
189
     * @return int
190
     */
191
    final public function getLineOffset()
192
    {
193
        return $this->lineOffset;
194
    }
195
196
    /**
197
     * Get the name of the item, which is just the file name without the extension.
198
     *
199
     * @return string
200
     */
201
    final public function getName()
202
    {
203 37
        return $this->fs->getBaseName($this->filePath);
204
    }
205
206
    /**
207
     * Get the relative path to this file relative to the root of the Stakx website.
208
     *
209
     * @return string
210
     */
211
    final public function getRelativeFilePath()
212
    {
213 65
        if ($this->filePath instanceof SplFileInfo)
214 65
        {
215 39
            return $this->filePath->getRelativePathname();
216
        }
217
218
        // TODO ensure that we always get SplFileInfo objects, even when handling VFS documents
219 28
        return $this->fs->getRelativePath($this->filePath);
220
    }
221
222
    /**
223
     * Get the destination of where this Content Item would be written to when the website is compiled.
224
     *
225
     * @return string
226
     */
227
    final public function getTargetFile()
228
    {
229 20
        $permalink = $this->getPermalink();
230 20
        $missingFile = (substr($permalink, -1) == '/');
231 20
        $permalink = str_replace('/', DIRECTORY_SEPARATOR, $permalink);
232
233
        if ($missingFile)
234 20
        {
235 12
            $permalink = rtrim($permalink, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'index.html';
236 12
        }
237
238 20
        return ltrim($permalink, DIRECTORY_SEPARATOR);
239
    }
240
241
    /**
242
     * Check whether this object has a reference to a collection or data item.
243
     *
244
     * @param string $namespace 'collections' or 'data'
245
     * @param string $needle
246
     *
247
     * @return bool
248
     */
249
    final public function hasTwigDependency($namespace, $needle)
250
    {
251
        return in_array($needle, $this->dataDependencies[$namespace]);
252
    }
253
254
    /**
255
     * Read the file, and parse its contents.
256
     */
257
    final public function refreshFileContent()
258
    {
259
        // This function can be called after the initial object was created and the file may have been deleted since the
260
        // creation of the object.
261 115
        if (!$this->fs->exists($this->filePath))
262 115
        {
263 1
            throw new FileNotFoundException(null, 0, null, $this->filePath);
264
        }
265
266
        // $fileStructure[1] is the YAML
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
267
        // $fileStructure[2] is the amount of new lines after the closing `---` and the beginning of content
268
        // $fileStructure[3] is the body of the document
269 115
        $fileStructure = array();
270
271 115
        $rawFileContents = file_get_contents($this->filePath);
272 115
        preg_match('/---\R(.*?\R)?---(\s+)(.*)/s', $rawFileContents, $fileStructure);
273
274 115
        if (count($fileStructure) != 4)
275 115
        {
276 9
            throw new InvalidSyntaxException('Invalid FrontMatter file', 0, null, $this->getRelativeFilePath());
277
        }
278
279 106
        if (empty(trim($fileStructure[3])))
280 106
        {
281 1
            throw new InvalidSyntaxException('FrontMatter files must have a body to render', 0, null, $this->getRelativeFilePath());
282
        }
283
284
        // The hard coded 1 is the offset used to count the new line used after the first `---` that is not caught in the regex
285 105
        $this->lineOffset = substr_count($fileStructure[1], "\n") + substr_count($fileStructure[2], "\n") + 1;
286 105
        $this->bodyContent = $fileStructure[3];
287
288 105
        if (!empty(trim($fileStructure[1])))
289 105
        {
290 89
            $this->frontMatter = Yaml::parse($fileStructure[1], Yaml::PARSE_DATETIME);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Symfony\Component\Yaml\...l\Yaml::PARSE_DATETIME) 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...
291
292 89
            if (!empty($this->frontMatter) && !is_array($this->frontMatter))
293 89
            {
294 1
                throw new ParseException('The evaluated FrontMatter should be an array');
295
            }
296 88
        }
297
        else
298
        {
299 19
            $this->frontMatter = array();
300
        }
301
302 104
        $this->frontMatterEvaluated = false;
303 104
        $this->bodyContentEvaluated = false;
304 104
        $this->permalink = null;
305
306 104
        $this->findTwigDataDependencies('collections');
307 104
        $this->findTwigDataDependencies('data');
308 104
    }
309
310
    /**
311
     * Get all of the references to either DataItems or ContentItems inside of given string.
312
     *
313
     * @param string $filter 'collections' or 'data'
314
     */
315
    private function findTwigDataDependencies($filter)
316
    {
317 104
        $regex = '/{[{%](?:.+)?(?:' . $filter . ')(?:\.|\[\')(\w+)(?:\'\])?.+[%}]}/';
318 104
        $results = array();
319
320 104
        preg_match_all($regex, $this->bodyContent, $results);
321
322 104
        $this->dataDependencies[$filter] = array_unique($results[1]);
323 104
    }
324
325
    //
326
    // Permalink and redirect functionality
327
    //
328
329
    /**
330
     * Get the permalink of this Content Item.
331
     *
332
     * @throws \Exception
333
     *
334
     * @return string
335
     */
336
    final public function getPermalink()
337
    {
338 39
        if (!is_null($this->permalink))
339 39
        {
340 8
            return $this->permalink;
341
        }
342
343 37
        if (!is_null($this->frontMatterParser) && $this->frontMatterParser->hasExpansion())
344 37
        {
345
            throw new \Exception('The permalink for this item has not been set');
346
        }
347
348 37
        $permalink = (is_array($this->frontMatter) && isset($this->frontMatter['permalink'])) ?
349 37
            $this->frontMatter['permalink'] : $this->getPathPermalink();
350
351 37
        if (is_array($permalink))
352 37
        {
353 19
            $this->permalink = $permalink[0];
354 19
            array_shift($permalink);
355 19
            $this->redirects = $permalink;
356 19
        }
357
        else
358
        {
359 24
            $this->permalink = $permalink;
360 24
            $this->redirects = array();
361
        }
362
363 37
        $this->permalink = $this->sanitizePermalink($this->permalink);
364 37
        $this->permalink = str_replace(DIRECTORY_SEPARATOR, '/', $this->permalink);
365 37
        $this->permalink = '/' . ltrim($this->permalink, '/'); // Permalinks should always use '/' and not be OS specific
366
367 37
        return $this->permalink;
368
    }
369
370
    /**
371
     * Get an array of URLs that will redirect to.
372
     *
373
     * @return string[]
0 ignored issues
show
Documentation introduced by
Should the return type not be null|string[]?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
374
     */
375
    final public function getRedirects()
376
    {
377 14
        if (is_null($this->redirects))
378 14
        {
379 3
            $this->getPermalink();
380 3
        }
381
382 14
        return $this->redirects;
383
    }
384
385
    /**
386
     * Get the permalink based off the location of where the file is relative to the website. This permalink is to be
387
     * used as a fallback in the case that a permalink is not explicitly specified in the Front Matter.
388
     *
389
     * @return string
390
     */
391
    private function getPathPermalink()
392
    {
393
        // Remove the protocol of the path, if there is one and prepend a '/' to the beginning
394 3
        $cleanPath = preg_replace('/[\w|\d]+:\/\//', '', $this->getRelativeFilePath());
395 3
        $cleanPath = ltrim($cleanPath, DIRECTORY_SEPARATOR);
396
397
        // Handle vfs:// paths by replacing their forward slashes with the OS appropriate directory separator
398 3
        if (DIRECTORY_SEPARATOR !== '/')
399 3
        {
400
            $cleanPath = str_replace('/', DIRECTORY_SEPARATOR, $cleanPath);
401
        }
402
403
        // Check the first folder and see if it's a data folder (starts with an underscore) intended for stakx
404 3
        $folders = explode(DIRECTORY_SEPARATOR, $cleanPath);
405
406 3
        if (substr($folders[0], 0, 1) === '_')
407 3
        {
408 1
            array_shift($folders);
409 1
        }
410
411 3
        $cleanPath = implode(DIRECTORY_SEPARATOR, $folders);
412
413 3
        return $cleanPath;
414
    }
415
416
    /**
417
     * Sanitize a permalink to remove unsupported characters or multiple '/' and replace spaces with hyphens.
418
     *
419
     * @param string $permalink A permalink
420
     *
421
     * @return string $permalink The sanitized permalink
422
     */
423
    private function sanitizePermalink($permalink)
424
    {
425
        // Remove multiple '/' together
426 37
        $permalink = preg_replace('/\/+/', '/', $permalink);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $permalink. This often makes code more readable.
Loading history...
427
428
        // Replace all spaces with hyphens
429 37
        $permalink = str_replace(' ', '-', $permalink);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $permalink. This often makes code more readable.
Loading history...
430
431
        // Remove all disallowed characters
432 37
        $permalink = preg_replace('/[^0-9a-zA-Z-_\/\\\.]/', '', $permalink);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $permalink. This often makes code more readable.
Loading history...
433
434
        // Handle unnecessary extensions
435 37
        $extensionsToStrip = array('twig');
436
437 37
        if (in_array($this->fs->getExtension($permalink), $extensionsToStrip))
438 37
        {
439 4
            $permalink = $this->fs->removeExtension($permalink);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $permalink. This often makes code more readable.
Loading history...
440 4
        }
441
442
        // Remove any special characters before a sane value
443 37
        $permalink = preg_replace('/^[^0-9a-zA-Z-_]*/', '', $permalink);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $permalink. This often makes code more readable.
Loading history...
444
445
        // Convert permalinks to lower case
446 37
        $permalink = mb_strtolower($permalink, 'UTF-8');
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $permalink. This often makes code more readable.
Loading history...
447
448 37
        return $permalink;
449
    }
450
451
    //
452
    // WritableFrontMatter Implementation
453
    //
454
455
    /**
456
     * {@inheritdoc}
457
     */
458
    final public function evaluateFrontMatter($variables = null)
459
    {
460 7
        if (!is_null($variables))
461 7
        {
462 7
            $this->frontMatter = array_merge($this->frontMatter, $variables);
463 7
            $this->evaluateYaml($this->frontMatter);
464 7
        }
465 7
    }
466
467
    /**
468
     * {@inheritdoc}
469
     */
470
    final public function getFrontMatter($evaluateYaml = true)
471
    {
472 29
        if (is_null($this->frontMatter))
473 29
        {
474
            $this->frontMatter = array();
475
        }
476 29
        elseif (!$this->frontMatterEvaluated && $evaluateYaml)
477
        {
478 23
            $this->evaluateYaml($this->frontMatter);
479 22
        }
480
481 28
        return $this->frontMatter;
482
    }
483
484
    /**
485
     * {@inheritdoc}
486
     */
487
    final public function hasExpandedFrontMatter()
488
    {
489 2
        return !is_null($this->frontMatterParser) && $this->frontMatterParser->hasExpansion();
490
    }
491
492
    /**
493
     * {@inheritdoc.
494
     */
495
    final public function appendFrontMatter(array $frontMatter)
496
    {
497
        foreach ($frontMatter as $key => $value)
498
        {
499
            $this->writableFrontMatter[$key] = $value;
500
        }
501
    }
502
503
    /**
504
     * {@inheritdoc.
505
     */
506
    final public function deleteFrontMatter($key)
507
    {
508
        if (!isset($this->writableFrontMatter[$key]))
509
        {
510
            return;
511
        }
512
513
        unset($this->writableFrontMatter[$key]);
514
    }
515
516
    /**
517
     * {@inheritdoc.
518
     */
519
    final public function setFrontMatter(array $frontMatter)
520
    {
521 2
        if (!is_array($frontMatter))
522 2
        {
523
            throw new \InvalidArgumentException('An array is required for setting the writable FrontMatter');
524
        }
525
526 2
        $this->writableFrontMatter = $frontMatter;
527 2
    }
528
529
    /**
530
     * Evaluate an array of data for FrontMatter variables. This function will modify the array in place.
531
     *
532
     * @param array $yaml An array of data containing FrontMatter variables
533
     *
534
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
535
     */
536
    private function evaluateYaml(&$yaml)
537
    {
538
        try
539
        {
540 30
            $this->frontMatterParser = new Parser($yaml);
541 29
            $this->frontMatterEvaluated = true;
542
        }
543 30
        catch (\Exception $e)
544
        {
545 1
            throw FileAwareException::castException($e, $this->getRelativeFilePath());
546
        }
547 29
    }
548
549
    //
550
    // ArrayAccess Implementation
551
    //
552
553
    /**
554
     * {@inheritdoc}
555
     */
556
    public function offsetSet($offset, $value)
557
    {
558
        if (is_null($offset))
559
        {
560
            throw new \InvalidArgumentException('$offset cannot be null');
561
        }
562
563
        $this->writableFrontMatter[$offset] = $value;
564
    }
565
566
    /**
567
     * {@inheritdoc}
568
     */
569
    public function offsetExists($offset)
570
    {
571 31
        if (isset($this->writableFrontMatter[$offset]) || isset($this->frontMatter[$offset]))
572 31
        {
573 30
            return true;
574
        }
575
576 14
        $fxnCall = 'get' . ucfirst($offset);
577
578 14
        return method_exists($this, $fxnCall) && in_array($fxnCall, static::$whiteListFunctions);
579
    }
580
581
    /**
582
     * {@inheritdoc}
583
     */
584
    public function offsetUnset($offset)
585
    {
586
        unset($this->writableFrontMatter[$offset]);
587
    }
588
589
    /**
590
     * {@inheritdoc}
591
     */
592
    public function offsetGet($offset)
593
    {
594 48
        $fxnCall = 'get' . ucfirst($offset);
595
596 48
        if (in_array($fxnCall, self::$whiteListFunctions) && method_exists($this, $fxnCall))
597 48
        {
598 6
            return call_user_func_array(array($this, $fxnCall), array());
599
        }
600
601 42
        if (isset($this->writableFrontMatter[$offset]))
602 42
        {
603
            return $this->writableFrontMatter[$offset];
604
        }
605
606 42
        if (isset($this->frontMatter[$offset]))
607 42
        {
608 41
            return $this->frontMatter[$offset];
609
        }
610
611 5
        return null;
612
    }
613
}
614