Addons::scanFile()   C
last analyzed

Complexity

Conditions 22
Paths 22

Size

Total Lines 53
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 78.3861

Importance

Changes 0
Metric Value
cc 22
eloc 38
nc 22
nop 1
dl 0
loc 53
ccs 22
cts 43
cp 0.5116
crap 78.3861
rs 6.1683
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2014 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden;
9
10
/**
11
 * Contains functionality that allows addons to enhance or change an application's functionality.
12
 *
13
 * An addon can do the following.
14
 *
15
 * 1. Any classes that the addon defines in its root, /controllers, /library, /models, and /modules
16
 *    directories are made available.
17
 * 2. The addon can contain a bootstrap.php which will be included at the app startup.
18
 * 3. If the addon declares any classes ending in *Plugin then those plugins will automatically
19
 *    bind their event handlers. (also *Hooks)
20
 */
21
class Addons {
22
    /// Constants ///
23
    const K_BOOTSTRAP = 'bootstrap'; // bootstrap path key
24
    const K_CLASSES = 'classes';
25
    const K_DIR = 'dir';
26
    const K_INFO = 'info'; // addon info key
27
28
    /// Properties ///
29
30
    /**
31
     * @var array An array that maps addon keys to full addon information.
32
     */
33
    protected static $all;
34
35
    /**
36
     * @var string The base directory where all of the addons are found.
37
     */
38
    protected static $baseDir;
39
40
    /**
41
     * @var array An array that maps class names to their fully namespaced class names.
42
     */
43
//    protected static $basenameMap;
44
45
    /**
46
     * @var array|null An array that maps class names to file paths.
47
     */
48
    protected static $classMap;
49
50
    /**
51
     * @var array An array that maps addon keys to full addon information for enabled addons.
52
     */
53
    protected static $enabled;
54
55
    /**
56
     * @var array An array of enabled addon keys.
57
     */
58
    protected static $enabledKeys;
59
60
    /**
61
     * @var bool Signals that the addon framework is in a shared environment and shouldn't use the enabled cache.
62
     */
63
    public static $sharedEnvironment;
64
65
    /// Methods ///
66
67
    /**
68
     * Get all of the available addons or a single addon from the available list.
69
     *
70
     * @param string $addon_key If you supply an addon key then only that addon will be returned.
71
     * @param string $key Supply one of the Addons::K_* constants to get a specific key from the addon.
72
     * @return array Returns the addon with the given key or all available addons if no key is passed.
73
     */
74
    public static function all($addon_key = null, $key = null) {
75
        if (self::$all === null) {
76
            self::$all = static::cacheGet('addons-all', array(get_class(), 'scanAddons'));
77
        }
78
79
        // The array should be built now return the addon.
80 View Code Duplication
        if ($addon_key === null) {
81
            return self::$all;
82
        } else {
83
            $addon = val(strtolower($addon_key), self::$all);
84
            if ($addon && $key) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $key of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
85
                return val($key, $addon);
86
            } elseif ($addon) {
87
                return $addon;
88
            } else {
89
                return null;
90
            }
91
        }
92
    }
93
94
    /**
95
     * An autoloader that will autoload a class based on which addons are enabled.
96
     *
97
     * @param string $classname The name of the class to load.
98
     */
99 6
    public static function autoload($classname) {
100 6
        list($fullClass, $path) = static::classMap($classname);
101 6
        if ($path) {
102 4
            require $path;
103 4
        }
104 6
    }
105
106
    /**
107
     * Gets/sets the base directory for addons.
108
     *
109
     * @param string $value Pass a value to set the new base directory.
110
     * @return string Returns the base directory for addons.
111
     */
112 34
    public static function baseDir($value = null) {
113 34
        if ($value !== null) {
114 34
            self::$baseDir = rtrim($value, '/');
115 34
        } elseif (self::$baseDir === null) {
116
            self::$baseDir = PATH_ROOT.'/addons';
117
        }
118 34
    }
119
120
121
    /**
122
     * Start up the addon framework.
123
     *
124
     * @param array $enabled_addons An array of enabled addons.
125
     */
126 34
    public static function bootstrap($enabled_addons = null) {
127
        // Load the addons from the config if they aren't passed in.
128 34
        if (!is_array($enabled_addons)) {
129
            $enabled_addons = config('addons', array());
130
        }
131
        // Reformat the enabled array into the form: array('addon_key' => 'addon_key')
132 34
        $enabled_keys = array_keys(array_change_key_case(array_filter($enabled_addons)));
133 34
        $enabled_keys = array_combine($enabled_keys, $enabled_keys);
134 34
        self::$enabledKeys = $enabled_keys;
135 34
        self::$classMap = null; // invalidate so it will rebuild
136
137
        // Enable the addon autoloader.
138 34
        spl_autoload_register(array(get_class(), 'autoload'), true, true);
139
140
        // Bind all of the addon plugin events now.
141 34
        foreach (self::enabled() as $addon) {
142 34
            if (!isset($addon[self::K_CLASSES])) {
143
                continue;
144
            }
145
146 34
            foreach ($addon[self::K_CLASSES] as $class_name => $class_path) {
147 34
                if (str_ends($class_name, 'plugin')) {
148
                    Event::bindClass($class_name);
149 34
                } elseif (str_ends($class_name, 'hooks')) {
150
                    // Vanilla 2 used hooks files for themes and applications.
151
                    $basename = ucfirst(rtrim_substr($class_name, 'hooks'));
152
                    deprecated($basename.'Hooks', $basename.'Plugin');
153
                    Event::bindClass($class_name);
154
                }
155 34
            }
156 34
        }
157
158
        Event::bind('bootstrap', function () {
159
            // Start each of the enabled addons.
160 34
            foreach (self::enabled() as $key => $value) {
161 34
                static::startAddon($key);
162 34
            }
163 34
        });
164 34
    }
165
166
    /**
167
     * Get the cached file or hydrate the cache with a callback.
168
     *
169
     * @param string $key The cache key to get.
170
     * @param callable $cache_cb The function to run when hydrating the cache.
171
     * @return array Returns the cached array.
172
     */
173 1
    protected static function cacheGet($key, callable $cache_cb) {
174
        // Salt the cache with the root path so that it will invalidate if the app is moved.
175 1
        $salt = substr(md5(static::baseDir()), 0, 10);
176
177 1
        $cache_path = PATH_ROOT."/cache/$key-$salt.json.php";
178 1
        if (file_exists($cache_path)) {
179
            $result = array_load($cache_path);
180
            return $result;
181
        } else {
182 1
            $result = $cache_cb();
183 1
            array_save($result, $cache_path);
184
        }
185 1
        return $result;
186
    }
187
188
    /**
189
     * A an array that maps class names to physical paths.
190
     *
191
     * @param string $classname An optional class name to get the path of.
192
     * @return array Returns an array in the form `[fullClassname, classPath]`.
193
     * If no {@link $classname} is passed then the entire class map is returned.
194
     * @throws \Exception Throws an exception if the class map is corrupt.
195
     */
196 36
    public static function classMap($classname = null) {
197 36
        if (self::$classMap === null) {
198
            // Loop through the enabled addons and grab their classes.
199 34
            $class_map = array();
200 34
            foreach (static::enabled() as $addon) {
201 34
                if (isset($addon[self::K_CLASSES])) {
202 34
                    $class_map = array_replace($class_map, $addon[self::K_CLASSES]);
203 34
                }
204 34
            }
205 34
            self::$classMap = $class_map;
206 34
        }
207
208
        // Now that the class map has been built return the result.
209 36
        if ($classname !== null) {
210 36
            if (strpos($classname, '\\') === false) {
211 34
                $basename = strtolower($classname);
212 34
            } else {
213 3
                $basename = strtolower(trim(strrchr($classname, '\\'), '\\'));
214
            }
215
216 36
            $row = val($basename, self::$classMap);
217
218 36
            if ($row === null) {
219 3
                return ['', ''];
220 34
            } elseif (is_string($row)) {
221
                return [$classname, $row];
222 34
            } elseif (is_array($row)) {
223 34
                return  $row;
224
            } else {
225
                return ['', ''];
226
            }
227
        } else {
228
            return self::$classMap;
229
        }
230
    }
231
232
    /**
233
     * Get all of the enabled addons or a single addon from the enabled list.
234
     *
235
     * @param string $addon_key If you supply an addon key then only that addon will be returned.
236
     * @param string $key Supply one of the Addons::K_* constants to get a specific key from the addon.
237
     * @return array Returns the addon with the given key or all enabled addons if no key is passed.
238
     * @throws \Exception Throws an exception if {@link Addons::bootstrap()} hasn't been called yet.
239
     */
240 34
    public static function enabled($addon_key = null, $key = null) {
241
        // Lazy build the enabled array.
242 34
        if (self::$enabled === null) {
243
            // Make sure the enabled addons have been added first.
244 1
            if (self::$enabledKeys === null) {
245
                throw new \Exception("Addons::boostrap() must be called before Addons::enabled() can be called.", 500);
246
            }
247
248 1
            if (self::$all !== null || self::$sharedEnvironment) {
249
                // Build the enabled array by filtering the all array.
250
                self::$enabled = array();
251
                foreach (self::all() as $key => $row) {
252
                    if (isset($key, self::$enabledKeys)) {
253
                        self::$enabled[$key] = $row;
254
                    }
255
                }
256
            } else {
257
                // Build the enabled array by walking the addons.
258 1
                self::$enabled = static::cacheGet('addons-enabled', function () {
259 1
                    return static::scanAddons(null, self::$enabledKeys);
260 1
                });
261
            }
262 1
        }
263
264
        // The array should be built now return the addon.
265 34 View Code Duplication
        if ($addon_key === null) {
266 34
            return self::$enabled;
267
        } else {
268 34
            $addon = val(strtolower($addon_key), self::$enabled);
269 34
            if ($addon && $key) {
270
                return val($key, $addon);
271 34
            } elseif ($addon) {
272 34
                return $addon;
273
            } else {
274
                return null;
275
            }
276
        }
277
    }
278
279
    /**
280
     * Return the info array for an addon.
281
     *
282
     * @param string $addon_key The addon key.
283
     * @return array|null Returns the addon's info array or null if the addon wasn't found.
284
     */
285
    public static function info($addon_key) {
286
        $addon_key = strtolower($addon_key);
287
288
        // Check the enabled array first so that we don't load all addons if we don't have to.
289
        if (isset(self::$enabledKeys[$addon_key])) {
290
            return static::enabled($addon_key, self::K_INFO);
291
        } else {
292
            return static::all($addon_key, self::K_INFO);
293
        }
294
    }
295
296
    /**
297
     * Scan an addon directory for information.
298
     *
299
     * @param string $dir The addon directory to scan.
300
     * @param array &$addons The addons array.
301
     * @param array $enabled An array of enabled addons or null to scan all addons.
302
     * @return array Returns an array in the form [addonKey, addonInfo].
303
     */
304 1
    protected static function scanAddonRecursive($dir, &$addons, $enabled = null) {
305 1
        $dir = rtrim($dir, '/');
306 1
        $addonKey = strtolower(basename($dir));
307
308
        // Scan the addon if it is enabled.
309 1
        if ($enabled === null || in_array($addonKey, $enabled)) {
310 1
            list($addonKey, $addon) = static::scanAddon($dir);
311 1
        } else {
312
            $addon = null;
313
        }
314
315
        // Add the addon to the collection array if one was supplied.
316 1
        if ($addon !== null) {
317 1
            $addons[$addonKey] = $addon;
318 1
        }
319
320
        // Recurse.
321 1
        $addon_subdirs = array('/addons');
322 1
        foreach ($addon_subdirs as $addon_subdir) {
323 1
            if (is_dir($dir.$addon_subdir)) {
324
                static::scanAddons($dir.$addon_subdir, $enabled, $addons);
325
            }
326 1
        }
327
328 1
        return array($addonKey, $addon);
329
    }
330
331
    /**
332
     * Scan an individual addon directory and return the information about that addon.
333
     *
334
     * @param string $dir The path to the addon.
335
     * @return array An array in the form of `[$addon_key, $addon_row]` or `[$addon_key, null]` if the directory doesn't
336
     * represent an addon.
337
     */
338 1
    protected static function scanAddon($dir) {
339 1
        $dir = rtrim($dir, '/');
340 1
        $addon_key = strtolower(basename($dir));
341
342
        // Look for the addon info array.
343 1
        $info_path = $dir.'/addon.json';
344 1
        $info = false;
345 1
        if (file_exists($info_path)) {
346 1
            $info = json_decode(file_get_contents($info_path), true);
347 1
        }
348 1
        if (!$info) {
349
            $info = array();
350
        }
351 1
        array_touch('name', $info, $addon_key);
352 1
        array_touch('version', $info, '0.0');
353
354
        // Look for the bootstrap.
355 1
        $bootstrap = $dir.'/bootstrap.php';
356 1
        if (!file_exists($dir.'/bootstrap.php')) {
357 1
            $bootstrap = null;
358 1
        }
359
360
        // Scan the appropriate subdirectories  for classes.
361 1
        $subdirs = array('', '/library', '/controllers', '/models', '/modules', '/settings');
362 1
        $classes = array();
363 1
        foreach ($subdirs as $subdir) {
364
            // Get all of the php files in the subdirectory.
365 1
            $paths = glob($dir.$subdir.'/*.php');
366 1
            foreach ($paths as $path) {
367 1
                $decls = static::scanFile($path);
368 1
                foreach ($decls as $namespace_row) {
369 1
                    if (isset($namespace_row['namespace']) && $namespace_row) {
370
                        $namespace = rtrim($namespace_row['namespace'], '\\').'\\';
371
                        $namespace_classes = $namespace_row['classes'];
372
                    } else {
373 1
                        $namespace = '';
374 1
                        $namespace_classes = $namespace_row;
375
                    }
376
377 1
                    foreach ($namespace_classes as $class_row) {
378 1
                        $classes[strtolower($class_row['name'])] = [$namespace.$class_row['name'], $path];
379 1
                    }
380 1
                }
381 1
            }
382 1
        }
383
384
        $addon = array(
385 1
            self::K_BOOTSTRAP => $bootstrap,
386 1
            self::K_CLASSES => $classes,
387 1
            self::K_DIR => $dir,
388 1
            self::K_INFO => $info
389 1
        );
390
391 1
        return array($addon_key, $addon);
392
    }
393
394
    /**
395
     * Scan a directory for addons.
396
     *
397
     * @param string $dir The directory to scan.
398
     * @param array $enabled An array of enabled addons in the form `[addonKey => enabled, ...]`.
399
     * @param array &$addons The addons will fill this array.
400
     * @return array Returns all of the addons.
401
     */
402 1
    protected static function scanAddons($dir = null, $enabled = null, &$addons = null) {
403 1
        if (!$dir) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dir of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
404 1
            $dir = static::$baseDir;
405 1
        }
406 1
        if ($addons === null) {
407 1
            $addons = array();
408 1
        }
409
410
        /* @var \DirectoryIterator */
411 1
        foreach (new \DirectoryIterator($dir) as $subdir) {
412 1
            if ($subdir->isDir() && !$subdir->isDot()) {
413
//                echo $subdir->getPathname().$subdir->isDir().$subdir->isDot().'<br />';
414 1
                static::scanAddonRecursive($subdir->getPathname(), $addons, $enabled);
415 1
            }
416 1
        }
417 1
        return $addons;
418
    }
419
420
    /**
421
     * Looks what classes and namespaces are defined in a file and returns the first found.
422
     *
423
     * @param string $file Path to file.
424
     * @return array Returns an empty array if no classes are found or an array with namespaces and
425
     * classes found in the file.
426
     * @see http://stackoverflow.com/a/11114724/1984219
427
     */
428 1
    protected static function scanFile($file) {
429 1
        $classes = $nsPos = $final = array();
430 1
        $foundNamespace = false;
431 1
        $ii = 0;
432
433 1
        if (!file_exists($file)) {
434
            return array();
435
        }
436
437 1
        $er = error_reporting();
438 1
        error_reporting(E_ALL ^ E_NOTICE);
439
440 1
        $php_code = file_get_contents($file);
441 1
        $tokens = token_get_all($php_code);
442 1
        $count = count($tokens);
443
444 1
        for ($i = 0; $i < $count; $i++) {
445 1
            if (!$foundNamespace && $tokens[$i][0] == T_NAMESPACE) {
446
                $nsPos[$ii]['start'] = $i;
447
                $foundNamespace = true;
448 1
            } elseif ($foundNamespace && ($tokens[$i] == ';' || $tokens[$i] == '{')) {
449
                $nsPos[$ii]['end'] = $i;
450
                $ii++;
451
                $foundNamespace = false;
452 1
            } elseif ($i - 2 >= 0 && $tokens[$i - 2][0] == T_CLASS && $tokens[$i - 1][0] == T_WHITESPACE && $tokens[$i][0] == T_STRING) {
453 1
                if ($i - 4 >= 0 && $tokens[$i - 4][0] == T_ABSTRACT) {
454
                    $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'ABSTRACT CLASS');
455
                } else {
456 1
                    $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'CLASS');
457
                }
458 1
            } elseif ($i - 2 >= 0 && $tokens[$i - 2][0] == T_INTERFACE && $tokens[$i - 1][0] == T_WHITESPACE && $tokens[$i][0] == T_STRING) {
459
                $classes[$ii][] = array('name' => $tokens[$i][1], 'type' => 'INTERFACE');
460
            }
461 1
        }
462 1
        error_reporting($er);
463 1
        if (empty($classes)) {
464
            return [];
465
        }
466
467 1
        if (!empty($nsPos)) {
468
            foreach ($nsPos as $k => $p) {
469
                $ns = '';
470
                for ($i = $p['start'] + 1; $i < $p['end']; $i++) {
471
                    $ns .= $tokens[$i][1];
472
                }
473
474
                $ns = trim($ns);
475
                $final[$k] = array('namespace' => $ns, 'classes' => $classes[$k + 1]);
476
            }
477
            $classes = $final;
478
        }
479 1
        return $classes;
480
    }
481
482
    /**
483
     * Start an addon.
484
     *
485
     * This function does the following:
486
     *
487
     * 1. Make the addon available in the autoloader.
488
     * 2. Run the addon's bootstrap.php if it exists.
489
     *
490
     * @param string $addon_key The key of the addon to enable.
491
     * @return bool Returns true if the addon was enabled. False otherwise.
492
     */
493 34
    public static function startAddon($addon_key) {
494 34
        $addon = static::enabled($addon_key);
495 34
        if (!$addon) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $addon of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
496
            return false;
497
        }
498
499
        // Run the class' bootstrap.
500 34
        if ($bootstrap_path = val(self::K_BOOTSTRAP, $addon)) {
501
            include_once $bootstrap_path;
502
        }
503 34
        return true;
504
    }
505
}