Test Failed
Push — master ( 8f2167...5d2217 )
by Georgi
08:27
created

ModuleManager   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 393
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 123
dl 0
loc 393
rs 7.44
c 0
b 0
f 0
wmc 52

17 Methods

Rating   Name   Duplication   Size   Complexity  
A isAvailable() 0 3 1
A clearCache() 0 7 1
A getAll() 0 11 4
A collect() 0 21 6
A call() 0 3 1
A getClass() 0 6 3
A isInstalled() 0 3 2
A listRecommended() 0 9 2
A uninstall() 0 41 5
A unsatisfiedDependencies() 0 2 1
A getCached() 0 7 2
A listDependents() 0 8 3
B install() 0 54 8
A discoverModuleClasses() 0 23 5
A listDependencies() 0 9 2
A getInstalled() 0 10 2
A satisfyDependencies() 0 23 4

How to fix   Complexity   

Complex Class

Complex classes like ModuleManager 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 ModuleManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Epesi\Core\System\Modules;
4
5
use Epesi\Core\System\Models\Module;
6
use Illuminate\Support\Facades\Cache;
7
use atk4\core\Exception;
8
use Illuminate\Support\Facades\File;
9
10
class ModuleManager
11
{
12
	use Concerns\HasPackageManifest;
13
	
14
	/**
15
	 * @var \Illuminate\Support\Collection
16
	 */
17
	private static $installed;
18
	private static $processing;
19
20
	/**
21
	 * Check if a module is installed providing an alias or class name
22
	 *
23
	 * @param string $classOrAlias
24
	 *
25
	 * @return boolean
26
	 */
27
	public static function isInstalled($classOrAlias)
28
	{
29
		return self::getClass($classOrAlias, true)? 1: 0;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::getClass($classOrAlias, true) ? 1 : 0 returns the type integer which is incompatible with the documented return type boolean.
Loading history...
30
	}
31
	
32
	/**
33
	 * Check if a module is available providing an alias or class name
34
	 * 
35
	 * @param string $classOrAlias
36
	 * 
37
	 * @return boolean
38
	 */
39
	public static function isAvailable($classOrAlias)
40
	{
41
		return class_exists(self::getClass($classOrAlias));
42
	}
43
44
	/**
45
	 * Get the module core class from class or alias
46
	 * 
47
	 * @param string $classOrAlias
48
	 * @return string;
49
	 */
50
	public static function getClass($classOrAlias, $installedOnly = false) {
51
		$modules = $installedOnly? self::getInstalled(): self::getAll();
52
53
		if (collect($modules)->contains($classOrAlias)) return $classOrAlias;
54
		
55
		return $modules[$classOrAlias]?? null;
56
	}
57
	
58
	/**
59
	 * Get a collection of installed modules in alias -> class pairs
60
	 * 
61
	 * @return \Illuminate\Support\Collection;
62
	 */
63
	public static function getInstalled()
64
	{
65
		return self::$installed = self::$installed?? self::getCached('epesi-modules-installed', function() {
66
			try {
67
			    $installedModules = Module::pluck('class', 'alias');
68
			} catch (\Exception $e) {
69
				$installedModules = collect();
70
			}
71
			
72
			return $installedModules;
73
		});
74
	}
75
	
76
	/**
77
	 * Get a collection of all manifested modules in alias -> class pairs
78
	 * 
79
	 * @return \Illuminate\Support\Collection;
80
	 */
81
	public static function getAll()
82
	{
83
		return self::getCached('epesi-modules-available', function () {
84
			$modules = collect();
85
			foreach (array_merge(config('epesi.modules', []), self::packageManifest()->modules()?: []) as $namespace => $path) {
86
				foreach (self::discoverModuleClasses($namespace, $path) as $moduleClass) {
87
					$modules->add(['alias' => $moduleClass::alias(), 'class' => $moduleClass]);
88
				}
89
			}
90
91
			return $modules->pluck('class', 'alias');
92
		});
93
	}
94
	
95
	/**
96
	 * Scans the profided $basePath directrory recursively to locate modules
97
	 * A directory is considered having a module when it has a file descendant of ModuleCore
98
	 * having the directory name with 'Core' suffix, e.g Test -> TestCore extends ModuleCore
99
	 * 
100
	 * @param string $namespace
101
	 * @param string $basePath
102
	 * 
103
	 * @return \Illuminate\Support\Collection
104
	 */
105
	protected static function discoverModuleClasses($namespace, $basePath)
106
	{
107
	    $ret = collect();
108
	    
109
	    $moduleNamespace = trim($namespace, '\\');
110
	    
111
	    $names = array_slice(explode('\\', $moduleNamespace), - 1);
112
	    
113
	    if ($name = $names? reset($names): '') {
114
	        $moduleClass = $moduleNamespace . '\\' . $name . 'Core';
115
	        
116
	        if (is_subclass_of($moduleClass, ModuleCore::class)) {
117
	            $ret->add($moduleClass);
118
	        }
119
	    }
120
	    
121
	    foreach (glob($basePath . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $path) {
122
	        $subModuleNamespace = $moduleNamespace . '\\' . basename($path);
123
	        
124
	        $ret = $ret->merge(self::discoverModuleClasses($subModuleNamespace, $path));
125
	    }
126
	    
127
	    return $ret;
128
	}
129
	
130
	/**
131
	 * Common method to use for caching of data within module manager
132
	 * 
133
	 * @param string $key
134
	 * @param \Closure $default
135
	 * @return mixed
136
	 */
137
	protected static function getCached($key, \Closure $default)
138
	{
139
		if (! Cache::has($key)) {
140
			Cache::forever($key, $default());
141
		}
142
143
		return Cache::get($key);
144
	}
145
	
146
	/**
147
	 * Clear module manager cache
148
	 */
149
	public static function clearCache()
150
	{
151
		self::$installed = null;
152
		File::cleanDirectory(base_path('bootstrap/cache'));
153
		
154
		Cache::forget('epesi-modules-installed');
155
		Cache::forget('epesi-modules-available');
156
	}
157
	
158
	/**
159
	 * Alias for collect when no return values expected
160
	 *
161
	 * @param string $method
162
	 * @return array
163
	 */
164
	public static function call($method, $args = [])
165
	{
166
		return self::collect($method, $args);
167
	}
168
	
169
	/**
170
	 * Collect array of results from $method in all installed module core classes
171
	 *
172
	 * @param string $method
173
	 * @return array
174
	 */
175
	public static function collect($method, $args = [])
176
	{
177
		$args = is_array($args)? $args: [$args];
178
		
179
		$installedModules = self::getInstalled();
180
		
181
		// if epesi is not installed fake having the system module to enable its functionality
182
		if (! $installedModules->contains(\Epesi\Core\System\SystemCore::class)) {
183
			$installedModules = collect([
184
				'system' => \Epesi\Core\System\SystemCore::class
185
			]);
186
		}
187
		
188
		$ret = [];
189
		foreach ($installedModules as $module) {
190
			if (! $list = $module::$method(...$args)) continue;
191
			
192
			$ret = array_merge($ret, is_array($list)? $list: [$list]);
193
		}
194
		
195
		return $ret;
196
	}
197
198
	/**
199
	 * Install the module class provided as argument
200
	 * 
201
	 * @param string $classOrAlias
202
	 */
203
	public static function install($classOrAlias, $installRecommended = true)
204
	{
205
		if (self::isInstalled($classOrAlias)) {
206
			print ('Module "' . $classOrAlias . '" already installed!');
207
			
208
			return true;
209
		}
210
		
211
		if (! $moduleClass = self::getClass($classOrAlias)) {			
212
			throw new \Exception('Module "' . $classOrAlias . '" could not be identified');
213
		}
214
		
215
		/**
216
		 * @var ModuleCore $module
217
		 */
218
		$module = new $moduleClass();
219
		
220
		$module->migrate();
221
		
222
		self::satisfyDependencies($moduleClass);
223
		
224
		try {
225
			$module->install();
226
		} catch (\Exception $exception) {
227
			$module->rollback();
228
			
229
			throw $exception;
230
		}
231
		
232
		$module->publishAssets();
233
		
234
		// update database
235
		Module::create()->insert([
236
				'class' => $moduleClass,
237
				'alias' => $module->alias()
238
		]);
239
		
240
		if ($installRecommended) {
241
			$installRecommended = is_array($installRecommended)? $installRecommended: $module->recommended();
242
			
243
			foreach ($installRecommended as $recommendedModule) {
244
				try {
245
					self::install($recommendedModule);
246
				} catch (\Exception $e) {
247
					// just continue, nothing to do if module cannot be installed
248
				}
249
			}
250
		}
251
				
252
		self::clearCache();
253
		
254
		print ('Module ' . $module->label() . ' successfully installed!');
255
		
256
		return true;
257
	}
258
	
259
	/**
260
	 * Install modules that $moduleClass requires
261
	 * Performs operation recursively for all required modules
262
	 * 
263
	 * @param string $moduleClass
264
	 * @throws \Exception
265
	 * @return boolean
266
	 */
267
	protected static function satisfyDependencies($moduleClass) {
268
		self::$processing[$moduleClass] = true;
269
		
270
		while ($unsatisfiedDependencies = self::unsatisfiedDependencies($moduleClass)) {
271
			$parentModule = array_shift($unsatisfiedDependencies);
272
				
273
			if (self::$processing[$parentModule]?? false) {
274
				throw new \Exception('Cross dependency: '. $parentModule);
275
			}
276
				
277
			if (! self::isAvailable($parentModule)) {
278
				throw new \Exception('Module not found: "' . $parentModule . '"');
279
			}
280
	
281
			print("\n\r");
282
			print('Installing required module: "' . $parentModule . '" by "' . $moduleClass . '"');
283
284
			self::install($parentModule);
285
		}
286
287
		unset(self::$processing[$moduleClass]);
288
		
289
		return true;
290
	}
291
	
292
	/**
293
	 * Finds modules which are required by $moduleClass but not yet installed
294
	 * 
295
	 * @param string $moduleClass
296
	 * 
297
	 * @return array
298
	 */
299
	protected static function unsatisfiedDependencies($moduleClass) {
300
		return collect($moduleClass::requires())->diff(self::getInstalled())->filter()->all();
301
	}	
302
	
303
	/**
304
	 * Finds $moduleClass dependencies recursively (including dependencies of dependencies)
305
	 * 
306
	 * @param string $moduleClass
307
	 * 
308
	 * @return array
309
	 */
310
	public static function listDependencies($moduleClass) {
311
		$ret = collect();
312
		foreach (collect($moduleClass::requires()) as $parentClass) {
313
			$ret->add($parentClass = self::getClass($parentClass));
314
			
315
			$ret = $ret->merge(self::listDependencies($parentClass));
316
		}
317
		
318
		return $ret->filter()->unique()->all();
319
	}
320
	
321
	/**
322
	 * Finds $moduleClass recommended modules recursively (including recommended of recommended)
323
	 * 
324
	 * @param string $moduleClass
325
	 * 
326
	 * @return array|number|mixed
327
	 */
328
	public static function listRecommended($moduleClass) {
329
		$ret = collect();
330
		foreach (collect($moduleClass::recommended()) as $childClass) {
331
			$ret->add($childClass = self::getClass($childClass));
332
			
333
			$ret = $ret->merge(self::listRecommended($childClass));
334
		}
335
		
336
		return $ret->filter()->unique()->all();
337
	}
338
	
339
	/**
340
	 * Creates array of dependencies of installed modules
341
	 * 
342
	 * @return array
343
	 */
344
	public static function listDependents() {
345
		$ret = [];
346
		foreach (self::getInstalled() as $moduleClass) {
347
			foreach ($moduleClass::requires() as $parentClass) {
348
				$ret[$parentClass][] = $moduleClass;
349
			}
350
		}
351
		return $ret;
352
	}
353
	
354
	/**
355
	 * Runs uninstallation routine on $classOrAlias
356
	 * 
357
	 * @param string $classOrAlias
358
	 * @throws \Exception
359
360
	 * @return boolean
361
	 */
362
	public static function uninstall($classOrAlias)
363
	{
364
		if (! self::isInstalled($classOrAlias)) {
365
			print ('Module "' . $classOrAlias . '" is not installed!');
366
			
367
			return true;
368
		}
369
		
370
		if (! $moduleClass = self::getClass($classOrAlias)) {
371
			throw new \Exception('Module "' . $classOrAlias . '" could not be identified');
372
		}
373
		
374
		foreach (self::listDependents()[$moduleClass]?? [] as $childModule) {
375
			self::uninstall($childModule);
376
		}
377
		
378
		/**
379
		 * @var ModuleCore $module
380
		 */
381
		$module = new $moduleClass();
382
		
383
		$module->rollback();
384
		
385
		try {
386
			$module->uninstall();
387
		} catch (\Exception $exception) {
388
			$module->migrate();
389
			
390
			throw $exception;
391
		}
392
		
393
		$module->unpublishAssets();
394
		
395
		// update database
396
		Module::create()->addCondition('class', $moduleClass)->tryLoadAny()->delete();
397
		
398
		self::clearCache();
399
		
400
		print ('Module ' . $module->label() . ' successfully uninstalled!');
401
		
402
		return true;
403
	}
404
}
405