Passed
Push — master ( 992e52...dc2c5c )
by Georgi
03:52
created

ModuleManager::install()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 49
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 24
c 2
b 0
f 0
nc 6
nop 1
dl 0
loc 49
rs 8.9137
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
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
	public static function isInstalled($classOrAlias)
21
	{
22
		return self::getClass($classOrAlias, true)? 1: 0;
23
	}
24
	
25
	public static function isAvailable($classOrAlias)
26
	{
27
		return class_exists(self::getClass($classOrAlias));
28
	}
29
30
	/**
31
	 * Get the module core class from class or alias
32
	 * 
33
	 * @param string $classOrAlias
34
	 * @return string;
35
	 */
36
	public static function getClass($classOrAlias, $installedOnly = false) {
37
		$modules = $installedOnly? self::getInstalled(): self::getAll();
38
		
39
		if (collect($modules)->contains($classOrAlias)) return $classOrAlias;
40
		
41
		return $modules[$classOrAlias]?? null;
42
	}
43
	
44
	/**
45
	 * Get a collection of installed modules in alias -> class pairs
46
	 * 
47
	 * @return \Illuminate\Support\Collection;
48
	 */
49
	public static function getInstalled()
50
	{
51
		return self::$installed = self::$installed?? self::getCached('epesi-modules-installed', function() {
52
			try {
53
				$installedModules = Module::pluck('class', 'alias');
54
			} catch (QueryException $e) {
55
				$installedModules = collect();
56
			}
57
			
58
			return $installedModules;
59
		});
60
	}
61
	
62
	/**
63
	 * Get a collection of all manifested modules in alias -> class pairs
64
	 * 
65
	 * @return \Illuminate\Support\Collection;
66
	 */
67
	public static function getAll()
68
	{
69
		return self::getCached('epesi-modules-available', function () {
70
			$modules = collect();
71
			foreach (array_merge(config('epesi.modules', []), self::packageManifest()->modules()?: []) as $moduleClass) {
72
				$modules->add(['alias' => $moduleClass::alias(), 'class' => $moduleClass]);
73
			}
74
75
			return $modules->pluck('class', 'alias');
76
		});
77
	}
78
	
79
	/**
80
	 * Common method to use for caching of data within module manager
81
	 * 
82
	 * @param string $key
83
	 * @param \Closure $default
84
	 * @return mixed
85
	 */
86
	protected static function getCached($key, \Closure $default)
87
	{
88
		if (! Cache::has($key)) {
89
			Cache::forever($key, $default());
90
		}
91
92
		return Cache::get($key);
93
	}
94
	
95
	/**
96
	 * Clear module manager cache
97
	 */
98
	public static function clearCache()
99
	{
100
		self::$installed = null;
101
		Cache::forget('epesi-modules-installed');
102
		Cache::forget('epesi-modules-available');
103
	}
104
	
105
	/**
106
	 * Alias for collect when no return values expected
107
	 *
108
	 * @param string $method
109
	 * @return array
110
	 */
111
	public static function call($method, $args = [])
112
	{
113
		return self::collect($method, $args);
114
	}
115
	
116
	/**
117
	 * Collect array of results from $method in all installed module core classes
118
	 *
119
	 * @param string $method
120
	 * @return array
121
	 */
122
	public static function collect($method, $args = [])
123
	{
124
		$args = is_array($args)? $args: [$args];
125
		
126
		$installedModules = self::getInstalled();
127
		
128
		// if epesi is not installed fake having the system module to enable its functionality
129
		if ($installedModules->isEmpty()) {
130
			$installedModules = collect([
131
				'system' => \Epesi\Core\System\SystemCore::class
132
			]);
133
		}
134
		
135
		$ret = [];
136
		foreach ($installedModules as $module) {
137
			if (! $list = $module::$method(...$args)) continue;
138
			
139
			$ret = array_merge($ret, is_array($list)? $list: [$list]);
140
		}
141
		
142
		return $ret;
143
	}
144
145
	/**
146
	 * Install the module class provided as argument
147
	 * 
148
	 * @param string $classOrAlias
149
	 */
150
	public static function install($classOrAlias)
151
	{
152
		if (self::isInstalled($classOrAlias)) {
153
			print ('Module "' . $classOrAlias . '" already installed!');
154
			
155
			return true;
156
		}
157
		
158
		if (! $moduleClass = self::getClass($classOrAlias)) {			
159
			throw new \Exception('Module "' . $classOrAlias . '" could not be identified');
160
		}
161
		
162
		/**
163
		 * @var ModuleCore $module
164
		 */
165
		$module = new $moduleClass();
166
		
167
		$module->migrate();
168
		
169
		self::satisfyDependencies($moduleClass);
170
		
171
		try {
172
			$module->install();
173
		} catch (\Exception $exception) {
174
			$module->rollback();
175
			
176
			throw $exception;
177
		}
178
		
179
		$module->publishAssets();
180
		
181
		// update database
182
		Module::create([
183
				'class' => $moduleClass,
184
				'alias' => $module->alias()
185
		]);
186
		
187
		foreach ($module->recommended() as $recommendedModule) {
188
			try {
189
				self::install($recommendedModule);
190
			} catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
191
			}			
192
		}
193
		
194
		self::clearCache();
195
		
196
		print ('Module ' . $classOrAlias . ' successfully installed!');
197
		
198
		return true;
199
	}
200
	
201
	/**
202
	 * Install modules that $moduleClass requires
203
	 * Performs operation recursively for all required modules
204
	 * 
205
	 * @param string $moduleClass
206
	 * @throws \Exception
207
	 * @return boolean
208
	 */
209
	protected static function satisfyDependencies($moduleClass) {
210
		self::$processing[$moduleClass] = true;
211
		
212
		while ($unsatisfiedDependencies = self::unsatisfiedDependencies($moduleClass)) {
213
			$parentModule = array_shift($unsatisfiedDependencies);
214
				
215
			if (self::$processing[$parentModule]?? false) {
216
				throw new Exception('Cross dependency: '. $parentModule);
217
			}
218
				
219
			if (! self::isAvailable($parentModule)) {
220
				throw new Exception('Module not found: "' . $parentModule . '"');
221
			}
222
	
223
			print("\n\r");
224
			print('Installing required module: "' . $parentModule . '" by "' . $moduleClass . '"');
225
226
			self::install($parentModule);
227
		}
228
229
		unset(self::$processing[$moduleClass]);
230
		
231
		return true;
232
	}
233
	
234
	protected static function unsatisfiedDependencies($moduleClass) {
235
		return collect($moduleClass::requires())->diff(self::getInstalled())->filter()->all();
236
	}	
237
	
238
	public static function uninstall($classOrAlias)
239
	{
240
		if (! self::isInstalled($classOrAlias)) {
241
			print ('Module "' . $classOrAlias . '" is not installed!');
242
			
243
			return true;
244
		}
245
		
246
		if (! $moduleClass = self::getClass($classOrAlias)) {
247
			throw new \Exception('Module "' . $classOrAlias . '" could not be identified');
248
		}
249
		
250
		/**
251
		 * @var ModuleCore $module
252
		 */
253
		$module = new $moduleClass();
254
		
255
		$module->rollback();
256
		
257
		try {
258
			$module->uninstall();
259
		} catch (\Exception $exception) {
260
			$module->migrate();
261
			
262
			throw $exception;
263
		}
264
		
265
		$module->unpublishAssets();
266
		
267
		// update database
268
		Module::where('class', $moduleClass)->delete();
269
		
270
		self::clearCache();
271
		
272
		print ('Module ' . $classOrAlias . ' successfully uninstalled!');
273
		
274
		return true;
275
	}
276
}
277