Completed
Push — master ( 2db1fb...5408f9 )
by Nazar
04:38
created

Packages_dependencies::conflicts_one_step()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 10
nc 3
nop 2
dl 0
loc 14
ccs 9
cts 9
cp 1
crap 3
rs 9.4285
c 1
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\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 2
	public static function get_dependencies ($meta, $update = false) {
28
		/**
29
		 * No `meta.json` - nothing to check, allow it
30
		 */
31 2
		if (!$meta) {
32 2
			return [];
33
		}
34 2
		$meta         = self::normalize_meta($meta);
35 2
		$Config       = Config::instance();
36
		$dependencies = [
37 2
			'provide'  => [],
38
			'require'  => [],
39
			'conflict' => []
40
		];
41
		/**
42
		 * Check for compatibility with modules
43
		 */
44 2
		foreach (array_keys($Config->components['modules']) as $module) {
45
			/**
46
			 * If module uninstalled - we do not care about it
47
			 */
48 2
			if ($Config->module($module)->uninstalled()) {
49 2
				continue;
50
			}
51
			/**
52
			 * Stub for the case if there is no `meta.json`
53
			 */
54
			$module_meta = [
55 2
				'package'  => $module,
56 2
				'category' => 'modules',
57 2
				'version'  => 0
58
			];
59 2
			if (file_exists(MODULES."/$module/meta.json")) {
60 2
				$module_meta = file_get_json(MODULES."/$module/meta.json");
61
			}
62 2
			self::common_checks($dependencies, $meta, $module_meta, $update);
63
		}
64 2
		unset($module, $module_meta);
65
		/**
66
		 * If some required packages still missing
67
		 */
68 2
		foreach ($meta['require'] as $package => $details) {
69 2
			$dependencies['require'][] = [
70 2
				'package'  => $package,
71 2
				'required' => $details
72
			];
73
		}
74 2
		unset($package, $details);
75 2
		if (!self::check_dependencies_db($meta['db_support'])) {
76 2
			$dependencies['db_support'] = $meta['db_support'];
77
		}
78 2
		if (!self::check_dependencies_storage($meta['storage_support'])) {
79 2
			$dependencies['storage_support'] = $meta['storage_support'];
80
		}
81 2
		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 2
	protected static function common_checks (&$dependencies, &$meta, $component_meta, $update) {
90 2
		$component_meta = self::normalize_meta($component_meta);
91 2
		$package        = $component_meta['package'];
92
		/**
93
		 * Do not compare component with itself
94
		 */
95 2
		if ($update && self::check_dependencies_are_the_same($meta, $component_meta)) {
96 2
			if (version_compare($meta['version'], $component_meta['version'], '<')) {
97 2
				$dependencies['update_older'] = [
98 2
					'from' => $component_meta['version'],
99 2
					'to'   => $meta['version']
100
				];
101 2
				return;
102 2
			} elseif ($meta['version'] == $component_meta['version']) {
103 2
				$dependencies['update_same'] = $meta['version'];
104 2
				return;
105
			}
106
			/**
107
			 * If update is supported - check whether update is possible from current version
108
			 */
109
			if (
110 2
				isset($meta['update_from_version']) &&
111 2
				version_compare($meta['update_from_version'], $component_meta['version'], '>')
112
			) {
113 2
				$dependencies['update_from'] = [
114 2
					'from'            => $component_meta['version'],
115 2
					'to'              => $meta['version'],
116 2
					'can_update_from' => $meta['update_from_version']
117
				];
118
			}
119 2
			return;
120
		}
121
		/**
122
		 * If component already provides the same functionality
123
		 */
124 2
		if ($already_provided = self::also_provided_by($meta, $component_meta)) {
125 2
			$dependencies['provide'][] = [
126 2
				'package'  => $package,
127 2
				'features' => $already_provided
128
			];
129
		}
130
		/**
131
		 * Check if component is required and satisfies requirement condition
132
		 */
133 2
		if ($dependencies_conflicts = self::check_requirement($meta, $component_meta)) {
134 2
			array_push($dependencies['require'], ...$dependencies_conflicts);
135
		}
136 2
		unset($meta['require'][$package]);
137
		/**
138
		 * Satisfy provided required functionality
139
		 */
140 2
		foreach ($component_meta['provide'] as $p) {
141 2
			unset($meta['require'][$p]);
142
		}
143
		/**
144
		 * Check for conflicts
145
		 */
146 2
		if ($dependencies_conflicts = self::check_conflicts($meta, $component_meta)) {
147 2
			array_push($dependencies['conflict'], ...$dependencies_conflicts);
148
		}
149 2
	}
150
	/**
151
	 * Check whether there is available supported DB engine
152
	 *
153
	 * @param string[] $db_support
154
	 *
155
	 * @return bool
156
	 */
157 2
	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 2
		if (!$db_support) {
162 2
			return true;
163
		}
164 2
		$Core   = Core::instance();
165 2
		$Config = Config::instance();
166 2
		if (in_array($Core->db_type, $db_support)) {
167 2
			return true;
168
		}
169 2
		foreach ($Config->db as $database) {
170 2
			if (isset($database['type']) && in_array($database['type'], $db_support)) {
171 2
				return true;
172
			}
173
		}
174 2
		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 2
	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 2
		if (!$storage_support) {
188 2
			return true;
189
		}
190 2
		$Core   = Core::instance();
191 2
		$Config = Config::instance();
192 2
		if (in_array($Core->storage_type, $storage_support)) {
193 2
			return true;
194
		}
195 2
		foreach ($Config->storage as $storage) {
196 2
			if (in_array($storage['connection'], $storage_support)) {
197 2
				return true;
198
			}
199
		}
200 2
		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 2
	protected static function check_dependencies_are_the_same ($new_meta, $existing_meta) {
211
		return
212 2
			$new_meta['package'] == $existing_meta['package'] &&
213 2
			$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 2
	protected static function also_provided_by ($new_meta, $existing_meta) {
224 2
		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 2
	protected static function check_requirement ($new_meta, $existing_meta) {
235 2
		$conflicts = self::check_conflicts_or_requirements($new_meta['require'], $existing_meta['package'], $existing_meta['version'], false);
236 2
		foreach ($conflicts as &$conflict) {
237
			$conflict = [
238 2
				'package'          => $existing_meta['package'],
239 2
				'existing_version' => $existing_meta['version'],
240 2
				'required_version' => $conflict
241
			];
242
		}
243 2
		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 2
	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 2
		if (!isset($requirements[$component])) {
260 2
			return [];
261
		}
262
		/**
263
		 * Otherwise compare required version with actual present
264
		 */
265 2
		$conflicts = [];
266 2
		foreach ($requirements[$component] as $details) {
267 2
			if (version_compare($version, $details[1], $details[0]) === $should_satisfy) {
268 2
				$conflicts[] = $details;
269
			}
270
		}
271 2
		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 2
	protected static function check_conflicts ($new_meta, $existing_meta) {
282
		/**
283
		 * Check whether two components conflict in any direction by direct conflicts
284
		 */
285 2
		return array_filter(
286
			array_merge(
287 2
				self::conflicts_one_step($new_meta, $existing_meta),
288 2
				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 2
	protected static function conflicts_one_step ($meta_from, $meta_to) {
299 2
		$conflicts = self::check_conflicts_or_requirements($meta_from['conflict'], $meta_to['package'], $meta_to['version'], true);
300 2
		if ($conflicts) {
301 2
			foreach ($conflicts as &$conflict) {
302
				$conflict = [
303 2
					'package'        => $meta_from['package'],
304 2
					'conflicts_with' => $meta_to['package'],
305 2
					'of_version'     => $conflict
306
				];
307
			}
308 2
			return $conflicts;
309
		}
310 2
		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 2
	public static function get_dependent_packages ($meta) {
320
		/**
321
		 * No `meta.json` - nothing to check, allow it
322
		 */
323 2
		if (!$meta) {
324 2
			return [];
325
		}
326 2
		$meta    = self::normalize_meta($meta);
327 2
		$Config  = Config::instance();
328 2
		$used_by = [];
329
		/**
330
		 * Checking for backward dependencies of modules
331
		 */
332 2
		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 2
					$meta['category'] == 'modules' &&
339 2
					$meta['package'] == $module
340
				) ||
341 2
				!file_exists(MODULES."/$module/meta.json") ||
342 2
				!$Config->module($module)->enabled()
343
			) {
344 2
				continue;
345
			}
346 2
			$module_meta = file_get_json(MODULES."/$module/meta.json");
347 2
			$module_meta = self::normalize_meta($module_meta);
348
			/**
349
			 * Check if component provided something important here
350
			 */
351
			if (
352 2
				isset($module_meta['require'][$meta['package']]) ||
353 2
				array_intersect(array_keys($module_meta['require']), $meta['provide'])
354
			) {
355 2
				$used_by[] = $module;
356
			}
357
		}
358 2
		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 2
	protected static function normalize_meta ($meta) {
370 2
		foreach (['db_support', 'storage_support', 'provide', 'require', 'conflict'] as $item) {
371 2
			$meta[$item] = isset($meta[$item]) ? (array)$meta[$item] : [];
372
		}
373 2
		foreach (['require', 'conflict'] as $item) {
374 2
			$meta[$item] = self::dep_normal($meta[$item]);
375
		}
376 2
		return $meta;
377
	}
378
	/**
379
	 * Function for normalization of dependencies structure
380
	 *
381
	 * @param array|string $dependence_structure
382
	 *
383
	 * @return array
384
	 */
385 2
	protected static function dep_normal ($dependence_structure) {
386 2
		$return = [];
387 2
		foreach ((array)$dependence_structure as $d) {
388 2
			preg_match('/^([^<=>!]+)([<=>!]*)(.*)$/', $d, $d);
389
			/** @noinspection NestedTernaryOperatorInspection */
390 2
			$return[$d[1]][] = [
391 2
				isset($d[2]) && $d[2] ? $d[2] : (isset($d[3]) && $d[3] ? '=' : '>='),
392 2
				isset($d[3]) && $d[3] ? (float)$d[3] : 0
393
			];
394
		}
395 2
		return $return;
396
	}
397
}
398