ModuleManager   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 346
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 123
dl 0
loc 346
rs 8.4
c 0
b 0
f 0
wmc 50

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 19 5
A call() 0 3 1
A getClass() 0 9 3
A isInstalled() 0 3 1
A listRecommended() 0 10 2
A uninstall() 0 41 5
A unsatisfiedDependencies() 0 3 1
A getCached() 0 7 2
A listDependents() 0 9 3
B install() 0 54 8
A discoverModuleClasses() 0 23 5
A listDependencies() 0 10 2
A getInstalled() 0 10 2
A satisfyDependencies() 0 24 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\Model\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
	public static function isInstalled(string $classOrAlias): bool
24
	{
25
		return (bool) self::getClass($classOrAlias, true);
26
	}
27
	
28
	/**
29
	 * Check if a module is available providing an alias or class name
30
	 */
31
	public static function isAvailable(string $classOrAlias): bool
32
	{
33
		return class_exists(self::getClass($classOrAlias));
0 ignored issues
show
Bug introduced by
It seems like self::getClass($classOrAlias) can also be of type null; however, parameter $class of class_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

33
		return class_exists(/** @scrutinizer ignore-type */ self::getClass($classOrAlias));
Loading history...
34
	}
35
36
	/**
37
	 * Get the module core class from class or alias
38
	 */
39
	public static function getClass(string $classOrAlias, bool $installedOnly = false): ?string
40
	{
41
		$modules = $installedOnly? self::getInstalled(): self::getAll();
42
43
		if (collect($modules)->contains($classOrAlias)) {
44
			return $classOrAlias;
45
		}
46
		
47
		return $modules[$classOrAlias] ?? null;
48
	}
49
	
50
	/**
51
	 * Get a collection of installed modules in alias -> class pairs
52
	 */
53
	public static function getInstalled(): array
54
	{
55
		return self::$installed = self::$installed ?? self::getCached('epesi-modules-installed', function() {
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::installed =...ion(...) { /* ... */ }) could return the type Illuminate\Support\Collection which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
56
			try {
57
			    $installedModules = Module::pluck('class', 'alias');
58
			} catch (\Exception $e) {
59
				$installedModules = collect();
60
			}
61
			
62
			return $installedModules->all();
63
		});
64
	}
65
	
66
	/**
67
	 * Get a collection of all manifested modules in alias -> class pairs
68
	 */
69
	public static function getAll(): array
70
	{
71
		return self::getCached('epesi-modules-available', function () {
72
			$modules = collect();
73
			foreach (array_merge(config('epesi.modules', []), self::packageManifest()->modules()?: []) as $namespace => $path) {
74
				foreach (self::discoverModuleClasses($namespace, $path) as $moduleClass) {
75
					$modules->add(['alias' => $moduleClass::alias(), 'class' => $moduleClass]);
76
				}
77
			}
78
79
			return $modules->pluck('class', 'alias')->all();
80
		});
81
	}
82
	
83
	/**
84
	 * Scans the profided $basePath directrory recursively to locate modules
85
	 * A directory is considered having a module when it has a file descendant of ModuleCore
86
	 * having the directory name with 'Core' suffix, e.g Test -> TestCore extends ModuleCore
87
	 */
88
	protected static function discoverModuleClasses(string $namespace, string $basePath): array
89
	{
90
	    $ret = collect();
91
	    
92
	    $moduleNamespace = trim($namespace, '\\');
93
	    
94
	    $names = array_slice(explode('\\', $moduleNamespace), - 1);
95
	    
96
	    if ($name = $names? reset($names): '') {
97
	        $moduleClass = $moduleNamespace . '\\' . $name . 'Core';
98
	        
99
	        if (is_subclass_of($moduleClass, ModuleCore::class)) {
100
	            $ret->add($moduleClass);
101
	        }
102
	    }
103
	    
104
	    foreach (glob($basePath . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $path) {
105
	        $subModuleNamespace = $moduleNamespace . '\\' . basename($path);
106
	        
107
	        $ret = $ret->merge(self::discoverModuleClasses($subModuleNamespace, $path));
108
	    }
109
	    
110
	    return $ret->all();
111
	}
112
	
113
	/**
114
	 * Common method to use for caching of data within module manager
115
116
	 * @return mixed
117
	 */
118
	protected static function getCached(string $key, \Closure $default)
119
	{
120
		if (! Cache::has($key)) {
121
			Cache::forever($key, $default());
122
		}
123
124
		return Cache::get($key);
125
	}
126
	
127
	/**
128
	 * Clear module manager cache
129
	 */
130
	public static function clearCache()
131
	{
132
		self::$installed = null;
133
		File::cleanDirectory(base_path('bootstrap/cache'));
134
		
135
		Cache::forget('epesi-modules-installed');
136
		Cache::forget('epesi-modules-available');
137
	}
138
	
139
	/**
140
	 * Alias for collect when no return values expected
141
	 */
142
	public static function call(string $method, array $args = []): void
143
	{
144
		self::collect($method, $args);
145
	}
146
	
147
	/**
148
	 * Collect array of results from $method in all installed module core classes
149
	 */
150
	public static function collect(string $method, array $args = []): array
151
	{
152
		$installedModules = collect(self::getInstalled());
153
		
154
		// if epesi is not installed fake having the system module to enable its functionality
155
		if (! $installedModules->contains(\Epesi\Core\System\SystemCore::class)) {
156
			$installedModules = collect([
157
				'system' => \Epesi\Core\System\SystemCore::class
158
			]);
159
		}
160
		
161
		$ret = [];
162
		foreach ($installedModules as $module) {
163
			if (! $list = $module::$method(...$args)) continue;
164
			
165
			$ret = array_merge($ret, is_array($list)? $list: [$list]);
166
		}
167
		
168
		return $ret;
169
	}
170
171
	/**
172
	 * Install the module class provided as argument
173
	 */
174
	public static function install(string $classOrAlias, bool $installRecommended = true)
175
	{
176
		if (self::isInstalled($classOrAlias)) {
177
			print sprintf('Module "%s" already installed!', $classOrAlias);
178
			
179
			return true;
180
		}
181
		
182
		if (! $moduleClass = self::getClass($classOrAlias)) {			
183
			throw new \Exception(sprintf('Module "%s" could not be identified', $classOrAlias));
184
		}
185
		
186
		/**
187
		 * @var ModuleCore $module
188
		 */
189
		$module = new $moduleClass();
190
		
191
		$module->migrate();
192
		
193
		self::satisfyDependencies($moduleClass);
194
		
195
		try {
196
			$module->install();
197
		} catch (\Exception $exception) {
198
			$module->rollback();
199
			
200
			throw $exception;
201
		}
202
		
203
		$module->publishAssets();
204
		
205
		// update database
206
		Module::create()->insert([
207
				'class' => $moduleClass,
208
				'alias' => $module->alias()
209
		]);
210
		
211
		if ($installRecommended) {
212
			$installRecommended = is_array($installRecommended)? $installRecommended: $module->recommended();
0 ignored issues
show
introduced by
The condition is_array($installRecommended) is always false.
Loading history...
213
			
214
			foreach ($installRecommended as $recommendedModule) {
215
				try {
216
					self::install($recommendedModule);
217
				} catch (\Exception $e) {
218
					// just continue, nothing to do if module cannot be installed
219
				}
220
			}
221
		}
222
				
223
		self::clearCache();
224
		
225
		print sprintf('Module "%s" successfully installed!', $module->label());
226
		
227
		return true;
228
	}
229
	
230
	/**
231
	 * Install modules that $moduleClass requires
232
	 * Performs operation recursively for all required modules
233
	 */
234
	protected static function satisfyDependencies(string $moduleClass): bool
235
	{
236
		self::$processing[$moduleClass] = true;
237
		
238
		while ($unsatisfiedDependencies = self::unsatisfiedDependencies($moduleClass)) {
239
			$parentModule = array_shift($unsatisfiedDependencies);
240
				
241
			if (self::$processing[$parentModule]?? false) {
242
				throw new \Exception(sprintf('Cross dependency: %s', $parentModule));
243
			}
244
				
245
			if (! self::isAvailable($parentModule)) {
246
				throw new \Exception(sprintf('Module "%s" not found!', $parentModule));
247
			}
248
	
249
			print("\n\r");
250
			print sprintf('Installing required module: "%s" by "%s"', $parentModule, $moduleClass);
251
252
			self::install($parentModule);
253
		}
254
255
		unset(self::$processing[$moduleClass]);
256
		
257
		return true;
258
	}
259
	
260
	/**
261
	 * Finds modules which are required by $moduleClass but not yet installed
262
	 */
263
	protected static function unsatisfiedDependencies(string $moduleClass): array
264
	{
265
		return collect($moduleClass::requires())->diff(self::getInstalled())->filter()->all();
266
	}	
267
	
268
	/**
269
	 * Finds $moduleClass dependencies recursively (including dependencies of dependencies)
270
	 */
271
	public static function listDependencies(string $moduleClass): array
272
	{
273
		$ret = collect();
274
		foreach (collect($moduleClass::requires()) as $parentClass) {
275
			$ret->add($parentClass = self::getClass($parentClass));
276
			
277
			$ret = $ret->merge(self::listDependencies($parentClass));
0 ignored issues
show
Bug introduced by
It seems like $parentClass can also be of type null; however, parameter $moduleClass of Epesi\Core\System\Module...ger::listDependencies() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

277
			$ret = $ret->merge(self::listDependencies(/** @scrutinizer ignore-type */ $parentClass));
Loading history...
278
		}
279
		
280
		return $ret->filter()->unique()->all();
281
	}
282
	
283
	/**
284
	 * Finds $moduleClass recommended modules recursively (including recommended of recommended)
285
	 */
286
	public static function listRecommended(string $moduleClass): array
287
	{
288
		$ret = collect();
289
		foreach (collect($moduleClass::recommended()) as $childClass) {
290
			$ret->add($childClass = self::getClass($childClass));
291
			
292
			$ret = $ret->merge(self::listRecommended($childClass));
0 ignored issues
show
Bug introduced by
It seems like $childClass can also be of type null; however, parameter $moduleClass of Epesi\Core\System\Module...ager::listRecommended() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

292
			$ret = $ret->merge(self::listRecommended(/** @scrutinizer ignore-type */ $childClass));
Loading history...
293
		}
294
		
295
		return $ret->filter()->unique()->all();
296
	}
297
	
298
	/**
299
	 * Creates array of dependencies of installed modules
300
	 */
301
	public static function listDependents(): array
302
	{
303
		$ret = [];
304
		foreach (self::getInstalled() as $moduleClass) {
305
			foreach ($moduleClass::requires() as $parentClass) {
306
				$ret[$parentClass][] = $moduleClass;
307
			}
308
		}
309
		return $ret;
310
	}
311
	
312
	/**
313
	 * Runs uninstallation routine on $classOrAlias
314
	 */
315
	public static function uninstall(string $classOrAlias): bool
316
	{
317
		if (! self::isInstalled($classOrAlias)) {
318
			print sprintf('Module "%s" is not installed!', $classOrAlias);
319
			
320
			return true;
321
		}
322
		
323
		if (! $moduleClass = self::getClass($classOrAlias)) {
324
			throw new \Exception(sprintf('Module "%s" could not be identified', $classOrAlias));
325
		}
326
		
327
		foreach (self::listDependents()[$moduleClass]?? [] as $childModule) {
328
			self::uninstall($childModule);
329
		}
330
		
331
		/**
332
		 * @var ModuleCore $module
333
		 */
334
		$module = new $moduleClass();
335
		
336
		$module->rollback();
337
		
338
		try {
339
			$module->uninstall();
340
		} catch (\Exception $exception) {
341
			$module->migrate();
342
			
343
			throw $exception;
344
		}
345
		
346
		$module->unpublishAssets();
347
		
348
		// update database
349
		Module::create()->addCondition('class', $moduleClass)->tryLoadAny()->delete();
350
		
351
		self::clearCache();
352
		
353
		print sprintf('Module "%s" successfully uninstalled!', $module->label());
354
		
355
		return true;
356
	}
357
}
358