Passed
Push — master ( 35a5e3...2ddbf5 )
by Georgi
03:24
created

ModuleManager::listDependents()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 0
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Epesi\Core\System\Integration\Modules;
4
5
use Epesi\Core\System\Database\Models\Module;
6
use Illuminate\Support\Facades\Cache;
7
use Illuminate\Database\QueryException;
8
use atk4\core\Exception;
9
use Illuminate\Support\Facades\File;
10
11
class ModuleManager
12
{
13
	use Concerns\HasPackageManifest;
14
	
15
	/**
16
	 * @var \Illuminate\Support\Collection
17
	 */
18
	private static $installed;
19
	private static $processing;
20
21
	public static function isInstalled($classOrAlias)
22
	{
23
		return self::getClass($classOrAlias, true)? 1: 0;
24
	}
25
	
26
	public static function isAvailable($classOrAlias)
27
	{
28
		return class_exists(self::getClass($classOrAlias));
29
	}
30
31
	/**
32
	 * Get the module core class from class or alias
33
	 * 
34
	 * @param string $classOrAlias
35
	 * @return string;
36
	 */
37
	public static function getClass($classOrAlias, $installedOnly = false) {
38
		$modules = $installedOnly? self::getInstalled(): self::getAll();
39
		
40
		if (collect($modules)->contains($classOrAlias)) return $classOrAlias;
41
		
42
		return $modules[$classOrAlias]?? null;
43
	}
44
	
45
	/**
46
	 * Get a collection of installed modules in alias -> class pairs
47
	 * 
48
	 * @return \Illuminate\Support\Collection;
49
	 */
50
	public static function getInstalled()
51
	{
52
		return self::$installed = self::$installed?? self::getCached('epesi-modules-installed', function() {
53
			try {
54
				$installedModules = Module::pluck('class', 'alias');
55
			} catch (QueryException $e) {
56
				$installedModules = collect();
57
			}
58
			
59
			return $installedModules;
60
		});
61
	}
62
	
63
	/**
64
	 * Get a collection of all manifested modules in alias -> class pairs
65
	 * 
66
	 * @return \Illuminate\Support\Collection;
67
	 */
68
	public static function getAll()
69
	{
70
		return self::getCached('epesi-modules-available', function () {
71
			$modules = collect();
72
			foreach (array_merge(config('epesi.modules', []), self::packageManifest()->modules()?: []) as $namespace => $path) {
73
				foreach (self::discoverModuleClasses($namespace, $path) as $moduleClass) {
74
					$modules->add(['alias' => $moduleClass::alias(), 'class' => $moduleClass]);
75
				}
76
			}
77
			
78
			return $modules->pluck('class', 'alias');
79
		});
80
	}
81
	
82
	protected static function discoverModuleClasses($namespace, $basePath)
83
	{
84
		$ret = collect();
85
		foreach (glob($basePath . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $path) {
86
			$moduleNamespace = trim($namespace, '\\') . '\\' . basename($path);
87
			
88
			$ret = $ret->merge(self::discoverModuleClasses($moduleNamespace, $path));
89
			
90
			$moduleClass = $moduleNamespace . '\\' . basename($path) . 'Core';
91
			
92
			if (! is_a($moduleClass, ModuleCore::class, true)) continue;
93
			
94
			$ret->add($moduleClass);
95
		}
96
		
97
		return $ret;
98
	}
99
	
100
	/**
101
	 * Common method to use for caching of data within module manager
102
	 * 
103
	 * @param string $key
104
	 * @param \Closure $default
105
	 * @return mixed
106
	 */
107
	protected static function getCached($key, \Closure $default)
108
	{
109
		if (! Cache::has($key)) {
110
			Cache::forever($key, $default());
111
		}
112
113
		return Cache::get($key);
114
	}
115
	
116
	/**
117
	 * Clear module manager cache
118
	 */
119
	public static function clearCache()
120
	{
121
		self::$installed = null;
122
		File::delete(base_path('bootstrap/cache'));
123
		
124
		Cache::forget('epesi-modules-installed');
125
		Cache::forget('epesi-modules-available');
126
	}
127
	
128
	/**
129
	 * Alias for collect when no return values expected
130
	 *
131
	 * @param string $method
132
	 * @return array
133
	 */
134
	public static function call($method, $args = [])
135
	{
136
		return self::collect($method, $args);
137
	}
138
	
139
	/**
140
	 * Collect array of results from $method in all installed module core classes
141
	 *
142
	 * @param string $method
143
	 * @return array
144
	 */
145
	public static function collect($method, $args = [])
146
	{
147
		$args = is_array($args)? $args: [$args];
148
		
149
		$installedModules = self::getInstalled();
150
		
151
		// if epesi is not installed fake having the system module to enable its functionality
152
		if (! $installedModules->contains(\Epesi\Core\System\SystemCore::class)) {
153
			$installedModules = collect([
154
				'system' => \Epesi\Core\System\SystemCore::class
155
			]);
156
		}
157
		
158
		$ret = [];
159
		foreach ($installedModules as $module) {
160
			if (! $list = $module::$method(...$args)) continue;
161
			
162
			$ret = array_merge($ret, is_array($list)? $list: [$list]);
163
		}
164
		
165
		return $ret;
166
	}
167
168
	/**
169
	 * Install the module class provided as argument
170
	 * 
171
	 * @param string $classOrAlias
172
	 */
173
	public static function install($classOrAlias, $installRecommended = true)
174
	{
175
		if (self::isInstalled($classOrAlias)) {
176
			print ('Module "' . $classOrAlias . '" already installed!');
177
			
178
			return true;
179
		}
180
		
181
		if (! $moduleClass = self::getClass($classOrAlias)) {			
182
			throw new \Exception('Module "' . $classOrAlias . '" could not be identified');
183
		}
184
		
185
		/**
186
		 * @var ModuleCore $module
187
		 */
188
		$module = new $moduleClass();
189
		
190
		$module->migrate();
191
		
192
		self::satisfyDependencies($moduleClass);
193
		
194
		try {
195
			$module->install();
196
		} catch (\Exception $exception) {
197
			$module->rollback();
198
			
199
			throw $exception;
200
		}
201
		
202
		$module->publishAssets();
203
		
204
		// update database
205
		Module::create([
206
				'class' => $moduleClass,
207
				'alias' => $module->alias()
208
		]);
209
		
210
		if ($installRecommended) {
211
			$installRecommended = is_array($installRecommended)? $installRecommended: $module->recommended();
212
			
213
			foreach ($installRecommended as $recommendedModule) {
214
				try {
215
					self::install($recommendedModule);
216
				} catch (Exception $e) {
217
					// just continue, nothing to do if module cannot be installed
218
				}
219
			}
220
		}
221
				
222
		self::clearCache();
223
		
224
		print ('Module ' . $classOrAlias . ' successfully installed!');
225
		
226
		return true;
227
	}
228
	
229
	/**
230
	 * Install modules that $moduleClass requires
231
	 * Performs operation recursively for all required modules
232
	 * 
233
	 * @param string $moduleClass
234
	 * @throws \Exception
235
	 * @return boolean
236
	 */
237
	protected static function satisfyDependencies($moduleClass) {
238
		self::$processing[$moduleClass] = true;
239
		
240
		while ($unsatisfiedDependencies = self::unsatisfiedDependencies($moduleClass)) {
241
			$parentModule = array_shift($unsatisfiedDependencies);
242
				
243
			if (self::$processing[$parentModule]?? false) {
244
				throw new Exception('Cross dependency: '. $parentModule);
245
			}
246
				
247
			if (! self::isAvailable($parentModule)) {
248
				throw new Exception('Module not found: "' . $parentModule . '"');
249
			}
250
	
251
			print("\n\r");
252
			print('Installing required module: "' . $parentModule . '" by "' . $moduleClass . '"');
253
254
			self::install($parentModule);
255
		}
256
257
		unset(self::$processing[$moduleClass]);
258
		
259
		return true;
260
	}
261
	
262
	protected static function unsatisfiedDependencies($moduleClass) {
263
		return collect($moduleClass::requires())->diff(self::getInstalled())->filter()->all();
264
	}	
265
	
266
	public static function listDependencies($moduleClass) {
267
		$ret = collect();
268
		foreach (collect($moduleClass::requires()) as $parentClass) {
269
			$ret->add($parentClass = self::getClass($parentClass));
270
			
271
			$ret = $ret->merge(self::listDependencies($parentClass));
272
		}
273
		
274
		return $ret->filter()->unique()->all();
275
	}
276
	
277
	public static function listRecommended($moduleClass) {
278
		$ret = collect();
279
		foreach (collect($moduleClass::recommended()) as $childClass) {
280
			$ret->add($childClass = self::getClass($childClass));
281
			
282
			$ret = $ret->merge(self::listRecommended($childClass));
283
		}
284
		
285
		return $ret->filter()->unique()->all();
286
	}
287
	
288
	public static function listDependents() {
289
		$ret = [];
290
		foreach (self::getInstalled() as $moduleClass) {
291
			foreach ($moduleClass::requires() as $parentClass) {
292
				$ret[$parentClass][] = $moduleClass;
293
			}
294
		}
295
		return $ret;
296
	}
297
	
298
	public static function uninstall($classOrAlias)
299
	{
300
		if (! self::isInstalled($classOrAlias)) {
301
			print ('Module "' . $classOrAlias . '" is not installed!');
302
			
303
			return true;
304
		}
305
		
306
		if (! $moduleClass = self::getClass($classOrAlias)) {
307
			throw new \Exception('Module "' . $classOrAlias . '" could not be identified');
308
		}
309
		
310
		foreach (self::listDependents()[$moduleClass]?? [] as $childModule) {
311
			self::uninstall($childModule);
312
		}
313
		
314
		/**
315
		 * @var ModuleCore $module
316
		 */
317
		$module = new $moduleClass();
318
		
319
		$module->rollback();
320
		
321
		try {
322
			$module->uninstall();
323
		} catch (\Exception $exception) {
324
			$module->migrate();
325
			
326
			throw $exception;
327
		}
328
		
329
		$module->unpublishAssets();
330
		
331
		// update database
332
		Module::where('class', $moduleClass)->delete();
333
		
334
		self::clearCache();
335
		
336
		print ('Module ' . $classOrAlias . ' successfully uninstalled!');
337
		
338
		return true;
339
	}
340
}
341