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-2021 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\ConfigurationNotFoundException |
||
57 | */ |
||
58 | protected function getPrimitiveMappingConfigOrThrow($data): array |
||
59 | { |
||
60 | $result = null; |
||
0 ignored issues
–
show
Unused Code
introduced
by
![]() |
|||
61 | |||
62 | $type = \gettype($data); |
||
63 | $result = $this->primitives[ $type ] ?? null; |
||
64 | |||
65 | if ($result === null) { |
||
66 | throw new Ex\ConfigurationNotFoundException( |
||
67 | sprintf('No data conversion mapping configured for "%s" primitive.', $type)); |
||
68 | 16 | } |
|
69 | |||
70 | 16 | if ($this->debug_enabled) { |
|
71 | 16 | Log::debug(__CLASS__ . ": Converting primitive type of '{$type}' to data node with key '{$result[RB::KEY_KEY]}'."); |
|
72 | } |
||
73 | |||
74 | 16 | return $result; |
|
75 | 16 | } |
|
76 | 16 | ||
77 | 11 | /** |
|
78 | 11 | * Returns "converter/map" mapping configured for given $data object class or throws exception if not found. |
|
79 | * |
||
80 | * @param object $data Object to get config for. |
||
81 | 5 | * |
|
82 | 5 | * @return array |
|
83 | 4 | * |
|
84 | 4 | * @throws Ex\ConfigurationNotFoundException |
|
85 | 4 | */ |
|
86 | protected function getClassMappingConfigOrThrow(object $data): array |
||
87 | { |
||
88 | $result = null; |
||
89 | $debug_result = ''; |
||
90 | |||
91 | 16 | // check for exact class name match... |
|
92 | 1 | $cls = \get_class($data); |
|
93 | if ($cls !== false) { |
||
94 | if (\array_key_exists($cls, $this->classes)) { |
||
95 | 15 | $result = $this->classes[ $cls ]; |
|
96 | $debug_result = 'exact config match'; |
||
97 | } else { |
||
98 | // no exact match, then lets try with `instanceof` |
||
99 | 15 | foreach (\array_keys($this->classes) as $class_name) { |
|
100 | if ($data instanceof $class_name) { |
||
101 | $result = $this->classes[ $class_name ]; |
||
102 | $debug_result = "subclass of {$class_name}"; |
||
103 | break; |
||
104 | } |
||
105 | } |
||
106 | } |
||
107 | } |
||
108 | |||
109 | if ($result === null) { |
||
110 | throw new Ex\ConfigurationNotFoundException( |
||
111 | 37 | sprintf('No data conversion mapping configured for "%s" class.', $cls)); |
|
112 | } |
||
113 | 37 | ||
114 | 15 | if ($this->debug_enabled) { |
|
115 | Log::debug(__CLASS__ . ": Converting {$cls} using {$result[RB::KEY_HANDLER]} because: {$debug_result}."); |
||
116 | } |
||
117 | 22 | ||
118 | 22 | return $result; |
|
119 | } |
||
120 | 21 | ||
121 | 11 | /** |
|
122 | 10 | * Main entry for data conversion |
|
123 | 10 | * |
|
124 | * @param mixed|null $data |
||
125 | 10 | * |
|
126 | * @return array|null |
||
127 | */ |
||
128 | 19 | public function convert($data = null): ?array |
|
129 | { |
||
130 | if ($data === null) { |
||
131 | return null; |
||
132 | } |
||
133 | |||
134 | $result = null; |
||
135 | |||
136 | Validator::assertIsType('data', $data, [ |
||
137 | Type::ARRAY, |
||
138 | Type::BOOLEAN, |
||
139 | Type::DOUBLE, |
||
140 | Type::INTEGER, |
||
141 | 10 | Type::OBJECT, |
|
142 | Type::STRING, |
||
143 | ]); |
||
144 | |||
145 | if ($result === null && \is_object($data)) { |
||
146 | 10 | $cfg = $this->getClassMappingConfigOrThrow($data); |
|
147 | 10 | $worker = new $cfg[ RB::KEY_HANDLER ](); |
|
148 | 10 | $result = $worker->convert($data, $cfg); |
|
149 | 10 | $result = $cfg[ RB::KEY_KEY ] === null ? $result : [$cfg[ RB::KEY_KEY ] => $result]; |
|
150 | 6 | } |
|
151 | |||
152 | 6 | if ($result === null && \is_array($data)) { |
|
153 | $cfg = $this->getPrimitiveMappingConfigOrThrow($data); |
||
154 | |||
155 | 10 | $result = $this->convertArray($data); |
|
156 | 1 | if (!Util::isArrayWithNonNumericKeys($data)) { |
|
157 | $result = [$cfg[ RB::KEY_KEY ] => $result]; |
||
158 | 1 | } |
|
159 | } |
||
160 | |||
161 | if (\is_bool($data) || \is_float($data) || \is_int($data) || \is_string($data)) { |
||
162 | 9 | $result = [$this->getPrimitiveMappingConfigOrThrow($data)[ RB::KEY_KEY ] => $data]; |
|
163 | 9 | } |
|
164 | 4 | ||
165 | 9 | return $result; |
|
166 | 5 | } |
|
167 | 5 | ||
168 | 5 | /** |
|
169 | 5 | * Recursively walks $data array and converts all known objects if found. Note |
|
170 | * $data array is passed by reference so source $data array may be modified. |
||
171 | * |
||
172 | * @param array $data array to recursively convert known elements of |
||
173 | 9 | * |
|
174 | * @return array |
||
175 | */ |
||
176 | protected function convertArray(array $data): array |
||
177 | { |
||
178 | Validator::assertArrayHasNoMixedKeys($data); |
||
179 | |||
180 | foreach ($data as $key => $val) { |
||
181 | if (\is_array($val)) { |
||
182 | $data[ $key ] = $this->convertArray($val); |
||
183 | 42 | } elseif (\is_object($val)) { |
|
184 | $cfg = $this->getClassMappingConfigOrThrow($val); |
||
185 | 42 | $worker = new $cfg[ RB::KEY_HANDLER ](); |
|
186 | $converted_data = $worker->convert($val, $cfg); |
||
187 | 42 | $data[ $key ] = $converted_data; |
|
188 | 41 | } |
|
189 | 3 | } |
|
190 | 3 | ||
191 | return $data; |
||
192 | } |
||
193 | |||
194 | 38 | /** |
|
195 | * Reads and validates "converter/map" config mapping |
||
196 | 38 | * |
|
197 | 38 | * @return array Classes mapping as specified in configuration or empty array if configuration found |
|
198 | 38 | * |
|
199 | 1 | * @throws Ex\InvalidConfigurationException if whole config mapping is technically invalid (i.e. not an array etc). |
|
200 | * @throws Ex\InvalidConfigurationElementException if config for specific class is technically invalid (i.e. not an array etc). |
||
201 | * @throws Ex\IncompleteConfigurationException if config for specific class is incomplete (misses some mandatory fields etc). |
||
202 | */ |
||
203 | protected static function getClassesMapping(): array |
||
204 | 1 | { |
|
205 | $classes = Config::get(RB::CONF_KEY_CONVERTER_CLASSES) ?? []; |
||
206 | |||
207 | 38 | if (!\is_array($classes)) { |
|
208 | throw new Ex\InvalidConfigurationException( |
||
209 | \sprintf('"%s" must be an array (%s found)', RB::CONF_KEY_CONVERTER_CLASSES, \gettype($classes))); |
||
210 | } |
||
211 | |||
212 | if (!empty($classes)) { |
||
213 | $mandatory_keys = [ |
||
214 | RB::KEY_HANDLER => [TYPE::STRING], |
||
215 | RB::KEY_KEY => [TYPE::STRING, TYPE::NULL], |
||
216 | ]; |
||
217 | foreach ($classes as $class_name => $class_config) { |
||
218 | if (!\is_array($class_config)) { |
||
219 | throw new Ex\InvalidConfigurationElementException( |
||
220 | sprintf("Config for '{$class_name}' class must be an array (%s found).", \gettype($class_config))); |
||
221 | } |
||
222 | foreach ($mandatory_keys as $key_name => $allowed_types) { |
||
223 | if (!\array_key_exists($key_name, $class_config)) { |
||
224 | throw new Ex\IncompleteConfigurationException( |
||
225 | "Missing '{$key_name}' entry in '{$class_name}' class mapping config."); |
||
226 | } |
||
227 | |||
228 | Validator::assertIsType(RB::KEY_KEY, $class_config[$key_name], $allowed_types); |
||
229 | } |
||
230 | } |
||
231 | } |
||
232 | |||
233 | return $classes; |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * Reads and validates "converter/primitives" config mapping |
||
238 | * |
||
239 | * @return array Primitives mapping config as specified in configuration or empty array if configuration found |
||
240 | * |
||
241 | * @throws Ex\InvalidConfigurationException if whole config mapping is technically invalid (i.e. not an array etc). |
||
242 | * @throws Ex\InvalidConfigurationElementException if config for specific class is technically invalid (i.e. not an array etc). |
||
243 | * @throws Ex\IncompleteConfigurationException if config for specific class is incomplete (misses some mandatory fields etc). |
||
244 | */ |
||
245 | protected static function getPrimitivesMapping(): array |
||
246 | { |
||
247 | $primitives = Config::get(RB::CONF_KEY_CONVERTER_PRIMITIVES) ?? []; |
||
248 | |||
249 | if (!\is_array($primitives)) { |
||
250 | throw new Ex\InvalidConfigurationException( |
||
251 | \sprintf('"%s" mapping must be an array (%s found)', RB::CONF_KEY_CONVERTER_PRIMITIVES, \gettype($primitives))); |
||
252 | } |
||
253 | |||
254 | if (!empty($primitives)) { |
||
255 | $mandatory_keys = [ |
||
256 | RB::KEY_KEY, |
||
257 | ]; |
||
258 | |||
259 | foreach ($primitives as $type => $config) { |
||
260 | if (!\is_array($config)) { |
||
261 | throw new Ex\InvalidConfigurationElementException( |
||
262 | sprintf("Config for '{$type}' primitive must be an array (%s found).", \gettype($config))); |
||
263 | } |
||
264 | foreach ($mandatory_keys as $key_name) { |
||
265 | if (!\array_key_exists($key_name, $config)) { |
||
266 | throw new Ex\IncompleteConfigurationException( |
||
267 | "Missing '{$key_name}' entry in '{$type}' primitive mapping config."); |
||
268 | } |
||
269 | } |
||
270 | } |
||
271 | } |
||
272 | |||
273 | return $primitives; |
||
274 | } |
||
275 | } |
||
276 |