Total Complexity | 50 |
Total Lines | 416 |
Duplicated Lines | 1.92 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like AbstractModel 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 AbstractModel, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
55 | abstract class AbstractModel extends AbstractEntity implements |
||
56 | ModelInterface, |
||
57 | DescribablePropertyInterface, |
||
58 | LoggerAwareInterface, |
||
59 | ValidatableInterface, |
||
60 | ViewableInterface |
||
61 | { |
||
62 | use LoggerAwareTrait; |
||
63 | use DescribableTrait; |
||
64 | use DescribablePropertyTrait; |
||
65 | use StorableTrait; |
||
66 | use ValidatableTrait; |
||
67 | use ViewableTrait; |
||
68 | |||
69 | const DEFAULT_SOURCE_TYPE = 'database'; |
||
70 | |||
71 | /** |
||
72 | * @param array $data Dependencies. |
||
73 | */ |
||
74 | public function __construct(array $data = null) |
||
75 | { |
||
76 | // LoggerAwareInterface dependencies |
||
77 | $this->setLogger($data['logger']); |
||
78 | |||
79 | // Optional DescribableInterface dependencies |
||
80 | if (isset($data['property_factory'])) { |
||
81 | $this->setPropertyFactory($data['property_factory']); |
||
82 | } |
||
83 | if (isset($data['metadata'])) { |
||
84 | $this->setMetadata($data['metadata']); |
||
85 | } |
||
86 | if (isset($data['metadata_loader'])) { |
||
87 | $this->setMetadataLoader($data['metadata_loader']); |
||
88 | } |
||
89 | |||
90 | // Optional StorableInterface dependencies |
||
91 | if (isset($data['source'])) { |
||
92 | $this->setSource($data['source']); |
||
93 | } |
||
94 | if (isset($data['source_factory'])) { |
||
95 | $this->setSourceFactory($data['source_factory']); |
||
96 | } |
||
97 | |||
98 | // Optional ViewableInterface dependencies |
||
99 | if (isset($data['view'])) { |
||
100 | $this->setView($data['view']); |
||
101 | } |
||
102 | |||
103 | // Optional dependencies injection via Pimple Container |
||
104 | if (isset($data['container'])) { |
||
105 | $this->setDependencies($data['container']); |
||
106 | } |
||
107 | } |
||
108 | |||
109 | /** |
||
110 | * Sets the object data, from an associative array map (or any other Traversable). |
||
111 | * |
||
112 | * @param array $data The entity data. Will call setters. |
||
113 | * @return self |
||
114 | */ |
||
115 | public function setData(array $data) |
||
116 | { |
||
117 | $data = $this->setIdFromData($data); |
||
118 | |||
119 | parent::setData($data); |
||
120 | return $this; |
||
121 | } |
||
122 | |||
123 | /** |
||
124 | * Retrieve the model data as a structure (serialize to array). |
||
125 | * |
||
126 | * @param array $properties Optional. List of property identifiers |
||
127 | * for retrieving a subset of data. |
||
128 | * @return array |
||
129 | */ |
||
130 | public function data(array $properties = null) |
||
131 | { |
||
132 | $data = []; |
||
133 | $properties = $this->properties($properties); |
||
134 | foreach ($properties as $propertyIdent => $property) { |
||
135 | // Ensure objects are properly encoded. |
||
136 | $val = $this->propertyValue($propertyIdent); |
||
137 | $val = $this->serializedValue($val); |
||
138 | $data[$propertyIdent] = $val; |
||
139 | } |
||
140 | |||
141 | return $data; |
||
142 | } |
||
143 | |||
144 | /** |
||
145 | * Merge data on the model. |
||
146 | * |
||
147 | * Overrides `\Charcoal\Config\AbstractEntity::setData()` |
||
148 | * to take properties into consideration. |
||
149 | * |
||
150 | * Also add a special case, to merge values for l10n properties. |
||
151 | * |
||
152 | * @param array $data The data to merge. |
||
153 | * @return self |
||
154 | */ |
||
155 | public function mergeData(array $data) |
||
156 | { |
||
157 | $data = $this->setIdFromData($data); |
||
158 | |||
159 | foreach ($data as $propIdent => $val) { |
||
160 | if (!$this->hasProperty($propIdent)) { |
||
161 | $this->logger->warning(sprintf( |
||
162 | 'Cannot set property "%s" on object; not defined in metadata.', |
||
163 | $propIdent |
||
164 | )); |
||
165 | continue; |
||
166 | } |
||
167 | |||
168 | $property = $this->p($propIdent); |
||
169 | if ($property->l10n() && is_array($val)) { |
||
170 | $currentValue = json_decode(json_encode($this[$propIdent]), true); |
||
171 | if (is_array($currentValue)) { |
||
172 | $this[$propIdent] = array_merge($currentValue, $val); |
||
173 | } else { |
||
174 | $this[$propIdent] = $val; |
||
175 | } |
||
176 | } else { |
||
177 | $this[$propIdent] = $val; |
||
178 | } |
||
179 | } |
||
180 | |||
181 | return $this; |
||
182 | } |
||
183 | |||
184 | /** |
||
185 | * Retrieve the default values, from the model's metadata. |
||
186 | * |
||
187 | * @return array |
||
188 | */ |
||
189 | public function defaultData() |
||
190 | { |
||
191 | $metadata = $this->metadata(); |
||
192 | return $metadata->defaultData(); |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | * Set the model data (from a flattened structure). |
||
197 | * |
||
198 | * This method takes a 1-dimensional array and fills the object with its values. |
||
199 | * |
||
200 | * @param array $flatData The model data. |
||
201 | * @return self |
||
202 | */ |
||
203 | public function setFlatData(array $flatData) |
||
237 | } |
||
238 | |||
239 | /** |
||
240 | * Retrieve the model data as a flattened structure. |
||
241 | * |
||
242 | * This method returns a 1-dimensional array of the object's values. |
||
243 | * |
||
244 | * @todo Implementation required. |
||
245 | * @return array |
||
246 | */ |
||
247 | public function flatData() |
||
248 | { |
||
249 | return []; |
||
250 | } |
||
251 | |||
252 | /** |
||
253 | * Retrieve the value for the given property. |
||
254 | * |
||
255 | * @param string $propertyIdent The property identifier to fetch. |
||
256 | * @return mixed |
||
257 | */ |
||
258 | public function propertyValue($propertyIdent) |
||
259 | { |
||
260 | return $this[$propertyIdent]; |
||
261 | } |
||
262 | |||
263 | /** |
||
264 | * @param array $properties Optional array of properties to save. If null, use all object's properties. |
||
265 | * @return boolean |
||
266 | */ |
||
267 | public function saveProperties(array $properties = null) |
||
268 | { |
||
269 | if ($properties === null) { |
||
270 | $properties = array_keys($this->metadata()->properties()); |
||
271 | } |
||
272 | |||
273 | foreach ($properties as $propertyIdent) { |
||
274 | $p = $this->p($propertyIdent); |
||
275 | $v = $p->save($this->propertyValue($propertyIdent)); |
||
276 | |||
277 | if ($v === null) { |
||
278 | continue; |
||
279 | } |
||
280 | |||
281 | $this[$propertyIdent] = $v; |
||
282 | } |
||
283 | |||
284 | return true; |
||
285 | } |
||
286 | |||
287 | /** |
||
288 | * Load an object from the database from its l10n key $key. |
||
289 | * Also retrieve and return the actual language that matched. |
||
290 | * |
||
291 | * @param string $key Key pointing a column's l10n base ident. |
||
292 | * @param mixed $value Value to search in all languages. |
||
293 | * @param array $langs List of languages (code, ex: "en") to check into. |
||
294 | * @throws PDOException If the PDO query fails. |
||
295 | * @return string The matching language. |
||
296 | */ |
||
297 | public function loadFromL10n($key, $value, array $langs) |
||
298 | { |
||
299 | $switch = []; |
||
300 | $where = []; |
||
301 | foreach ($langs as $lang) { |
||
302 | $switch[] = 'when `'.$key.'_'.$lang.'` = :ident then \''.$lang.'\''; |
||
303 | $where[] = '`'.$key.'_'.$lang.'` = :ident'; |
||
304 | } |
||
305 | |||
306 | $q = ' |
||
307 | SELECT |
||
308 | *, |
||
309 | (case |
||
310 | '.implode("\n", $switch).' |
||
311 | end) as _lang |
||
312 | FROM |
||
313 | `'.$this->source()->table().'` |
||
314 | WHERE |
||
315 | ('.implode(' OR ', $where).') |
||
316 | LIMIT |
||
317 | 1'; |
||
318 | |||
319 | $binds = [ |
||
320 | 'ident' => $value |
||
321 | ]; |
||
322 | |||
323 | $sth = $this->source()->dbQuery($q, $binds); |
||
324 | if ($sth === false) { |
||
325 | throw new PDOException('Could not load item.'); |
||
326 | } |
||
327 | |||
328 | $data = $sth->fetch(PDO::FETCH_ASSOC); |
||
329 | $lang = $data['_lang']; |
||
330 | unset($data['_lang']); |
||
331 | |||
332 | if ($data) { |
||
333 | $this->setFlatData($data); |
||
334 | } |
||
335 | |||
336 | return $lang; |
||
337 | } |
||
338 | |||
339 | /** |
||
340 | * Convert the current class name in "type-ident" format. |
||
341 | * |
||
342 | * @return string |
||
343 | */ |
||
344 | public function objType() |
||
345 | { |
||
346 | $ident = preg_replace('/([a-z])([A-Z])/', '$1-$2', get_class($this)); |
||
347 | $objType = strtolower(str_replace('\\', '/', $ident)); |
||
348 | return $objType; |
||
349 | } |
||
350 | |||
351 | |||
352 | /** |
||
353 | * Inject dependencies from a DI Container. |
||
354 | * |
||
355 | * @param Container $container A Pimple DI service container. |
||
356 | * @return void |
||
357 | */ |
||
358 | protected function setDependencies(Container $container) |
||
359 | { |
||
360 | // This method is a stub. Reimplement in children method to inject dependencies in your class from a container. |
||
361 | } |
||
362 | |||
363 | /** |
||
364 | * Set the object's ID from an associative array map (or any other Traversable). |
||
365 | * |
||
366 | * Useful for setting the object ID before the rest of the object's data. |
||
367 | * |
||
368 | * @param array $data The object data. |
||
369 | * @return array The object data without the pre-set ID. |
||
370 | */ |
||
371 | protected function setIdFromData(array $data) |
||
372 | { |
||
373 | $key = $this->key(); |
||
374 | if (isset($data[$key])) { |
||
375 | $this->setId($data[$key]); |
||
376 | unset($data[$key]); |
||
377 | } |
||
378 | |||
379 | return $data; |
||
380 | } |
||
381 | |||
382 | /** |
||
383 | * Serialize the given value. |
||
384 | * |
||
385 | * @param mixed $val The value to serialize. |
||
386 | * @return mixed |
||
387 | */ |
||
388 | protected function serializedValue($val) |
||
389 | { |
||
390 | if (is_scalar($val)) { |
||
391 | return $val; |
||
392 | } elseif ($val instanceof DateTimeInterface) { |
||
393 | return $val->format('Y-m-d H:i:s'); |
||
394 | } else { |
||
395 | return json_decode(json_encode($val), true); |
||
396 | } |
||
397 | } |
||
398 | |||
399 | /** |
||
400 | * StorableTrait > preSave(). Save hook called before saving the model. |
||
401 | * |
||
402 | * @return boolean |
||
403 | */ |
||
404 | protected function preSave() |
||
405 | { |
||
406 | return $this->saveProperties(); |
||
407 | } |
||
408 | |||
409 | /** |
||
410 | * StorableTrait > preUpdate(). Update hook called before updating the model. |
||
411 | * |
||
412 | * @param string[] $properties Optional. The properties to update. |
||
413 | * @return boolean |
||
414 | */ |
||
415 | protected function preUpdate(array $properties = null) |
||
416 | { |
||
417 | return $this->saveProperties($properties); |
||
418 | } |
||
419 | |||
420 | /** |
||
421 | * DescribableTrait > createMetadata(). |
||
422 | * |
||
423 | * @return MetadataInterface |
||
424 | */ |
||
425 | protected function createMetadata() |
||
426 | { |
||
427 | return new ModelMetadata(); |
||
428 | } |
||
429 | |||
430 | /** |
||
431 | * StorableInterface > createSource() |
||
432 | * |
||
433 | * @throws UnexpectedValueException If the metadata source can not be found. |
||
434 | * @return \Charcoal\Source\SourceInterface |
||
435 | */ |
||
436 | protected function createSource() |
||
456 | } |
||
457 | |||
458 | /** |
||
459 | * ValidatableInterface > create_validator(). |
||
460 | * |
||
461 | * @param array $data Optional. |
||
462 | * @return \Charcoal\Validator\ValidatorInterface |
||
463 | */ |
||
464 | protected function createValidator(array $data = null) |
||
471 | } |
||
472 | } |
||
473 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.