Completed
Push — master ( 7992f0...f74224 )
by Nazar
04:19
created

Packages_dependencies::is_used_by()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

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