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