Completed
Push — master ( 67d50f...8a7d08 )
by Nazar
04:56
created

Packages_dependencies::get_dependent_packages()   D

Complexity

Conditions 9
Paths 5

Size

Total Lines 41
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 21
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 41
ccs 0
cts 29
cp 0
crap 90
rs 4.909
1
<?php
2
/**
3
 * @package    CleverStyle Framework
4
 * @subpackage System module
5
 * @category   modules
6
 * @author     Nazar Mokrynskyi <[email protected]>
7
 * @copyright  Copyright (c) 2015-2016, Nazar Mokrynskyi
8
 * @license    MIT License, see license.txt
9
 */
10
namespace cs\modules\System;
11
use
12
	cs\Config,
13
	cs\Core;
14
15
/**
16
 * Utility functions, necessary for determining package's dependencies and which packages depend on it
17
 */
18
class Packages_dependencies {
19
	/**
20
	 * Check dependencies for new component (during installation/updating/enabling)
21
	 *
22
	 * @param array $meta   `meta.json` contents of target component
23
	 * @param bool  $update Is this for updating component to newer version (`$meta` will correspond to the version that components is going to be updated to)
24
	 *
25
	 * @return array
26
	 */
27
	static function get_dependencies ($meta, $update = false) {
28
		/**
29
		 * No `meta.json` - nothing to check, allow it
30
		 */
31
		if (!$meta) {
32
			return [];
33
		}
34
		$meta         = self::normalize_meta($meta);
35
		$Config       = Config::instance();
36
		$dependencies = [];
37
		/**
38
		 * Check for compatibility with modules
39
		 */
40
		foreach (array_keys($Config->components['modules']) as $module) {
41
			/**
42
			 * If module uninstalled - we do not care about it
43
			 */
44
			if ($Config->module($module)->uninstalled()) {
45
				continue;
46
			}
47
			/**
48
			 * Stub for the case if there is no `meta.json`
49
			 */
50
			$module_meta = [
51
				'package'  => $module,
52
				'category' => 'modules',
53
				'version'  => 0
54
			];
55
			if (file_exists(MODULES."/$module/meta.json")) {
56
				$module_meta = file_get_json(MODULES."/$module/meta.json");
57
			}
58
			self::common_checks($dependencies, $meta, $module_meta, $update);
59
		}
60
		unset($module, $module_meta);
61
		/**
62
		 * If some required packages still missing
63
		 */
64
		foreach ($meta['require'] as $package => $details) {
65
			$dependencies['require']['unknown'][] = [
66
				'name'     => $package,
67
				'required' => $details
68
			];
69
		}
70
		unset($package, $details);
71
		if (!self::check_dependencies_db($meta['db_support'])) {
72
			$dependencies['db_support'] = $meta['db_support'];
73
		}
74
		if (!self::check_dependencies_storage($meta['storage_support'])) {
75
			$dependencies['storage_support'] = $meta['storage_support'];
76
		}
77
		return $dependencies;
78
	}
79
	/**
80
	 * @param array $dependencies
81
	 * @param array $meta
82
	 * @param array $component_meta
83
	 * @param bool  $update
84
	 */
85
	protected static function common_checks (&$dependencies, &$meta, $component_meta, $update) {
86
		$component_meta = self::normalize_meta($component_meta);
87
		$package        = $component_meta['package'];
88
		$category       = $component_meta['category'];
89
		/**
90
		 * Do not compare component with itself
91
		 */
92
		if (self::check_dependencies_are_the_same($meta, $component_meta)) {
93
			if (version_compare($meta['version'], $component_meta['version'], '<')) {
94
				$dependencies['update_older'][$meta['category']] = [
95
					[
96
						'from' => $component_meta['version'],
97
						'to'   => $meta['version']
98
					]
99
				];
100
				return;
101
			} elseif ($update && $meta['version'] == $component_meta['version']) {
102
				$dependencies['update_same'][$meta['category']] = [
103
					[
104
						'version' => $meta['version']
105
					]
106
				];
107
				return;
108
			}
109
			/**
110
			 * If update is supported - check whether update is possible from current version
111
			 */
112
			if (
113
				isset($meta['update_from']) &&
114
				version_compare($meta['update_from_version'], $component_meta['version'], '>')
115
			) {
116
				$dependencies['update_from'] = [
117
					'from'            => $component_meta['version'],
118
					'to'              => $meta['version'],
119
					'can_update_from' => $meta['update_from_version']
120
				];
121
			}
122
			return;
123
		}
124
		/**
125
		 * If component already provides the same functionality
126
		 */
127
		if ($already_provided = self::also_provided_by($meta, $component_meta)) {
128
			$dependencies['provide'][$category][] = [
129
				'name'     => $package,
130
				'features' => $already_provided
131
			];
132
		}
133
		/**
134
		 * Check if component is required and satisfies requirement condition
135
		 */
136
		if ($dependencies_conflicts = self::check_requirement_satisfaction($meta, $component_meta)) {
137
			$dependencies['require'][$category][] = [
138
				'name'     => $package,
139
				'existing' => $component_meta['version'],
140
				'required' => $dependencies_conflicts
141
			];
142
		}
143
		unset($meta['require'][$package]);
144
		/**
145
		 * Satisfy provided required functionality
146
		 */
147
		foreach ($component_meta['provide'] as $p) {
148
			unset($meta['require'][$p]);
149
		}
150
		/**
151
		 * Check for conflicts
152
		 */
153
		if ($dependencies_conflicts = self::conflicts($meta, $component_meta)) {
154
			$dependencies['conflict'][$category][] = [
155
				'name'      => $package,
156
				'conflicts' => $dependencies_conflicts
157
			];
158
		}
159
	}
160
	/**
161
	 * Check whether there is available supported DB engine
162
	 *
163
	 * @param string[] $db_support
164
	 *
165
	 * @return bool
166
	 */
167
	protected static function check_dependencies_db ($db_support) {
168
		/**
169
		 * Component doesn't support (and thus use) any DB engines, so we don't care what system have
170
		 */
171
		if (!$db_support) {
172
			return true;
173
		}
174
		$Core   = Core::instance();
175
		$Config = Config::instance();
176
		if (in_array($Core->db_type, $db_support)) {
177
			return true;
178
		}
179
		foreach ($Config->db as $database) {
180
			if (isset($database['type']) && in_array($database['type'], $db_support)) {
181
				return true;
182
			}
183
		}
184
		return false;
185
	}
186
	/**
187
	 * Check whether there is available supported Storage engine
188
	 *
189
	 * @param string[] $storage_support
190
	 *
191
	 * @return bool
192
	 */
193
	protected static function check_dependencies_storage ($storage_support) {
194
		/**
195
		 * Component doesn't support (and thus use) any Storage engines, so we don't care what system have
196
		 */
197
		if (!$storage_support) {
198
			return true;
199
		}
200
		$Core   = Core::instance();
201
		$Config = Config::instance();
202
		if (in_array($Core->storage_type, $storage_support)) {
203
			return true;
204
		}
205
		foreach ($Config->storage as $storage) {
206
			if (in_array($storage['connection'], $storage_support)) {
207
				return true;
208
			}
209
		}
210
		return false;
211
	}
212
	/**
213
	 * Check if two both components are the same
214
	 *
215
	 * @param array $new_meta      `meta.json` content of new component
216
	 * @param array $existing_meta `meta.json` content of existing component
217
	 *
218
	 * @return bool
219
	 */
220
	protected static function check_dependencies_are_the_same ($new_meta, $existing_meta) {
221
		return
222
			$new_meta['package'] == $existing_meta['package'] &&
223
			$new_meta['category'] == $existing_meta['category'];
224
	}
225
	/**
226
	 * Check for functionality provided by other components
227
	 *
228
	 * @param array $new_meta      `meta.json` content of new component
229
	 * @param array $existing_meta `meta.json` content of existing component
230
	 *
231
	 * @return array
232
	 */
233
	protected static function also_provided_by ($new_meta, $existing_meta) {
234
		return array_intersect($new_meta['provide'], $existing_meta['provide']);
235
	}
236
	/**
237
	 * Check whether other component is required and have satisfactory version
238
	 *
239
	 * @param array $new_meta      `meta.json` content of new component
240
	 * @param array $existing_meta `meta.json` content of existing component
241
	 *
242
	 * @return array
243
	 */
244
	protected static function check_requirement_satisfaction ($new_meta, $existing_meta) {
245
		return self::check_conflicts($new_meta['require'], $existing_meta['package'], $existing_meta['version']);
246
	}
247
	/**
248
	 * Check whether other component is required and have satisfactory version
249
	 *
250
	 * @param array  $requirements
251
	 * @param string $component
252
	 * @param string $version
253
	 *
254
	 * @return array
255
	 */
256
	protected static function check_conflicts ($requirements, $component, $version) {
257
		/**
258
		 * If we are not interested in component - we are good
259
		 */
260
		if (!isset($requirements[$component])) {
261
			return [];
262
		}
263
		/**
264
		 * Otherwise compare required version with actual present
265
		 */
266
		$conflicts = [];
267
		foreach ($requirements[$component] as $details) {
268
			if (!version_compare($version, $details[1], $details[0])) {
269
				$conflicts[] = $details;
270
			}
271
		}
272
		return $conflicts;
273
	}
274
	/**
275
	 * Check for if component conflicts other components
276
	 *
277
	 * @param array $new_meta      `meta.json` content of new component
278
	 * @param array $existing_meta `meta.json` content of existing component
279
	 *
280
	 * @return array
281
	 */
282
	protected static function conflicts ($new_meta, $existing_meta) {
283
		/**
284
		 * Check whether two components conflict in any direction by direct conflicts
285
		 */
286
		return array_filter(
287
			[
288
				self::conflicts_one_step($new_meta, $existing_meta),
289
				self::conflicts_one_step($existing_meta, $new_meta)
290
			]
291
		);
292
	}
293
	/**
294
	 * @param array $meta_from
295
	 * @param array $meta_to
296
	 *
297
	 * @return array
298
	 */
299
	protected static function conflicts_one_step ($meta_from, $meta_to) {
300
		$conflicts = self::check_conflicts($meta_from['conflict'], $meta_to['package'], $meta_to['version']);
301
		if ($conflicts) {
302
			return [
303
				'package'        => $meta_from['package'],
304
				'conflicts_with' => $meta_to['package'],
305
				'of_versions'    => $conflicts
306
			];
307
		}
308
		return [];
309
	}
310
	/**
311
	 * Check whether package is currently used by any other package (during uninstalling/disabling)
312
	 *
313
	 * @param array $meta `meta.json` contents of target component
314
	 *
315
	 * @return string[][] Empty array if dependencies are fine or array with optional key `modules` that contain array of dependent packages
316
	 */
317
	static function get_dependent_packages ($meta) {
318
		/**
319
		 * No `meta.json` - nothing to check, allow it
320
		 */
321
		if (!$meta) {
322
			return [];
323
		}
324
		$meta    = self::normalize_meta($meta);
325
		$Config  = Config::instance();
326
		$used_by = [];
327
		/**
328
		 * Checking for backward dependencies of modules
329
		 */
330
		foreach (array_keys($Config->components['modules']) as $module) {
331
			/**
332
			 * If module is not enabled, we compare module with itself or there is no `meta.json` - we do not care about it
333
			 */
334
			if (
335
				(
336
					$meta['category'] == 'modules' &&
337
					$meta['package'] == $module
338
				) ||
339
				!file_exists(MODULES."/$module/meta.json") ||
340
				!$Config->module($module)->enabled()
341
			) {
342
				continue;
343
			}
344
			$module_meta = file_get_json(MODULES."/$module/meta.json");
345
			$module_meta = self::normalize_meta($module_meta);
346
			/**
347
			 * Check if component provided something important here
348
			 */
349
			if (
350
				isset($module_meta['require'][$meta['package']]) ||
351
				array_intersect(array_keys($module_meta['require']), $meta['provide'])
352
			) {
353
				$used_by['modules'][] = $module;
354
			}
355
		}
356
		return $used_by;
357
	}
358
	/**
359
	 * Normalize structure of `meta.json`
360
	 *
361
	 * Addition necessary items if they are not present and casting some string values to arrays in order to decrease number of checks in further code
362
	 *
363
	 * @param array $meta
364
	 *
365
	 * @return array mixed
366
	 */
367
	protected static function normalize_meta ($meta) {
368
		foreach (['db_support', 'storage_support', 'provide', 'require', 'conflict'] as $item) {
369
			$meta[$item] = isset($meta[$item]) ? (array)$meta[$item] : [];
370
		}
371
		foreach (['require', 'conflict'] as $item) {
372
			$meta[$item] = self::dep_normal($meta[$item]);
373
		}
374
		return $meta;
375
	}
376
	/**
377
	 * Function for normalization of dependencies structure
378
	 *
379
	 * @param array|string $dependence_structure
380
	 *
381
	 * @return array
382
	 */
383
	protected static function dep_normal ($dependence_structure) {
384
		$return = [];
385
		foreach ((array)$dependence_structure as $d) {
386
			preg_match('/^([^<=>!]+)([<=>!]*)(.*)$/', $d, $d);
387
			/** @noinspection NestedTernaryOperatorInspection */
388
			$return[$d[1]][] = [
389
				isset($d[2]) && $d[2] ? $d[2] : (isset($d[3]) && $d[3] ? '=' : '>='),
390
				isset($d[3]) && $d[3] ? $d[3] : 0
391
			];
392
		}
393
		return $return;
394
	}
395
}
396