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 classes like Addons 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 Addons, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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) { |
|
1 ignored issue
–
show
|
|||
81 | return self::$all; |
||
82 | } else { |
||
83 | $addon = val(strtolower($addon_key), self::$all); |
||
84 | if ($addon && $key) { |
||
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 | } |
||
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 | 1 | } 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 | 34 | Event::bindClass($class_name); |
|
154 | } |
||
155 | } |
||
156 | } |
||
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 | } |
||
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) { |
|
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 | } |
||
204 | } |
||
205 | 34 | self::$classMap = $class_map; |
|
206 | } |
||
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 | } 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 | } |
||
263 | |||
264 | // The array should be built now return the addon. |
||
265 | 34 | View Code Duplication | if ($addon_key === null) { |
1 ignored issue
–
show
|
|||
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) { |
||
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) { |
|
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) { |
|
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) { |
|
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) { |
|
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) { |
|
505 | } |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.