Completed
Push — master ( fc2c42...d0941c )
by Richard
15s
created

Assets   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 451
Duplicated Lines 5.76 %

Coupling/Cohesion

Components 1
Dependencies 21

Test Coverage

Coverage 65.06%

Importance

Changes 0
Metric Value
dl 26
loc 451
ccs 136
cts 209
cp 0.6506
rs 1.9075
c 0
b 0
f 0
wmc 73
lcom 1
cbo 21

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 3
C readAssetsPrefs() 0 47 15
A saveAssetsPrefs() 12 12 3
A setDebug() 0 5 1
A copyBaseFileAssets() 0 6 2
B copyFileAssets() 0 36 5
F registerAssetReference() 7 76 20
F getUrlToAssets() 7 121 24

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Assets 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Assets, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 You may not change or alter any portion of this comment or credits
4
 of supporting developers from this source code or any supporting source code
5
 which is considered copyrighted (c) material of the original comment or credit authors.
6
7
 This program is distributed in the hope that it will be useful,
8
 but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
*/
11
12
namespace Xoops\Core;
13
14
use Assetic\AssetManager;
15
use Assetic\FilterManager;
16
use Assetic\Filter;
17
use Assetic\Factory\AssetFactory;
18
use Assetic\Factory\Worker\CacheBustingWorker;
19
use Assetic\AssetWriter;
20
use Assetic\Asset\AssetCollection;
21
use Assetic\Asset\FileAsset;
22
use Assetic\Asset\GlobAsset;
23
use Xmf\Yaml;
24
25
/**
26
 * Provides a standardized asset strategy
27
 *
28
 * @category  Assets
29
 * @package   Assets
30
 * @author    Richard Griffith <[email protected]>
31
 * @copyright 2014-2017 XOOPS Project (http://xoops.org)
32
 * @license   GNU GPL 2 or later (http://www.gnu.org/licenses/gpl-2.0.html)
33
 * @version   Release: 1.0
34
 * @link      http://xoops.org
35
 * @since     2.6.0
36
 */
37
class Assets
38
{
39
    /** @var array  */
40
    protected $assetPrefs;
41
42
    /** @var boolean */
43
    private $debug = false;
44
45
    /** @var array of default filter strings - may be overridden by prefs */
46
    private $default_filters = array(
47
            'css' => 'cssimport,cssembed,?cssmin',
48
            'js'  => '?jsqueeze',
49
    );
50
51
    /** @var array of output locations in assets directory */
52
    private $default_output = array(
53
            'css' => 'css/*.css',
54
            'js'  => 'js/*.js',
55
    );
56
57
    /** @var array of asset reference definitions - may be overridden by prefs */
58
    private $default_asset_refs = array(
59
        array(
60
            'name' => 'jquery',
61
            'assets' => array('media/jquery/jquery.js'),
62
            'filters' => null,
63
        ),
64
        array(
65
            'name' => 'jqueryui',
66
            'assets' => array('media/jquery/ui/jquery-ui.js'),
67
            'filters' => null,
68
        ),
69
        array(
70
            'name' => 'jgrowl',
71
            'assets' => array('media/jquery/plugins/jquery.jgrowl.js'),
72
            'filters' => null,
73
        ),
74
        array(
75
            'name' => 'fontawesome',
76
            'assets' => array('media/font-awesome/css/font-awesome.min.css'),
77
            'filters' => null,
78
        ),
79
    );
80
81
    /**
82
     * @var array of file assets to copy to assets
83
     */
84
    private $default_file_assets = array(
85
        array(
86
            'type' => 'fonts',
87
            'path' => 'media/font-awesome/fonts',
88
            'pattern' => '*',
89
        ),
90
    );
91
92
93
    /** @var AssetManager */
94
    private $assetManager = null;
95
96
    /** @var string config file with assets prefs */
97
    private $assetsPrefsFilename = 'var/configs/system_assets_prefs.yml';
98
99
    /** @var string config cache key */
100
    private $assetsPrefsCacheKey = 'system/assets/prefs';
101
102
    /** @var string string to identify Assetic filters using instanceof */
103
    private $filterInterface = '\Assetic\Filter\FilterInterface';
104
105
    /**
106
     * __construct
107
     */
108 3
    public function __construct()
0 ignored issues
show
Coding Style introduced by
__construct uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
109
    {
110 3
        $this->assetManager = new AssetManager();
111 3
        if (isset($_REQUEST['ASSET_DEBUG'])) {
112
            $this->setDebug();
113
        }
114 3
        $this->assetPrefs = $this->readAssetsPrefs();
115
        // register any asset references
116 3
        foreach ($this->default_asset_refs as $ref) {
117 3
            $this->registerAssetReference($ref['name'], $ref['assets'], $ref['filters']);
118
        }
119 3
    }
120
121
    /**
122
     * readAssetsPrefs - read configured asset preferences
123
     *
124
     * @return array of assets preferences
125
     */
126 3
    protected function readAssetsPrefs()
127
    {
128 3
        $xoops = \Xoops::getInstance();
129
130
        try {
131 3
            $assetsPrefs = $xoops->cache()->read($this->assetsPrefsCacheKey);
132 3
            $file = $xoops->path($this->assetsPrefsFilename);
133 3
            $mtime = filemtime($file);
134 3
            if ($assetsPrefs===false || !isset($assetsPrefs['mtime']) || !$mtime
135 3
                || (isset($assetsPrefs['mtime']) && $assetsPrefs['mtime']<$mtime)) {
136 1
                if ($mtime) {
137
                    $assetsPrefs = Yaml::read($file);
138
                    if (!is_array($assetsPrefs)) {
139
                        $xoops->logger()->error("Invalid config in system_assets_prefs.yml");
140
                        $assetsPrefs = array();
141
                    } else {
142
                        $assetsPrefs['mtime']=$mtime;
143
                        $xoops->cache()->write($this->assetsPrefsCacheKey, $assetsPrefs);
144
                        $this->copyBaseFileAssets();
145
                    }
146
                } else {
147
                    // use defaults to create file
148
                    $assetsPrefs = array(
149 1
                        'default_filters' => $this->default_filters,
150 1
                        'default_asset_refs' => $this->default_asset_refs,
151 1
                        'default_file_assets' => $this->default_file_assets,
152 1
                        'mtime' => time(),
153
                    );
154 1
                    $this->saveAssetsPrefs($assetsPrefs);
155 1
                    $this->copyBaseFileAssets();
156
                }
157
            }
158 3
            if (!empty($assetsPrefs['default_filters']) && is_array($assetsPrefs['default_filters'])) {
159 3
                $this->default_filters = $assetsPrefs['default_filters'];
160
            }
161 3
            if (!empty($assetsPrefs['default_asset_refs']) && is_array($assetsPrefs['default_asset_refs'])) {
162 3
                $this->default_asset_refs = $assetsPrefs['default_asset_refs'];
163
            }
164 3
            if (!empty($assetsPrefs['default_file_assets']) && is_array($assetsPrefs['default_file_assets'])) {
165 3
                $this->default_file_assets = $assetsPrefs['default_file_assets'];
166
            }
167
        } catch (\Exception $e) {
168
            $xoops->events()->triggerEvent('core.exception', $e);
169
            $assetsPrefs = array();
170
        }
171 3
        return $assetsPrefs;
172
    }
173
174
    /**
175
     * saveAssetsPrefs - record array of assets preferences in config file, and
176
     * update cache
177
     *
178
     * @param array $assets_prefs array of asset preferences to save
179
     *
180
     * @return void
181
     */
182 1 View Code Duplication
    protected function saveAssetsPrefs($assets_prefs)
183
    {
184 1
        if (is_array($assets_prefs)) {
185 1
            $xoops = \Xoops::getInstance();
186
            try {
187 1
                Yaml::save($assets_prefs, $xoops->path($this->assetsPrefsFilename));
188 1
                $xoops->cache()->write($this->assetsPrefsCacheKey, $assets_prefs);
189
            } catch (\Exception $e) {
190
                $xoops->events()->triggerEvent('core.exception', $e);
191
            }
192
        }
193 1
    }
194
195
196
    /**
197
     * getUrlToAssets
198
     *
199
     * Create an asset file from a list of assets
200
     *
201
     * @param string       $type    type of asset, css or js
202
     * @param array        $assets  list of source files to process
203
     * @param string|array $filters either a comma separated list of known namsed filters
204
     *                              or an array of named filters and/or filter object
205
     * @param string       $target  target path, will default to assets directory
206
     *
207
     * @return string URL to asset file
208
     */
209 1
    public function getUrlToAssets($type, $assets, $filters = 'default', $target = null)
210
    {
211 1
        if (is_scalar($assets)) {
212
            $assets = array($assets); // just a single path name
213
        }
214
215 1
        if ($filters==='default') {
216 1
            if (isset($this->default_filters[$type])) {
217 1
                $filters = $this->default_filters[$type];
218
            } else {
219
                $filters = '';
220
            }
221
        }
222
223 1 View Code Duplication
        if (!is_array($filters)) {
224 1
            if (empty($filters)) {
225
                $filters = array();
226
            } else {
227 1
                $filters = explode(',', str_replace(' ', '', $filters));
228
            }
229
        }
230
231 1
        if (isset($this->default_output[$type])) {
232 1
            $output = $this->default_output[$type];
233
        } else {
234
            $output = '';
235
        }
236
237 1
        $xoops = \Xoops::getInstance();
238
239 1
        if (isset($target)) {
240
            $target_path = $target;
241
        } else {
242 1
            $target_path = $xoops->path('assets');
243
        }
244
245
        try {
246 1
            $am = $this->assetManager;
247 1
            $fm = new FilterManager();
248
249 1
            foreach ($filters as $filter) {
250 1
                if (is_object($filter) && $filter instanceof $this->filterInterface) {
251
                    $filterArray[] = $filter;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$filterArray was never initialized. Although not strictly required by PHP, it is generally a good practice to add $filterArray = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
252
                } else {
253 1
                    switch (ltrim($filter, '?')) {
254 1
                        case 'cssembed':
255 1
                            $fm->set('cssembed', new Filter\PhpCssEmbedFilter());
256 1
                            break;
257 1
                        case 'cssmin':
258 1
                            $fm->set('cssmin', new Filter\CssMinFilter());
259 1
                            break;
260 1
                        case 'cssimport':
261 1
                            $fm->set('cssimport', new Filter\CssImportFilter());
262 1
                            break;
263 1
                        case 'cssrewrite':
264
                            $fm->set('cssrewrite', new Filter\CssRewriteFilter());
265
                            break;
266 1
                        case 'lessphp':
267
                            $fm->set('lessphp', new Filter\LessphpFilter());
268
                            break;
269 1
                        case 'scssphp':
270
                            $fm->set('scssphp', new Filter\ScssphpFilter());
271
                            break;
272 1
                        case 'jsmin':
273
                            $fm->set('jsmin', new Filter\JSMinFilter());
274
                            break;
275 1
                        case 'jsqueeze':
276 1
                            $fm->set('jsqueeze', new Filter\JSqueezeFilter());
277 1
                            break;
278
                        default:
279
                            throw new \Exception(sprintf('%s filter not implemented.', $filter));
280 1
                            break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
281
                    }
282
                }
283
            }
284
285
            // Factory setup
286 1
            $factory = new AssetFactory($target_path);
287 1
            $factory->setAssetManager($am);
288 1
            $factory->setFilterManager($fm);
289 1
            $factory->setDefaultOutput($output);
290 1
            $factory->setDebug($this->debug);
291 1
            $factory->addWorker(new CacheBustingWorker());
292
293
            // Prepare the assets writer
294 1
            $writer = new AssetWriter($target_path);
295
296
            // Translate asset paths, remove duplicates
297 1
            $translated_assets = array();
298 1
            foreach ($assets as $k => $v) {
299
                // translate path if not a reference or absolute path
300 1
                if (0 == preg_match("/^\\/|^\\\\|^[a-zA-Z]:|^@/", $v)) {
301 1
                    $v = $xoops->path($v);
302
                }
303 1
                if (!in_array($v, $translated_assets)) {
304 1
                    $translated_assets[] = $v;
305
                }
306
            }
307
308
            // Create the asset
309 1
            $asset = $factory->createAsset(
310 1
                $translated_assets,
311 1
                $filters
312
            );
313 1
            $asset_path = $asset->getTargetPath();
314 1
            if (!is_readable($target_path . $asset_path)) {
315
                $assetKey = 'Asset '.$asset_path;
316
                $xoops->events()->triggerEvent('debug.timer.start', $assetKey);
317
                $oldumask = umask(0002);
318
                $writer->writeAsset($asset);
319
                umask($oldumask);
320
                $xoops->events()->triggerEvent('debug.timer.stop', $assetKey);
321
            }
322
323 1
            return $xoops->url('assets/' . $asset_path);
324
325
        } catch (\Exception $e) {
326
            $xoops->events()->triggerEvent('core.exception', $e);
327
            return null;
328
        }
329
    }
330
331
332
    /**
333
     * setDebug enable debug mode, will skip filters prefixed with '?'
334
     *
335
     * @return true
336
     */
337 1
    public function setDebug()
338
    {
339 1
        $this->debug = true;
340 1
        return true;
341
    }
342
343
    /**
344
     * Add an asset reference to the asset manager
345
     *
346
     * @param string       $name    the name of the reference to be added
347
     * @param mixed        $assets  a string asset path, or an array of asset paths,
348
     *                              may include wildcard
349
     * @param string|array $filters either a comma separated list of known named filters
350
     *                              or an array of named filters and/or filter object
351
     *
352
     * @return boolean true if asset registers, false on error
353
     */
354 3
    public function registerAssetReference($name, $assets, $filters = null)
355
    {
356 3
        $xoops = \Xoops::getInstance();
357
358 3
        $assetArray = array();
359 3
        $filterArray = array();
360
361
        try {
362 3
            if (is_scalar($assets)) {
363 1
                $assets = array($assets);  // just a single path name
364
            }
365 3
            foreach ($assets as $a) {
0 ignored issues
show
Bug introduced by
The expression $assets of type object|array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
366
                // translate path if not a reference or absolute path
367 3
                if ((substr_compare($a, '@', 0, 1) != 0)
368 3
                    && (substr_compare($a, '/', 0, 1) != 0)) {
369 3
                    $a = $xoops->path($a);
370
                }
371 3
                if (false===strpos($a, '*')) {
372 3
                    $assetArray[] = new FileAsset($a); // single file
373
                } else {
374 3
                    $assetArray[] = new GlobAsset($a);  // wild card match
375
                }
376
            }
377
378 3 View Code Duplication
            if (!is_array($filters)) {
379 3
                if (empty($filters)) {
380 3
                    $filters = array();
381
                } else {
382
                    $filters = explode(',', str_replace(' ', '', $filters));
383
                }
384
            }
385 3
            foreach ($filters as $filter) {
386
                if (is_object($filter) && $filter instanceof $this->filterInterface) {
387
                    $filterArray[] = $filter;
388
                } else {
389
                    switch (ltrim($filter, '?')) {
390
                        case 'cssembed':
391
                            $filterArray[] = new Filter\PhpCssEmbedFilter();
392
                            break;
393
                        case 'cssmin':
394
                            $filterArray[] = new Filter\CssMinFilter();
395
                            break;
396
                        case 'cssimport':
397
                            $filterArray[] = new Filter\CssImportFilter();
398
                            break;
399
                        case 'cssrewrite':
400
                            $filterArray[] = new Filter\CssRewriteFilter();
401
                            break;
402
                        case 'lessphp':
403
                            $filterArray[] = new Filter\LessphpFilter();
404
                            break;
405
                        case 'scssphp':
406
                            $filterArray[] = new Filter\ScssphpFilter();
407
                            break;
408
                        case 'jsmin':
409
                            $filterArray[] = new Filter\JSMinFilter();
410
                            break;
411
                        case 'jsqueeze':
412
                            $filterArray[] = new Filter\JSqueezeFilter();
413
                            break;
414
                        default:
415
                            throw new \Exception(sprintf('%s filter not implemented.', $filter));
416
                            break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
417
                    }
418
                }
419
            }
420
421 3
            $collection = new AssetCollection($assetArray, $filterArray);
422 3
            $this->assetManager->set($name, $collection);
423
424 3
            return true;
425
        } catch (\Exception $e) {
426
            $xoops->events()->triggerEvent('core.exception', $e);
427
            return false;
428
        }
429
    }
430
431 1
    public function copyBaseFileAssets()
432
    {
433 1
        foreach ($this->default_file_assets as $fileSpec) {
434 1
            $this->copyFileAssets($fileSpec['path'], trim($fileSpec['pattern']), $fileSpec['type']);
435
        }
436 1
    }
437
438
    /**
439
     * copyFileAssets - copy files to the appropriate asset directory.
440
     *
441
     * Copying is normally only needed for fonts or images when they are referenced by a
442
     * relative url in stylesheet, or are located outside of the web root.
443
     *
444
     * @param string $fromPath path to files to copy
445
     * @param string $pattern  glob pattern to match files to be copied
446
     * @param string $output   output type (css, fonts, images, js)
447
     *
448
     * @return mixed boolean false if target directory is not writable, otherwise
449
     *               integer count of files copied
450
     */
451 2
    public function copyFileAssets($fromPath, $pattern, $output)
452
    {
453 2
        $xoops = \Xoops::getInstance();
454
455 2
        $fromPath = $xoops->path($fromPath) . '/';
456 2
        $toPath = $xoops->path('assets') . '/' . $output . '/';
457 2
        $from = glob($fromPath . '/' . $pattern);
458
459 2
        if (!is_dir($toPath)) {
460 2
            $oldUmask = umask(0);
461 2
            mkdir($toPath, 0775, true);
462 2
            umask($oldUmask);
463
        }
464
465 2
        if (is_writable($toPath)) {
466 2
            $count = 0;
467 2
            $oldUmask = umask(0002);
468 2
            foreach ($from as $filepath) {
469 2
                $xoops->events()->triggerEvent('debug.timer.start', $filepath);
470 2
                $filename = basename($filepath);
471 2
                $status=copy($filepath, $toPath.$filename);
472 2
                if (false===$status) {
473
                    $xoops->logger()->warning('Failed to copy asset '.$filename);
474
                } else {
475
                    //$xoops->logger()->debug('Copied asset '.$filename);
476 2
                    ++$count;
477
                }
478 2
                $xoops->events()->triggerEvent('debug.timer.stop', $filepath);
479
            }
480 2
            umask($oldUmask);
481 2
            return $count;
482
        } else {
483
            $xoops->logger()->warning('Asset directory is not writable. ' . $output);
484
            return false;
485
        }
486
    }
487
}
488