Packages_dependencies::check_requirement()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
ccs 7
cts 7
cp 1
crap 2
1
<?php
2
/**
3
 * @package    CleverStyle Framework
4
 * @subpackage System module
5
 * @category   modules
6
 * @author     Nazar Mokrynskyi <[email protected]>
7
 * @license    0BSD
8
 */
9
namespace cs\modules\System;
10
use
11
	cs\Config,
12
	cs\Config\Module_Properties,
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 3
	public static function get_dependencies ($meta, $update = false) {
28
		/**
29
		 * No `meta.json` - nothing to check, allow it
30
		 */
31 3
		if (!$meta) {
32 3
			return [];
33
		}
34 3
		$meta         = self::normalize_meta($meta);
35 3
		$Config       = Config::instance();
36
		$dependencies = [
37 3
			'provide'  => [],
38
			'require'  => [],
39
			'conflict' => []
40
		];
41
		/**
42
		 * Check for compatibility with modules
43
		 */
44 3
		foreach (array_keys($Config->components['modules']) as $module) {
45
			/**
46
			 * If module uninstalled - we do not care about it
47
			 */
48 3
			if ($Config->module($module)->uninstalled()) {
49 3
				continue;
50
			}
51
			/**
52
			 * Stub for the case if there is no `meta.json`
53
			 */
54
			$module_meta = [
55 3
				'package'  => $module,
56 3
				'category' => 'modules',
57 3
				'version'  => 0
58
			];
59 3
			if (file_exists(MODULES."/$module/meta.json")) {
60 3
				$module_meta = file_get_json(MODULES."/$module/meta.json");
61
			}
62 3
			self::common_checks($dependencies, $meta, $module_meta, $update);
63
		}
64 3
		unset($module, $module_meta);
65
		/**
66
		 * If some required packages still missing
67
		 */
68 3
		foreach ($meta['require'] as $package => $details) {
69 3
			$dependencies['require'][] = [
70 3
				'package'  => $package,
71 3
				'required' => $details
72
			];
73
		}
74 3
		unset($package, $details);
75 3
		if (!self::check_dependencies_db($meta['db_support'])) {
76 3
			$dependencies['db_support'] = $meta['db_support'];
77
		}
78 3
		if (!self::check_dependencies_storage($meta['storage_support'])) {
79 3
			$dependencies['storage_support'] = $meta['storage_support'];
80
		}
81 3
		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 3
	protected static function common_checks (&$dependencies, &$meta, $component_meta, $update) {
90 3
		$component_meta = self::normalize_meta($component_meta);
91 3
		$package        = $component_meta['package'];
92
		/**
93
		 * Do not compare component with itself
94
		 */
95 3
		if (self::check_dependencies_are_the_same($meta, $component_meta)) {
96 3
			if (version_compare($meta['version'], $component_meta['version'], '<')) {
97 3
				$dependencies['update_older'] = [
98 3
					'from' => $component_meta['version'],
99 3
					'to'   => $meta['version']
100
				];
101 3
				return;
102 3
			} elseif ($update && $meta['version'] == $component_meta['version']) {
103 3
				$dependencies['update_same'] = $meta['version'];
104 3
				return;
105
			}
106
			/**
107
			 * If update is supported - check whether update is possible from current version
108
			 */
109
			if (
110 3
				isset($meta['update_from_version']) &&
111 3
				version_compare($meta['update_from_version'], $component_meta['version'], '>')
112
			) {
113 3
				$dependencies['update_from'] = [
114 3
					'from'            => $component_meta['version'],
115 3
					'to'              => $meta['version'],
116 3
					'can_update_from' => $meta['update_from_version']
117
				];
118
			}
119 3
			return;
120
		}
121
		/**
122
		 * If component already provides the same functionality
123
		 */
124 3
		if ($already_provided = self::also_provided_by($meta, $component_meta)) {
125 3
			$dependencies['provide'][] = [
126 3
				'package'  => $package,
127 3
				'features' => $already_provided
128
			];
129
		}
130
		/**
131
		 * Check if component is required and satisfies requirement condition
132
		 */
133 3
		if ($dependencies_conflicts = self::check_requirement($meta, $component_meta)) {
134 3
			array_push($dependencies['require'], ...$dependencies_conflicts);
0 ignored issues
show
Bug introduced by
$dependencies_conflicts is expanded, but the parameter $var of array_push() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

134
			array_push($dependencies['require'], /** @scrutinizer ignore-type */ ...$dependencies_conflicts);
Loading history...
135
		}
136 3
		unset($meta['require'][$package]);
137
		/**
138
		 * Satisfy provided required functionality
139
		 */
140 3
		foreach ($component_meta['provide'] as $p) {
141 3
			unset($meta['require'][$p]);
142
		}
143
		/**
144
		 * Check for conflicts
145
		 */
146 3
		if ($dependencies_conflicts = self::check_conflicts($meta, $component_meta)) {
147 3
			array_push($dependencies['conflict'], ...$dependencies_conflicts);
148
		}
149 3
	}
150
	/**
151
	 * Check whether there is available supported DB driver
152
	 *
153
	 * @param string[] $db_support
154
	 *
155
	 * @return bool
156
	 */
157 3
	protected static function check_dependencies_db ($db_support) {
158
		/**
159
		 * Component doesn't support (and thus use) any DB drivers, so we don't care what system have
160
		 */
161 3
		if (!$db_support) {
162 3
			return true;
163
		}
164 3
		$Core   = Core::instance();
165 3
		$Config = Config::instance();
166 3
		if (in_array($Core->db_driver, $db_support)) {
167 3
			return true;
168
		}
169 3
		foreach ($Config->db as $database) {
170 3
			if (isset($database['driver']) && in_array($database['driver'], $db_support)) {
171 3
				return true;
172
			}
173
		}
174 3
		return false;
175
	}
176
	/**
177
	 * Check whether there is available supported Storage driver
178
	 *
179
	 * @param string[] $storage_support
180
	 *
181
	 * @return bool
182
	 */
183 3
	protected static function check_dependencies_storage ($storage_support) {
184
		/**
185
		 * Component doesn't support (and thus use) any Storage drivers, so we don't care what system have
186
		 */
187 3
		if (!$storage_support) {
188 3
			return true;
189
		}
190 3
		$Core   = Core::instance();
191 3
		$Config = Config::instance();
192 3
		if (in_array($Core->storage_driver, $storage_support)) {
193 3
			return true;
194
		}
195 3
		foreach ($Config->storage as $storage) {
196 3
			if (in_array($storage['driver'], $storage_support)) {
197 3
				return true;
198
			}
199
		}
200 3
		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 3
	protected static function check_dependencies_are_the_same ($new_meta, $existing_meta) {
211
		return
212 3
			$new_meta['package'] == $existing_meta['package'] &&
213 3
			$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 3
	protected static function also_provided_by ($new_meta, $existing_meta) {
224 3
		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 3
	protected static function check_requirement ($new_meta, $existing_meta) {
235 3
		$conflicts = self::check_conflicts_or_requirements($new_meta['require'], $existing_meta['package'], $existing_meta['version'], false);
236 3
		foreach ($conflicts as &$conflict) {
237
			$conflict = [
238 3
				'package'          => $existing_meta['package'],
239 3
				'existing_version' => $existing_meta['version'],
240 3
				'required_version' => $conflict
241
			];
242
		}
243 3
		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 3
	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 3
		if (!isset($requirements[$component])) {
260 3
			return [];
261
		}
262
		/**
263
		 * Otherwise compare required version with actual present
264
		 */
265 3
		$conflicts = [];
266 3
		foreach ($requirements[$component] as $details) {
267 3
			if (version_compare($version, $details[1], $details[0]) === $should_satisfy) {
268 3
				$conflicts[] = $details;
269
			}
270
		}
271 3
		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 3
	protected static function check_conflicts ($new_meta, $existing_meta) {
282
		/**
283
		 * Check whether two components conflict in any direction by direct conflicts
284
		 */
285 3
		return array_filter(
286 3
			array_merge(
287 3
				self::conflicts_one_step($new_meta, $existing_meta),
288 3
				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 3
	protected static function conflicts_one_step ($meta_from, $meta_to) {
299 3
		$conflicts = self::check_conflicts_or_requirements($meta_from['conflict'], $meta_to['package'], $meta_to['version'], true);
300 3
		if ($conflicts) {
301 3
			foreach ($conflicts as &$conflict) {
302
				$conflict = [
303 3
					'package'        => $meta_from['package'],
304 3
					'conflicts_with' => $meta_to['package'],
305 3
					'of_version'     => $conflict
306
				];
307
			}
308 3
			return $conflicts;
309
		}
310 3
		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 3
	public static function get_dependent_packages ($meta) {
320
		/**
321
		 * No `meta.json` - nothing to check, allow it
322
		 */
323 3
		if (!$meta) {
324 3
			return [];
325
		}
326 3
		$meta    = self::normalize_meta($meta);
327 3
		$used_by = [];
328
		/**
329
		 * Checking for backward dependencies of modules
330
		 */
331 3
		foreach (Config::instance()->components['modules'] as $module => $module_data) {
332 3
			if (self::is_used_by($meta, $module, $module_data['active'])) {
333 3
				$used_by[] = $module;
334
			}
335
		}
336 3
		return $used_by;
337
	}
338
	/**
339
	 * @param array  $meta
340
	 * @param string $module
341
	 * @param int    $active
342
	 *
343
	 * @return bool
344
	 */
345 3
	protected static function is_used_by ($meta, $module, $active) {
346
		/**
347
		 * If module is not enabled, we compare module with itself or there is no `meta.json` - we do not care about it
348
		 */
349
		if (
350 3
			$active != Module_Properties::ENABLED ||
351 3
			self::check_dependencies_are_the_same($meta, ['category' => 'modules', 'package' => $module]) ||
352 3
			!file_exists(MODULES."/$module/meta.json")
353
		) {
354 3
			return false;
355
		}
356 3
		$module_meta = file_get_json(MODULES."/$module/meta.json");
357 3
		$module_meta = self::normalize_meta($module_meta);
358
		/**
359
		 * Check if component provided something important here
360
		 */
361
		return
362 3
			isset($module_meta['require'][$meta['package']]) ||
363 3
			array_intersect(array_keys($module_meta['require']), $meta['provide']);
364
	}
365
	/**
366
	 * Normalize structure of `meta.json`
367
	 *
368
	 * 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
369
	 *
370
	 * @param array $meta
371
	 *
372
	 * @return array mixed
373
	 */
374 3
	protected static function normalize_meta ($meta) {
375 3
		foreach (['db_support', 'storage_support', 'provide', 'require', 'conflict'] as $item) {
376 3
			$meta[$item] = isset($meta[$item]) ? (array)$meta[$item] : [];
377
		}
378 3
		foreach (['require', 'conflict'] as $item) {
379 3
			$meta[$item] = self::dep_normal($meta[$item]);
380
		}
381 3
		return $meta;
382
	}
383
	/**
384
	 * Function for normalization of dependencies structure
385
	 *
386
	 * @param array|string $dependence_structure
387
	 *
388
	 * @return array
389
	 */
390 3
	protected static function dep_normal ($dependence_structure) {
391 3
		$return = [];
392 3
		foreach ((array)$dependence_structure as $d) {
393 3
			preg_match('/^([^<=>!]+)([<=>!]*)(.*)$/', $d, $d);
394
			/** @noinspection NestedTernaryOperatorInspection */
395 3
			$return[$d[1]][] = [
396 3
				isset($d[2]) && $d[2] ? $d[2] : (isset($d[3]) && $d[3] ? '=' : '>='),
397 3
				isset($d[3]) && $d[3] ? (float)$d[3] : 0
398
			];
399
		}
400 3
		return $return;
401
	}
402
}
403