| Total Complexity | 50 |
| Total Lines | 433 |
| Duplicated Lines | 0 % |
| Changes | 16 | ||
| Bugs | 1 | Features | 0 |
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 | * @see AbstractEntity::setData() |
||
| 115 | */ |
||
| 116 | public function setData(array $data) |
||
| 122 | } |
||
| 123 | |||
| 124 | /** |
||
| 125 | * Retrieve the model data as a structure (serialize to array). |
||
| 126 | * |
||
| 127 | * @param array $properties Optional. List of property identifiers |
||
| 128 | * for retrieving a subset of data. |
||
| 129 | * @return array |
||
| 130 | */ |
||
| 131 | public function data(array $properties = null) |
||
| 132 | { |
||
| 133 | $data = []; |
||
| 134 | $properties = $this->properties($properties); |
||
| 135 | foreach ($properties as $propertyIdent => $property) { |
||
| 136 | // Ensure objects are properly encoded. |
||
| 137 | $val = $this->propertyValue($propertyIdent); |
||
| 138 | $val = $this->serializedValue($val); |
||
| 139 | $data[$propertyIdent] = $val; |
||
| 140 | } |
||
| 141 | |||
| 142 | return $data; |
||
| 143 | } |
||
| 144 | |||
| 145 | /** |
||
| 146 | * Merge data on the model. |
||
| 147 | * |
||
| 148 | * Overrides `\Charcoal\Config\AbstractEntity::setData()` |
||
| 149 | * to take properties into consideration. |
||
| 150 | * |
||
| 151 | * Also add a special case, to merge values for l10n properties. |
||
| 152 | * |
||
| 153 | * @param array $data The data to merge. |
||
| 154 | * @return self |
||
| 155 | */ |
||
| 156 | public function mergeData(array $data) |
||
| 157 | { |
||
| 158 | $data = $this->setIdFromData($data); |
||
| 159 | |||
| 160 | foreach ($data as $propIdent => $val) { |
||
| 161 | if (!$this->hasProperty($propIdent)) { |
||
| 162 | $this->logger->warning(sprintf( |
||
| 163 | 'Cannot set property "%s" on object; not defined in metadata.', |
||
| 164 | $propIdent |
||
| 165 | )); |
||
| 166 | continue; |
||
| 167 | } |
||
| 168 | |||
| 169 | $property = $this->p($propIdent); |
||
| 170 | if ($property['l10n'] && is_array($val)) { |
||
| 171 | $currentValue = json_decode(json_encode($this[$propIdent]), true); |
||
| 172 | if (is_array($currentValue)) { |
||
| 173 | $this[$propIdent] = array_merge($currentValue, $val); |
||
| 174 | } else { |
||
| 175 | $this[$propIdent] = $val; |
||
| 176 | } |
||
| 177 | } else { |
||
| 178 | $this[$propIdent] = $val; |
||
| 179 | } |
||
| 180 | } |
||
| 181 | |||
| 182 | return $this; |
||
| 183 | } |
||
| 184 | |||
| 185 | /** |
||
| 186 | * Retrieve the default values, from the model's metadata. |
||
| 187 | * |
||
| 188 | * @return array |
||
| 189 | */ |
||
| 190 | public function defaultData() |
||
| 191 | { |
||
| 192 | $metadata = $this->metadata(); |
||
| 193 | return $metadata->defaultData(); |
||
| 194 | } |
||
| 195 | |||
| 196 | /** |
||
| 197 | * Set the model data (from a flattened structure). |
||
| 198 | * |
||
| 199 | * This method takes a 1-dimensional array and fills the object with its values. |
||
| 200 | * |
||
| 201 | * @param array $flatData The model data. |
||
| 202 | * @return self |
||
| 203 | */ |
||
| 204 | public function setFlatData(array $flatData) |
||
| 205 | { |
||
| 206 | $flatData = $this->setIdFromData($flatData); |
||
| 207 | |||
| 208 | $data = []; |
||
| 209 | $properties = $this->properties(); |
||
| 210 | foreach ($properties as $propertyIdent => $property) { |
||
| 211 | $fields = $property->fields(null); |
||
| 212 | foreach ($fields as $k => $f) { |
||
| 213 | if (is_string($k)) { |
||
| 214 | $fid = $f->ident(); |
||
| 215 | $snake = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $propertyIdent)); |
||
| 216 | $key = str_replace($snake.'_', '', $fid); |
||
| 217 | if (isset($flatData[$fid])) { |
||
| 218 | $data[$propertyIdent][$key] = $flatData[$fid]; |
||
| 219 | unset($flatData[$fid]); |
||
| 220 | } |
||
| 221 | } else { |
||
| 222 | $fid = $f->ident(); |
||
| 223 | if (isset($flatData[$fid])) { |
||
| 224 | $data[$propertyIdent] = $flatData[$fid]; |
||
| 225 | unset($flatData[$fid]); |
||
| 226 | } |
||
| 227 | } |
||
| 228 | } |
||
| 229 | } |
||
| 230 | |||
| 231 | $this->setData($data); |
||
| 232 | |||
| 233 | // Set remaining (non-property) data. |
||
| 234 | if (!empty($flatData)) { |
||
| 235 | $this->setData($flatData); |
||
| 236 | } |
||
| 237 | |||
| 238 | return $this; |
||
| 239 | } |
||
| 240 | |||
| 241 | /** |
||
| 242 | * Retrieve the model data as a flattened structure. |
||
| 243 | * |
||
| 244 | * This method returns a 1-dimensional array of the object's values. |
||
| 245 | * |
||
| 246 | * @todo Implementation required. |
||
| 247 | * @return array |
||
| 248 | */ |
||
| 249 | public function flatData() |
||
| 250 | { |
||
| 251 | return []; |
||
| 252 | } |
||
| 253 | |||
| 254 | /** |
||
| 255 | * Retrieve the value for the given property. |
||
| 256 | * Force camelcase on the parameter. |
||
| 257 | * |
||
| 258 | * @param string $propertyIdent The property identifier to fetch. |
||
| 259 | * @return mixed |
||
| 260 | */ |
||
| 261 | public function propertyValue($propertyIdent) |
||
| 262 | { |
||
| 263 | $propertyIdent = $this->camelize($propertyIdent); |
||
| 264 | return $this[$propertyIdent]; |
||
| 265 | } |
||
| 266 | |||
| 267 | /** |
||
| 268 | * @param array $properties Optional array of properties to save. If null, use all object's properties. |
||
| 269 | * @return boolean |
||
| 270 | */ |
||
| 271 | public function saveProperties(array $properties = null) |
||
| 272 | { |
||
| 273 | if ($properties === null) { |
||
| 274 | $properties = array_keys($this->metadata()->properties()); |
||
| 275 | } |
||
| 276 | |||
| 277 | foreach ($properties as $propertyIdent) { |
||
| 278 | $p = $this->p($propertyIdent); |
||
| 279 | $v = $p->save($this->propertyValue($propertyIdent)); |
||
| 280 | |||
| 281 | if ($v === null) { |
||
| 282 | continue; |
||
| 283 | } |
||
| 284 | |||
| 285 | $this[$propertyIdent] = $v; |
||
| 286 | } |
||
| 287 | |||
| 288 | return true; |
||
| 289 | } |
||
| 290 | |||
| 291 | /** |
||
| 292 | * Load an object from the database from its l10n key $key. |
||
| 293 | * Also retrieve and return the actual language that matched. |
||
| 294 | * |
||
| 295 | * @param string $key Key pointing a column's l10n base ident. |
||
| 296 | * @param mixed $value Value to search in all languages. |
||
| 297 | * @param array $langs List of languages (code, ex: "en") to check into. |
||
| 298 | * @throws PDOException If the PDO query fails. |
||
| 299 | * @return string The matching language. |
||
| 300 | */ |
||
| 301 | public function loadFromL10n($key, $value, array $langs) |
||
| 302 | { |
||
| 303 | $switch = []; |
||
| 304 | $where = []; |
||
| 305 | foreach ($langs as $lang) { |
||
| 306 | $switch[] = 'when `'.$key.'_'.$lang.'` = :ident then \''.$lang.'\''; |
||
| 307 | $where[] = '`'.$key.'_'.$lang.'` = :ident'; |
||
| 308 | } |
||
| 309 | |||
| 310 | $q = ' |
||
| 311 | SELECT |
||
| 312 | *, |
||
| 313 | (case |
||
| 314 | '.implode("\n", $switch).' |
||
| 315 | end) as _lang |
||
| 316 | FROM |
||
| 317 | `'.$this->source()->table().'` |
||
|
|
|||
| 318 | WHERE |
||
| 319 | ('.implode(' OR ', $where).') |
||
| 320 | LIMIT |
||
| 321 | 1'; |
||
| 322 | |||
| 323 | $binds = [ |
||
| 324 | 'ident' => $value |
||
| 325 | ]; |
||
| 326 | |||
| 327 | $sth = $this->source()->dbQuery($q, $binds); |
||
| 328 | if ($sth === false) { |
||
| 329 | throw new PDOException('Could not load item.'); |
||
| 330 | } |
||
| 331 | |||
| 332 | $data = $sth->fetch(PDO::FETCH_ASSOC); |
||
| 333 | $lang = $data['_lang']; |
||
| 334 | unset($data['_lang']); |
||
| 335 | |||
| 336 | if ($data) { |
||
| 337 | $this->setFlatData($data); |
||
| 338 | } |
||
| 339 | |||
| 340 | return $lang; |
||
| 341 | } |
||
| 342 | |||
| 343 | /** |
||
| 344 | * Generate a model type identifier from this object's class name. |
||
| 345 | * |
||
| 346 | * Based on {@see DescribableTrait::generateMetadataIdent()}. |
||
| 347 | * |
||
| 348 | * @return string |
||
| 349 | */ |
||
| 350 | public static function objType() |
||
| 351 | { |
||
| 352 | $class = get_called_class(); |
||
| 353 | $ident = preg_replace('/([a-z])([A-Z])/', '$1-$2', $class); |
||
| 354 | $ident = strtolower(str_replace('\\', '/', $ident)); |
||
| 355 | return $ident; |
||
| 356 | } |
||
| 357 | |||
| 358 | /** |
||
| 359 | * Inject dependencies from a DI Container. |
||
| 360 | * |
||
| 361 | * @param Container $container A Pimple DI service container. |
||
| 362 | * @return void |
||
| 363 | */ |
||
| 364 | protected function setDependencies(Container $container) |
||
| 365 | { |
||
| 366 | // This method is a stub. |
||
| 367 | // Reimplement in children method to inject dependencies in your class from a Pimple container. |
||
| 368 | } |
||
| 369 | |||
| 370 | /** |
||
| 371 | * Set the object's ID from an associative array map (or any other Traversable). |
||
| 372 | * |
||
| 373 | * Useful for setting the object ID before the rest of the object's data. |
||
| 374 | * |
||
| 375 | * @param array $data The object data. |
||
| 376 | * @return array The object data without the pre-set ID. |
||
| 377 | */ |
||
| 378 | protected function setIdFromData(array $data) |
||
| 379 | { |
||
| 380 | $key = $this->key(); |
||
| 381 | if (isset($data[$key])) { |
||
| 382 | $this->setId($data[$key]); |
||
| 383 | unset($data[$key]); |
||
| 384 | } |
||
| 385 | |||
| 386 | return $data; |
||
| 387 | } |
||
| 388 | |||
| 389 | /** |
||
| 390 | * Serialize the given value. |
||
| 391 | * |
||
| 392 | * @param mixed $val The value to serialize. |
||
| 393 | * @return mixed |
||
| 394 | */ |
||
| 395 | protected function serializedValue($val) |
||
| 403 | } |
||
| 404 | } |
||
| 405 | |||
| 406 | /** |
||
| 407 | * Save event called (in storable trait) before saving the model. |
||
| 408 | * |
||
| 409 | * @see StorableTrait::preSave() |
||
| 410 | * @return boolean |
||
| 411 | */ |
||
| 412 | protected function preSave() |
||
| 413 | { |
||
| 414 | return $this->saveProperties(); |
||
| 415 | } |
||
| 416 | |||
| 417 | /** |
||
| 418 | * StorableTrait > preUpdate(). Update hook called before updating the model. |
||
| 419 | * |
||
| 420 | * @param string[] $properties Optional. The properties to update. |
||
| 421 | * @see StorableTrait::preUpdate() |
||
| 422 | * @return boolean |
||
| 423 | */ |
||
| 424 | protected function preUpdate(array $properties = null) |
||
| 425 | { |
||
| 426 | return $this->saveProperties($properties); |
||
| 427 | } |
||
| 428 | |||
| 429 | /** |
||
| 430 | * Create a new metadata object. |
||
| 431 | * |
||
| 432 | * @see DescribablePropertyTrait::createMetadata() |
||
| 433 | * @return ModelMetadata |
||
| 434 | */ |
||
| 435 | protected function createMetadata() |
||
| 436 | { |
||
| 437 | $class = $this->metadataClass(); |
||
| 438 | return new $class(); |
||
| 439 | } |
||
| 440 | |||
| 441 | /** |
||
| 442 | * Retrieve the class name of the metadata object. |
||
| 443 | * |
||
| 444 | * @see DescribableTrait::metadataClass() |
||
| 445 | * @return string |
||
| 446 | */ |
||
| 447 | protected function metadataClass() |
||
| 450 | } |
||
| 451 | |||
| 452 | /** |
||
| 453 | * @throws UnexpectedValueException If the metadata source can not be found. |
||
| 454 | * @see StorableTrait::createSource() |
||
| 455 | * @return \Charcoal\Source\SourceInterface |
||
| 456 | */ |
||
| 457 | protected function createSource() |
||
| 477 | } |
||
| 478 | |||
| 479 | /** |
||
| 480 | * ValidatableInterface > create_validator(). |
||
| 481 | * |
||
| 482 | * @return \Charcoal\Validator\ValidatorInterface |
||
| 483 | */ |
||
| 484 | protected function createValidator() |
||
| 488 | } |
||
| 489 | } |
||
| 490 |