Passed
Push — analysis-bQLb94 ( b3de70 )
by Arnaud
05:32 queued 11s
created

Extension   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 542
Duplicated Lines 0 %

Importance

Changes 11
Bugs 0 Features 0
Metric Value
eloc 196
c 11
b 0
f 0
dl 0
loc 542
rs 5.5199
wmc 56

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 3 1
A __construct() 0 10 2
A getEnv() 0 3 2
A readtime() 0 9 2
A excerpt() 0 10 2
A excerptHtml() 0 10 2
B getFilters() 0 73 1
A minify() 0 7 2
A toCss() 0 7 2
A sortByDate() 0 20 5
A resize() 0 3 1
B filterBy() 0 18 7
A minifyJs() 0 11 2
A getFunctions() 0 26 1
A asset() 0 3 1
A integrity() 0 7 2
A sortByWeight() 0 20 5
A minifyCss() 0 11 2
A url() 0 3 1
A filterBySection() 0 3 1
A sortByTitle() 0 6 1
A scssToCss() 0 17 3
A inline() 0 7 2
A html() 0 26 6

How to fix   Complexity   

Complex Class

Complex classes like Extension often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Extension, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the Cecil/Cecil package.
4
 *
5
 * Copyright (c) Arnaud Ligny <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Cecil\Renderer\Twig;
12
13
use Cecil\Assets\Asset;
14
use Cecil\Assets\Cache;
15
use Cecil\Assets\Image;
16
use Cecil\Assets\Url;
17
use Cecil\Builder;
18
use Cecil\Collection\CollectionInterface;
19
use Cecil\Collection\Page\Collection as PagesCollection;
20
use Cecil\Collection\Page\Page;
21
use Cecil\Config;
22
use Cecil\Exception\Exception;
23
use Cocur\Slugify\Bridge\Twig\SlugifyExtension;
24
use Cocur\Slugify\Slugify;
25
use MatthiasMullie\Minify;
26
use ScssPhp\ScssPhp\Compiler;
27
28
/**
29
 * Class Twig\Extension.
30
 */
31
class Extension extends SlugifyExtension
32
{
33
    /** @var Builder */
34
    protected $builder;
35
    /** @var Config */
36
    protected $config;
37
    /** @var Slugify */
38
    private static $slugifier;
39
40
    /**
41
     * @param Builder $builder
42
     */
43
    public function __construct(Builder $builder)
44
    {
45
        if (!self::$slugifier instanceof Slugify) {
46
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
47
        }
48
49
        parent::__construct(self::$slugifier);
50
51
        $this->builder = $builder;
52
        $this->config = $this->builder->getConfig();
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58
    public function getName()
59
    {
60
        return 'cecil';
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public function getFilters()
67
    {
68
        return [
69
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
70
            // sort
71
            new \Twig\TwigFilter('sort_by_title', [$this, 'sortByTitle']),
72
            new \Twig\TwigFilter('sort_by_weight', [$this, 'sortByWeight']),
73
            new \Twig\TwigFilter('sort_by_date', [$this, 'sortByDate']),
74
            // assets
75
            new \Twig\TwigFilter('url', [$this, 'url']),
76
            new \Twig\TwigFilter('html', [$this, 'html']),
77
            new \Twig\TwigFilter('inline', [$this, 'inline']),
78
            new \Twig\TwigFilter('to_css', [$this, 'toCss']),
79
            new \Twig\TwigFilter('minify', [$this, 'minify']),
80
            new \Twig\TwigFilter('minify_css', [$this, 'minifyCss']),
81
            new \Twig\TwigFilter('minify_js', [$this, 'minifyJs']),
82
            new \Twig\TwigFilter('scss_to_css', [$this, 'scssToCss']),
83
            new \Twig\TwigFilter('sass_to_css', [$this, 'scssToCss']),
84
            new \Twig\TwigFilter('resize', [$this, 'resize']),
85
            // content
86
            new \Twig\TwigFilter('slugify', [$this, 'slugifyFilter']),
87
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
88
            new \Twig\TwigFilter('excerpt_html', [$this, 'excerptHtml']),
89
            // deprecated
90
            new \Twig\TwigFilter(
91
                'filterBySection',
92
                [$this, 'filterBySection'],
93
                ['deprecated' => true, 'alternative' => 'filter_by']
94
            ),
95
            new \Twig\TwigFilter(
96
                'filterBy',
97
                [$this, 'filterBy'],
98
                ['deprecated' => true, 'alternative' => 'filter_by']
99
            ),
100
            new \Twig\TwigFilter(
101
                'sortByTitle',
102
                [$this, 'sortByTitle'],
103
                ['deprecated' => true, 'alternative' => 'sort_by_title']
104
            ),
105
            new \Twig\TwigFilter(
106
                'sortByWeight',
107
                [$this, 'sortByWeight'],
108
                ['deprecated' => true, 'alternative' => 'sort_by_weight']
109
            ),
110
            new \Twig\TwigFilter(
111
                'sortByDate',
112
                [$this, 'sortByDate'],
113
                ['deprecated' => true, 'alternative' => 'sort_by_date']
114
            ),
115
            new \Twig\TwigFilter(
116
                'minifyCSS',
117
                [$this, 'minifyCss'],
118
                ['deprecated' => true, 'alternative' => 'minifyCss']
119
            ),
120
            new \Twig\TwigFilter(
121
                'minifyJS',
122
                [$this, 'minifyJs'],
123
                ['deprecated' => true, 'alternative' => 'minifyJs']
124
            ),
125
            new \Twig\TwigFilter(
126
                'SCSStoCSS',
127
                [$this, 'scssToCss'],
128
                ['deprecated' => true, 'alternative' => 'scss_to_css']
129
            ),
130
            new \Twig\TwigFilter(
131
                'excerptHtml',
132
                [$this, 'excerptHtml'],
133
                ['deprecated' => true, 'alternative' => 'excerpt_html']
134
            ),
135
            new \Twig\TwigFilter(
136
                'urlize',
137
                [$this, 'slugifyFilter'],
138
                ['deprecated' => true, 'alternative' => 'slugify']
139
            ),
140
        ];
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function getFunctions()
147
    {
148
        return [
149
            // assets
150
            new \Twig\TwigFunction('url', [$this, 'url']),
151
            new \Twig\TwigFunction('asset', [$this, 'asset']),
152
            new \Twig\TwigFunction('integrity', [$this, 'integrity']),
153
            // content
154
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
155
            // others
156
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
157
            // deprecated
158
            new \Twig\TwigFunction(
159
                'minify',
160
                [$this, 'minify'],
161
                ['deprecated' => true, 'alternative' => 'minify filter']
162
            ),
163
            new \Twig\TwigFunction(
164
                'toCSS',
165
                [$this, 'toCss'],
166
                ['deprecated' => true, 'alternative' => 'to_css filter']
167
            ),
168
            new \Twig\TwigFunction(
169
                'hash',
170
                [$this, 'integrity'],
171
                ['deprecated' => true, 'alternative' => 'integrity']
172
            ),
173
        ];
174
    }
175
176
    /**
177
     * Filters by Section.
178
     * Alias of `filterBy('section', $value)`.
179
     *
180
     * @param PagesCollection $pages
181
     * @param string          $section
182
     *
183
     * @return CollectionInterface
184
     */
185
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
186
    {
187
        return $this->filterBy($pages, 'section', $section);
188
    }
189
190
    /**
191
     * Filters by variable's name/value.
192
     *
193
     * @param PagesCollection $pages
194
     * @param string          $variable
195
     * @param string          $value
196
     *
197
     * @return CollectionInterface
198
     */
199
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
200
    {
201
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
202
            $notVirtual = false;
203
            if (!$page->isVirtual()) {
204
                $notVirtual = true;
205
            }
206
            // is a dedicated getter exists?
207
            $method = 'get'.ucfirst($variable);
208
            if (method_exists($page, $method) && $page->$method() == $value) {
209
                return $notVirtual && true;
210
            }
211
            if ($page->getVariable($variable) == $value) {
212
                return $notVirtual && true;
213
            }
214
        });
215
216
        return $filteredPages;
217
    }
218
219
    /**
220
     * Sorts by title.
221
     *
222
     * @param \Traversable $collection
223
     *
224
     * @return array
225
     */
226
    public function sortByTitle(\Traversable $collection): array
227
    {
228
        $collection = iterator_to_array($collection);
229
        array_multisort(array_keys($collection), SORT_NATURAL | SORT_FLAG_CASE, $collection);
1 ignored issue
show
Bug introduced by
array_keys($collection) cannot be passed to array_multisort() as the parameter $arr expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

229
        array_multisort(/** @scrutinizer ignore-type */ array_keys($collection), SORT_NATURAL | SORT_FLAG_CASE, $collection);
Loading history...
230
231
        return $collection;
232
    }
233
234
    /**
235
     * Sorts by weight.
236
     *
237
     * @param \Traversable $collection
238
     *
239
     * @return array
240
     */
241
    public function sortByWeight(\Traversable $collection): array
242
    {
243
        $callback = function ($a, $b) {
244
            if (!isset($a['weight'])) {
245
                return 1;
246
            }
247
            if (!isset($b['weight'])) {
248
                return -1;
249
            }
250
            if ($a['weight'] == $b['weight']) {
251
                return 0;
252
            }
253
254
            return ($a['weight'] < $b['weight']) ? -1 : 1;
255
        };
256
257
        $collection = iterator_to_array($collection);
258
        usort($collection, $callback);
259
260
        return $collection;
261
    }
262
263
    /**
264
     * Sorts by date.
265
     *
266
     * @param \Traversable $collection
267
     *
268
     * @return array
269
     */
270
    public function sortByDate(\Traversable $collection): array
271
    {
272
        $callback = function ($a, $b) {
273
            if (!isset($a['date'])) {
274
                return -1;
275
            }
276
            if (!isset($b['date'])) {
277
                return 1;
278
            }
279
            if ($a['date'] == $b['date']) {
280
                return 0;
281
            }
282
283
            return ($a['date'] > $b['date']) ? -1 : 1;
284
        };
285
286
        $collection = iterator_to_array($collection);
287
        usort($collection, $callback);
288
289
        return $collection;
290
    }
291
292
    /**
293
     * Creates an URL.
294
     *
295
     * $options[
296
     *     'canonical' => true,
297
     *     'addhash'   => false,
298
     *     'format'    => 'json',
299
     * ];
300
     *
301
     * @param Page|Asset|string|null $value
302
     * @param array|null             $options
303
     *
304
     * @return mixed
305
     */
306
    public function url($value = null, array $options = null)
307
    {
308
        return new Url($this->builder, $value, $options);
309
    }
310
311
    /**
312
     * Creates an asset (CSS, JS, images, etc.).
313
     *
314
     * @param string     $path    File path (relative from static/ dir).
315
     * @param array|null $options
316
     *
317
     * @return Asset
318
     */
319
    public function asset(string $path, array $options = null): Asset
320
    {
321
        return new Asset($this->builder, $path, $options);
322
    }
323
324
    /**
325
     * Minifying an asset (CSS or JS).
326
     * ie: minify('css/style.css').
327
     *
328
     * @param string|Asset $asset
329
     *
330
     * @return Asset
331
     */
332
    public function minify($asset): Asset
333
    {
334
        if (!$asset instanceof Asset) {
335
            $asset = new Asset($this->builder, $asset);
336
        }
337
338
        return $asset->minify();
339
    }
340
341
    /**
342
     * Compiles a SCSS asset.
343
     *
344
     * @param string|Asset $asset
345
     *
346
     * @return Asset
347
     */
348
    public function toCss($asset): Asset
349
    {
350
        if (!$asset instanceof Asset) {
351
            $asset = new Asset($this->builder, $asset);
352
        }
353
354
        return $asset->compile();
355
    }
356
357
    /**
358
     * Resizes an image.
359
     *
360
     * @param string $path Image path (relative from static/ dir or external).
361
     * @param int    $size Image new size (width).
362
     *
363
     * @return string
364
     */
365
    public function resize(string $path, int $size): string
366
    {
367
        return (new Image($this->builder))->resize($path, $size);
368
    }
369
370
    /**
371
     * Hashing an asset with algo (sha384 by default).
372
     *
373
     * @param string|Asset $path
374
     * @param string       $algo
375
     *
376
     * @return string
377
     */
378
    public function integrity($asset, string $algo = 'sha384'): string
379
    {
380
        if (!$asset instanceof Asset) {
381
            $asset = new Asset($this->builder, $asset);
382
        }
383
384
        return $asset->getIntegrity($algo);
385
    }
386
387
    /**
388
     * Minifying a CSS string.
389
     *
390
     * @param string $value
391
     *
392
     * @return string
393
     */
394
    public function minifyCss(string $value): string
395
    {
396
        $cache = new Cache($this->builder, 'assets');
397
        $cacheKey = $cache->createKeyFromValue($value);
398
        if (!$cache->has($cacheKey)) {
399
            $minifier = new Minify\CSS($value);
400
            $value = $minifier->minify();
401
            $cache->set($cacheKey, $value);
402
        }
403
404
        return $cache->get($cacheKey, $value);
405
    }
406
407
    /**
408
     * Minifying a JavaScript string.
409
     *
410
     * @param string $value
411
     *
412
     * @return string
413
     */
414
    public function minifyJs(string $value): string
415
    {
416
        $cache = new Cache($this->builder, 'assets');
417
        $cacheKey = $cache->createKeyFromValue($value);
418
        if (!$cache->has($cacheKey)) {
419
            $minifier = new Minify\JS($value);
420
            $value = $minifier->minify();
421
            $cache->set($cacheKey, $value);
422
        }
423
424
        return $cache->get($cacheKey, $value);
425
    }
426
427
    /**
428
     * Compiles a SCSS string.
429
     *
430
     * @param string $value
431
     *
432
     * @return string
433
     */
434
    public function scssToCss(string $value): string
435
    {
436
        $cache = new Cache($this->builder, 'assets');
437
        $cacheKey = $cache->createKeyFromValue($value);
438
        if (!$cache->has($cacheKey)) {
439
            $scssPhp = new Compiler();
440
            $formatter = \sprintf('ScssPhp\ScssPhp\Formatter\%s', ucfirst((string) $this->config->get('assets.compile.style')));
441
            if (!class_exists($formatter)) {
442
                throw new Exception(\sprintf('Scss formatter "%s" doesn\'t exists.', $formatter));
443
            }
444
            $scssPhp->setFormatter($formatter);
445
            $scssPhp->setVariables($this->config->get('assets.compile.variables') ?? []);
446
            $value = $scssPhp->compile($value);
447
            $cache->set($cacheKey, $value);
448
        }
449
450
        return $cache->get($cacheKey, $value);
451
    }
452
453
    /**
454
     * Creates an HTML element from an asset.
455
     *
456
     * @param Asset      $asset
457
     * @param array|null $attributes
458
     *
459
     * @return string
460
     */
461
    public function html(Asset $asset, array $attributes = null): string
462
    {
463
        foreach ($attributes as $name => $value) {
464
            if (!empty($value)) {
465
                $htmlAttributes .= \sprintf(' %s="%s"', $name, $value);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $htmlAttributes seems to be never defined.
Loading history...
466
            } else {
467
                $htmlAttributes .= \sprintf(' %s', $name);
468
            }
469
        }
470
471
        switch ($asset['ext']) {
472
            case 'css':
473
                return \sprintf('<link rel="stylesheet" href="%s"%s>', $asset['path'], $htmlAttributes);
474
            case 'js':
475
                return \sprintf('<script src="%s"%%s></script>', $asset['path'], $htmlAttributes);
476
        }
477
478
        if ($asset['type'] == 'image') {
479
            return \sprintf(
480
                '<img src="%s"%s>',
481
                $asset['path'],
482
                $htmlAttributes
483
            );
484
        }
485
486
        throw new Exception(\sprintf('%s is available with CSS, JS and images files only.', '"html" filter'));
487
    }
488
489
    /**
490
     * Returns the content of an Asset.
491
     *
492
     * @param Asset $asset
493
     *
494
     * @return string
495
     */
496
    public function inline(Asset $asset): string
497
    {
498
        if (is_null($asset['content'])) {
499
            throw new Exception(\sprintf('%s is available with CSS et JS files only.', '"inline" filter'));
500
        }
501
502
        return $asset['content'];
503
    }
504
505
    /**
506
     * Reads $length first characters of a string and adds a suffix.
507
     *
508
     * @param string|null $string
509
     * @param int         $length
510
     * @param string      $suffix
511
     *
512
     * @return string|null
513
     */
514
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
515
    {
516
        $string = str_replace('</p>', '<br /><br />', $string);
517
        $string = trim(strip_tags($string, '<br>'), '<br />');
518
        if (mb_strlen($string) > $length) {
519
            $string = mb_substr($string, 0, $length);
520
            $string .= $suffix;
521
        }
522
523
        return $string;
524
    }
525
526
    /**
527
     * Reads characters before '<!-- excerpt|break -->'.
528
     *
529
     * @param string|null $string
530
     *
531
     * @return string|null
532
     */
533
    public function excerptHtml(string $string = null): ?string
534
    {
535
        // https://regex101.com/r/Xl7d5I/3
536
        $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
537
        preg_match('/'.$pattern.'/is', $string, $matches);
538
        if (empty($matches)) {
539
            return $string;
540
        }
541
542
        return trim($matches[1]);
543
    }
544
545
    /**
546
     * Calculates estimated time to read a text.
547
     *
548
     * @param string|null $text
549
     *
550
     * @return string
551
     */
552
    public function readtime(string $text = null): string
553
    {
554
        $words = str_word_count(strip_tags($text));
555
        $min = floor($words / 200);
556
        if ($min === 0) {
557
            return '1';
558
        }
559
560
        return (string) $min;
561
    }
562
563
    /**
564
     * Gets the value of an environment variable.
565
     *
566
     * @param string $var
567
     *
568
     * @return string|null
569
     */
570
    public function getEnv(string $var): ?string
571
    {
572
        return getenv($var) ?: null;
573
    }
574
}
575