Total Complexity | 62 |
Total Lines | 434 |
Duplicated Lines | 0 % |
Changes | 3 | ||
Bugs | 0 | Features | 0 |
Complex classes like BaseObject often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use BaseObject, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
10 | abstract class BaseObject implements \JsonSerializable |
||
11 | { |
||
12 | public const CAMEL_CASE = ''; |
||
13 | public const SNAKE_LASE = '_'; |
||
14 | |||
15 | /** |
||
16 | * @var array $data |
||
17 | * Array of data fields with data |
||
18 | */ |
||
19 | protected array $data; |
||
20 | |||
21 | /** |
||
22 | * @var string[] |
||
23 | * Define mappings of fields |
||
24 | */ |
||
25 | protected array $fieldTypeMapping = [ |
||
26 | |||
27 | ]; |
||
28 | |||
29 | /** |
||
30 | * @var string[] |
||
31 | * Define fields which are list of objects |
||
32 | */ |
||
33 | protected array $listFields = [ |
||
34 | |||
35 | ]; |
||
36 | |||
37 | /** |
||
38 | * @var string[] |
||
39 | * List of primitives |
||
40 | */ |
||
41 | protected array $primitiveTypes = [ |
||
42 | 'int', |
||
43 | 'integer', |
||
44 | 'string', |
||
45 | 'boolean', |
||
46 | 'double', |
||
47 | 'string', |
||
48 | ]; |
||
49 | |||
50 | public function __construct(array $data = []) |
||
51 | { |
||
52 | if (!is_array($data)) { |
||
|
|||
53 | throw new \RuntimeException('$data should be an array'); |
||
54 | } |
||
55 | $this->data = $data; |
||
56 | |||
57 | foreach ($this->data as $key => $value) { |
||
58 | if (is_array($value)) { |
||
59 | $this->setProperty($key, $this->fromArray($key, $value)); |
||
60 | } |
||
61 | } |
||
62 | |||
63 | /** |
||
64 | * Call init$PropertyName if method exist |
||
65 | */ |
||
66 | foreach (array_keys($this->data) as $option) { |
||
67 | $key = $this->convertFieldName($option); |
||
68 | |||
69 | $method = 'init' . ucfirst($key); |
||
70 | if (method_exists($this, $method)) { |
||
71 | $this->$method(); |
||
72 | } |
||
73 | } |
||
74 | } |
||
75 | |||
76 | /** |
||
77 | * @param string $field |
||
78 | * @param array $value |
||
79 | * @return DataObject|mixed|BaseObject |
||
80 | */ |
||
81 | private function fromArray(string $field, array $value) |
||
82 | { |
||
83 | /** |
||
84 | * Check if this field has type mapping |
||
85 | */ |
||
86 | if (array_key_exists($field, $this->fieldTypeMapping)) { |
||
87 | $className = $this->fieldTypeMapping[$field]; |
||
88 | if (class_exists($className)) { |
||
89 | return new $className($value); |
||
90 | } |
||
91 | } |
||
92 | |||
93 | /** |
||
94 | * Check if this field is list of objects |
||
95 | */ |
||
96 | if (array_key_exists($field, $this->listFields)) { |
||
97 | $className = $this->listFields[$field]; |
||
98 | |||
99 | /** |
||
100 | * Check if class is primitive (int, string, etc.) |
||
101 | */ |
||
102 | if (in_array($className, $this->primitiveTypes, true)) { |
||
103 | return $value; |
||
104 | } |
||
105 | |||
106 | if (class_exists($className)) { |
||
107 | $list = []; |
||
108 | foreach ($value as $item) { |
||
109 | $list[] = new $className($item); |
||
110 | } |
||
111 | return $list; |
||
112 | } |
||
113 | } |
||
114 | |||
115 | return new DataObject($value); |
||
116 | } |
||
117 | |||
118 | /** |
||
119 | * @param $name |
||
120 | * @return mixed|null |
||
121 | * Return object |
||
122 | */ |
||
123 | public function __get($name) |
||
124 | { |
||
125 | return $this->data[$name] ?? null; |
||
126 | } |
||
127 | |||
128 | /** |
||
129 | * @param $name |
||
130 | * @param $value |
||
131 | * Set object |
||
132 | */ |
||
133 | public function __set($name, $value) |
||
134 | { |
||
135 | $this->data[$name] = $value; |
||
136 | } |
||
137 | |||
138 | /** |
||
139 | * @param $name |
||
140 | * @return bool |
||
141 | * Check if field exist |
||
142 | */ |
||
143 | public function __isset($name): bool |
||
144 | { |
||
145 | return $this->_has($name); |
||
146 | } |
||
147 | |||
148 | /** |
||
149 | * @param $name |
||
150 | * @return bool |
||
151 | * Check if field exist |
||
152 | */ |
||
153 | private function _has($name): bool |
||
154 | { |
||
155 | return array_key_exists( |
||
156 | $name, |
||
157 | $this->data |
||
158 | ) && $this->data[$name]; |
||
159 | } |
||
160 | |||
161 | /** |
||
162 | * @param $name |
||
163 | * @param $arguments |
||
164 | * @return boolean|mixed|null |
||
165 | */ |
||
166 | public function __call($name, $arguments) |
||
167 | { |
||
168 | $words = preg_split('/(?=[A-Z])/', $name); |
||
169 | $methodType = $words[0]; |
||
170 | unset($words[0]); |
||
171 | $field_name = $this->getFieldName(implode('', $words)); |
||
172 | switch ($methodType) { |
||
173 | case 'get': |
||
174 | return $this->$field_name; |
||
175 | break; |
||
176 | case 'has': |
||
177 | case 'is': |
||
178 | return $this->_has($field_name); |
||
179 | break; |
||
180 | } |
||
181 | } |
||
182 | |||
183 | /** |
||
184 | * Return field name for data |
||
185 | * @param $name |
||
186 | * @return string |
||
187 | */ |
||
188 | private function getFieldName($name): string |
||
189 | { |
||
190 | $name = lcfirst($name); |
||
191 | $camelName = $this->convertFieldName($name, self::CAMEL_CASE); |
||
192 | $snakeName = $this->convertFieldName($name, self::SNAKE_LASE); |
||
193 | if (array_key_exists($camelName, $this->data)) { |
||
194 | return $camelName; |
||
195 | } |
||
196 | if (array_key_exists($snakeName, $this->data)) { |
||
197 | return $snakeName; |
||
198 | } |
||
199 | return $name; |
||
200 | } |
||
201 | |||
202 | /** |
||
203 | * Convert MyField to my_field or my_field to MyField |
||
204 | * @param string $name |
||
205 | * @param string $replacement |
||
206 | * @return string |
||
207 | */ |
||
208 | private function convertFieldName($name, $replacement = ''): string |
||
209 | { |
||
210 | $words = preg_split('/(?=[A-Z_])/', $name); |
||
211 | foreach ($words as &$word) { |
||
212 | $word = str_replace('_', '', $word); |
||
213 | } |
||
214 | if (!empty($replacement)) { |
||
215 | return strtolower(implode($replacement, $words)); |
||
216 | } |
||
217 | foreach ($words as $i => &$word) { |
||
218 | if ($i === 0) { |
||
219 | continue; |
||
220 | } |
||
221 | $word = ucfirst($word); |
||
222 | } |
||
223 | return implode($replacement, $words); |
||
224 | } |
||
225 | |||
226 | /** |
||
227 | * @param $key |
||
228 | * @return bool |
||
229 | * Check that fieldExist |
||
230 | */ |
||
231 | public function has($key): bool |
||
232 | { |
||
233 | return $this->_has($key); |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * @return bool |
||
238 | * Check if errors exist in object |
||
239 | */ |
||
240 | public function hasErrors(): bool |
||
241 | { |
||
242 | return $this->has('error') || $this->has('errors'); |
||
243 | } |
||
244 | |||
245 | /** |
||
246 | * @return string |
||
247 | * Return error |
||
248 | */ |
||
249 | public function getError(): string |
||
250 | { |
||
251 | return ($this->has('error')) ? $this->data['error'] : $this->data['errors']; |
||
252 | } |
||
253 | |||
254 | /** |
||
255 | * @return array |
||
256 | * Return all fields |
||
257 | */ |
||
258 | public function getData(): array |
||
259 | { |
||
260 | return $this->data; |
||
261 | } |
||
262 | |||
263 | /** |
||
264 | * @param string $propertyName |
||
265 | * @return mixed|null |
||
266 | * Return field |
||
267 | */ |
||
268 | public function getProperty(string $propertyName) |
||
269 | { |
||
270 | if ($this->has($propertyName)) { |
||
271 | return $this->$propertyName; |
||
272 | } |
||
273 | return null; |
||
274 | } |
||
275 | |||
276 | /** |
||
277 | * @return array |
||
278 | * Used during jsonSerialization |
||
279 | */ |
||
280 | public function jsonSerialize(): array |
||
281 | { |
||
282 | return $this->toArray(); |
||
283 | } |
||
284 | |||
285 | /** |
||
286 | * @param string $propertyName |
||
287 | * @param $value |
||
288 | * Set field |
||
289 | */ |
||
290 | public function setProperty(string $propertyName, $value): void |
||
293 | } |
||
294 | |||
295 | /** |
||
296 | * @return array |
||
297 | * Return array with data |
||
298 | */ |
||
299 | public function toArray(): array |
||
300 | { |
||
301 | return $this->getData(); |
||
302 | } |
||
303 | |||
304 | /** |
||
305 | * @param string $json |
||
306 | * @return BaseObject |
||
307 | * @throws \JsonException |
||
308 | * Create object from JSON string |
||
309 | */ |
||
310 | public static function fromJsonString(string $json): BaseObject |
||
311 | { |
||
312 | $jsonObject = json_decode($json, true, 512, JSON_THROW_ON_ERROR); |
||
313 | return new static($jsonObject); |
||
314 | } |
||
315 | |||
316 | /** |
||
317 | * @return array |
||
318 | * Return array with names of properties |
||
319 | */ |
||
320 | public function getPropertyNames(): array |
||
321 | { |
||
322 | return array_keys($this->toArray()); |
||
323 | } |
||
324 | |||
325 | /** |
||
326 | * @param string $propertyName |
||
327 | * @return bool |
||
328 | * Return true if property is list or array |
||
329 | */ |
||
330 | public function isList(string $propertyName): bool |
||
331 | { |
||
332 | return is_array($this->getProperty($propertyName)); |
||
333 | } |
||
334 | |||
335 | /** |
||
336 | * @param string $propertyName |
||
337 | * @return bool |
||
338 | * Return true if property is object or list of object |
||
339 | */ |
||
340 | public function isObject(string $propertyName): bool |
||
341 | { |
||
342 | $value = $this->getProperty($propertyName); |
||
343 | if (is_object($value)) { |
||
344 | return true; |
||
345 | } |
||
346 | if (in_array($propertyName, $this->listFields, true) && $this->isList($propertyName) && is_object($value[0])) { |
||
347 | return true; |
||
348 | } |
||
349 | return false; |
||
350 | } |
||
351 | |||
352 | /** |
||
353 | * @param string $propertyName |
||
354 | * @return bool |
||
355 | * Return true if property has primitive type (string, integer, boolean) |
||
356 | */ |
||
357 | public function isPrimitive(string $propertyName): bool |
||
360 | } |
||
361 | |||
362 | protected function filterProperties(string $filterFunction): array |
||
363 | { |
||
364 | if (!method_exists($this, $filterFunction)) { |
||
365 | return []; |
||
366 | } |
||
367 | |||
368 | $properties = $this->getPropertyNames(); |
||
369 | $result = []; |
||
370 | foreach ($properties as $property) { |
||
371 | if($this->$filterFunction($property)) { |
||
372 | $result[] = $property; |
||
373 | } |
||
374 | } |
||
375 | return $result; |
||
376 | } |
||
377 | |||
378 | /** |
||
379 | * @return array |
||
380 | * Return property names which has primitive type (string, integer, boolean) |
||
381 | */ |
||
382 | public function getPrimitivePropertyNames(): array |
||
383 | { |
||
384 | return $this->filterProperties('isPrimitive'); |
||
385 | } |
||
386 | |||
387 | /** |
||
388 | * @param array $names |
||
389 | * @return array |
||
390 | * Return array with desired properties and their values |
||
391 | */ |
||
392 | protected function getPropertiesByNames(array $names): array |
||
393 | { |
||
394 | $result = []; |
||
395 | foreach ($names as $property) { |
||
396 | $result[$property] = $this->getProperty($property); |
||
397 | } |
||
398 | return $result; |
||
399 | } |
||
400 | |||
401 | /** |
||
402 | * @return array |
||
403 | * Return array with properties which has primitive type (integer, string, boolean) |
||
404 | */ |
||
405 | public function getPrimitiveProperties(): array |
||
406 | { |
||
407 | return $this->getPropertiesByNames($this->getPrimitivePropertyNames()); |
||
408 | } |
||
409 | |||
410 | /** |
||
411 | * @return array |
||
412 | * Return array with property names which are list or array |
||
413 | */ |
||
414 | public function getListPropertyNames(): array |
||
415 | { |
||
416 | return $this->filterProperties('isList'); |
||
417 | } |
||
418 | |||
419 | /** |
||
420 | * @return array |
||
421 | * Return array with property which are list or array and their values |
||
422 | */ |
||
423 | public function getListProperties(): array |
||
426 | } |
||
427 | |||
428 | /** |
||
429 | * @return array |
||
430 | * Return array with property names which are objects or list of objects |
||
431 | */ |
||
432 | public function getObjectPropertyNames(): array |
||
433 | { |
||
434 | return $this->filterProperties('isObject'); |
||
435 | } |
||
436 | |||
437 | /** |
||
438 | * @return array |
||
439 | * Return array with properties which are objects or list of objects |
||
440 | */ |
||
441 | public function getObjectProperties(): array |
||
444 | } |
||
445 | } |
||
446 |