Passed
Push — master ( 42ae0f...32b24a )
by Marcin
08:00 queued 11s
created

Converter::getPrimitivesMapping()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 16
c 1
b 0
f 0
nc 5
nop 0
dl 0
loc 29
ccs 0
cts 0
cp 0
crap 56
rs 8.8333
1
<?php
2
declare(strict_types=1);
3
4
namespace MarcinOrlowski\ResponseBuilder;
5
6
/**
7
 * Laravel API Response Builder
8
 *
9
 * @package   MarcinOrlowski\ResponseBuilder
10
 *
11
 * @author    Marcin Orlowski <mail (#) marcinOrlowski (.) com>
12
 * @copyright 2016-2020 Marcin Orlowski
13
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
14
 * @link      https://github.com/MarcinOrlowski/laravel-api-response-builder
15
 */
16
17
use Illuminate\Support\Facades\Log;
18
use Illuminate\Support\Facades\Config;
19
use MarcinOrlowski\ResponseBuilder\Exceptions as Ex;
20
use MarcinOrlowski\ResponseBuilder\ResponseBuilder as RB;
21
22
/**
23
 * Data converter
24
 */
25
class Converter
26
{
27
	/** @var array */
28
	protected $classes = [];
29
30
	/** @var array */
31
	protected $primitives = [];
32
33
	/** @var bool */
34
	protected $debug_enabled = false;
35
36
	/**
37
	 * Converter constructor.
38
	 */
39 38
	public function __construct()
40
	{
41 38
		$this->classes = static::getClassesMapping() ?? [];
42
		$this->primitives = static::getPrimitivesMapping() ?? [];
43 37
44 37
		$this->debug_enabled = Config::get(RB::CONF_KEY_DEBUG_CONVERTER_DEBUG_ENABLED, false);
45
	}
46
47
	/**
48
	 * Returns "converter/primitives" entry for given primitive object or throws exception if no config found.
49
	 * Throws \RuntimeException if there's no config "classes" mapping entry for this object configured.
50
	 * Throws \InvalidArgumentException if No data conversion mapping configured for given class.
51 5
	 *
52
	 * @param boolean|string|double|array|int $data Primitive to get config for.
53 5
	 *
54
	 * @return array
55
	 *
56
	 * @throws Ex\InvalidConfigurationElementException
57
	 * @throws Ex\ConfigurationNotFoundException
58
	 */
59
	protected function getPrimitiveMappingConfigOrThrow($data): array
60
	{
61
		$result = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
62
63
		$type = \gettype($data);
64
		$result = $this->primitives[ $type ] ?? null;
65
		if (!\is_array($result) && !empty($result)) {
66
			throw new Ex\InvalidConfigurationElementException(
67
				sprintf('Invalid conversion mapping config for "%s" primitive.', $type));
68 16
		}
69
70 16
		if ($result === null) {
0 ignored issues
show
introduced by
The condition $result === null is always true.
Loading history...
71 16
			throw new Ex\ConfigurationNotFoundException(
72
				sprintf('No data conversion mapping configured for "%s" primitive.', $type));
73
		}
74 16
75 16
		if ($this->debug_enabled) {
76 16
			Log::debug(__CLASS__ . ": Converting primitive type of '{$type}' to data node '{$result[RB::KEY_KEY]}'.");
77 11
		}
78 11
79
		return $result;
80
	}
81 5
82 5
	/**
83 4
	 * Returns "converter/map" mapping configured for given $data object class or throws exception if not found.
84 4
	 *
85 4
	 * @param object $data Object to get config for.
86
	 *
87
	 * @return array
88
	 *
89
	 * @throws Ex\ConfigurationNotFoundException
90
	 */
91 16
	protected function getClassMappingConfigOrThrow(object $data): array
92 1
	{
93
		$result = null;
94
		$debug_result = '';
95 15
96
		// check for exact class name match...
97
		$cls = \get_class($data);
98
		if (\is_string($cls)) {
0 ignored issues
show
introduced by
The condition is_string($cls) is always true.
Loading history...
99 15
			if (\array_key_exists($cls, $this->classes)) {
100
				$result = $this->classes[ $cls ];
101
				$debug_result = 'exact config match';
102
			} else {
103
				// no exact match, then lets try with `instanceof`
104
				foreach (\array_keys($this->classes) as $class_name) {
105
					if ($data instanceof $class_name) {
106
						$result = $this->classes[ $class_name ];
107
						$debug_result = "subclass of {$class_name}";
108
						break;
109
					}
110
				}
111 37
			}
112
		}
113 37
114 15
		if ($result === null) {
115
			throw new Ex\ConfigurationNotFoundException(
116
				sprintf('No data conversion mapping configured for "%s" class.', $cls));
117 22
		}
118 22
119
		if ($this->debug_enabled) {
120 21
			Log::debug(__CLASS__ . ": Converting {$cls} using {$result[RB::KEY_HANDLER]} because: {$debug_result}.");
121 11
		}
122 10
123 10
		return $result;
124
	}
125 10
126
	/**
127
	 * Main entry for data conversion
128 19
	 *
129
	 * @param object|array|null $data
130
	 *
131
	 * @return mixed|null
132
	 */
133
	public function convert($data = null): ?array
134
	{
135
		if ($data === null) {
136
			return null;
137
		}
138
139
		$result = null;
140
141 10
		Validator::assertIsType('data', $data, [
142
			Type::ARRAY,
143
			Type::BOOLEAN,
144
			Type::DOUBLE,
145
			Type::INTEGER,
146 10
			Type::OBJECT,
147 10
			Type::STRING,
148 10
		]);
149 10
150 6
		if ($result === null && \is_object($data)) {
151
			$cfg = $this->getClassMappingConfigOrThrow($data);
152 6
			$worker = new $cfg[ RB::KEY_HANDLER ]();
153
			$result = [$cfg[ RB::KEY_KEY ] => $worker->convert($data, $cfg)];
154
		}
155 10
156 1
		if ($result === null && \is_array($data)) {
157
			$cfg = $this->getPrimitiveMappingConfigOrThrow($data);
158 1
159
			$result = $this->convertArray($data);
160
			if (!Util::isArrayWithNonNumericKeys($data)) {
161
				$result = [$cfg[ RB::KEY_KEY ] => $result];
162 9
			}
163 9
		}
164 4
165 9
		if (\is_bool($data) || \is_float($data) || \is_int($data) || \is_string($data)) {
0 ignored issues
show
introduced by
The condition is_string($data) is always false.
Loading history...
introduced by
The condition is_float($data) is always false.
Loading history...
introduced by
The condition is_int($data) is always false.
Loading history...
166 5
			$result = [$this->getPrimitiveMappingConfigOrThrow($data)[ RB::KEY_KEY ] => $data];
167 5
		}
168 5
169 5
		return $result;
170
	}
171
172
	/**
173 9
	 * Recursively walks $data array and converts all known objects if found. Note
174
	 * $data array is passed by reference so source $data array may be modified.
175
	 *
176
	 * @param array $data array to recursively convert known elements of
177
	 *
178
	 * @return array
179
	 */
180
	protected function convertArray(array $data): array
181
	{
182
		Validator::assertArrayHasNoMixedKeys($data);
183 42
184
		foreach ($data as $key => $val) {
185 42
			if (\is_array($val)) {
186
				$data[ $key ] = $this->convertArray($val);
187 42
			} elseif (\is_object($val)) {
188 41
				$cfg = $this->getClassMappingConfigOrThrow($val);
189 3
				$worker = new $cfg[ RB::KEY_HANDLER ]();
190 3
				$converted_data = $worker->convert($val, $cfg);
191
				$data[ $key ] = $converted_data;
192
			}
193
		}
194 38
195
		return $data;
196 38
	}
197 38
198 38
	/**
199 1
	 * Reads and validates "converter/map" config mapping
200
	 *
201
	 * @return array Classes mapping as specified in configuration or empty array if configuration found
202
	 *
203
	 * @throws Ex\InvalidConfigurationException if whole config mapping is technically invalid (i.e. not an array etc).
204 1
	 * @throws Ex\InvalidConfigurationElementException if config for specific class is technically invalid (i.e. not an array etc).
205
	 * @throws Ex\IncompleteConfigurationException if config for specific class is incomplete (misses some mandatory fields etc).
206
	 */
207 38
	protected static function getClassesMapping(): array
208
	{
209
		$classes = Config::get(RB::CONF_KEY_CONVERTER_CLASSES) ?? [];
210
211
		if (!\is_array($classes)) {
212
			throw new Ex\InvalidConfigurationException(
213
				\sprintf('"%s" must be an array (%s found)', RB::CONF_KEY_CONVERTER_CLASSES, \gettype($classes)));
214
		}
215
216
		if (!empty($classes)) {
217
			$mandatory_keys = [
218
				RB::KEY_HANDLER,
219
				RB::KEY_KEY,
220
			];
221
			foreach ($classes as $class_name => $class_config) {
222
				if (!\is_array($class_config)) {
223
					throw new Ex\InvalidConfigurationElementException(
224
						sprintf("Config for '{$class_name}' class must be an array (%s found).", \gettype($class_config)));
225
				}
226
				foreach ($mandatory_keys as $key_name) {
227
					if (!\array_key_exists($key_name, $class_config)) {
228
						throw new Ex\IncompleteConfigurationException(
229
							"Missing '{$key_name}' entry in '{$class_name}' class mapping config.");
230
					}
231
				}
232
			}
233
		}
234
235
		return $classes;
236
	}
237
238
	/**
239
	 * Reads and validates "converter/primitives" config mapping
240
	 *
241
	 * @return array Primitives mapping config as specified in configuration or empty array if configuration found
242
	 *
243
	 * @throws Ex\InvalidConfigurationException if whole config mapping is technically invalid (i.e. not an array etc).
244
	 * @throws Ex\InvalidConfigurationElementException if config for specific class is technically invalid (i.e. not an array etc).
245
	 * @throws Ex\IncompleteConfigurationException if config for specific class is incomplete (misses some mandatory fields etc).
246
	 */
247
	protected static function getPrimitivesMapping(): array
248
	{
249
		$primitives = Config::get(RB::CONF_KEY_CONVERTER_PRIMITIVES) ?? [];
250
251
		if (!\is_array($primitives)) {
252
			throw new Ex\InvalidConfigurationException(
253
				\sprintf('"%s" mapping must be an array (%s found)', RB::CONF_KEY_CONVERTER_PRIMITIVES, \gettype($primitives)));
254
		}
255
256
		if (!empty($primitives)) {
257
			$mandatory_keys = [
258
				RB::KEY_KEY,
259
			];
260
261
			foreach ($primitives as $type => $config) {
262
				if (!\is_array($config)) {
263
					throw new Ex\InvalidConfigurationElementException(
264
						sprintf("Config for '{$type}' primitive must be an array (%s found).", \gettype($config)));
265
				}
266
				foreach ($mandatory_keys as $key_name) {
267
					if (!\array_key_exists($key_name, $config)) {
268
						throw new Ex\IncompleteConfigurationException(
269
							"Missing '{$key_name}' entry in '{$type}' primitive mapping config.");
270
					}
271
				}
272
			}
273
		}
274
275
		return $primitives;
276
	}
277
}
278