Complex classes like RecordWrapper 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
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 RecordWrapper, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
35 | class RecordWrapper implements \ArrayAccess, \Countable, \Iterator |
||
36 | { |
||
37 | /** |
||
38 | * An associative array of models to which this model has a one to may relationship. |
||
39 | * |
||
40 | * @var array |
||
41 | */ |
||
42 | protected $hasMany = []; |
||
43 | |||
44 | /** |
||
45 | * An associative array of models which have a one-to-many relationship with this model. |
||
46 | * |
||
47 | * @var array |
||
48 | */ |
||
49 | protected $belongsTo = []; |
||
50 | |||
51 | /** |
||
52 | * An associative array of models with which this model has a many to many relationship. |
||
53 | * |
||
54 | * @var array |
||
55 | */ |
||
56 | protected $manyHaveMany = []; |
||
57 | |||
58 | /** |
||
59 | * The name of the database table. |
||
60 | * |
||
61 | * @var string |
||
62 | */ |
||
63 | protected $table; |
||
64 | |||
65 | /** |
||
66 | * The name of the schema to which this table belongs. |
||
67 | * |
||
68 | * @var string |
||
69 | */ |
||
70 | protected $schema; |
||
71 | |||
72 | /** |
||
73 | * Temporary data held in the model object. |
||
74 | * |
||
75 | * @var array |
||
76 | */ |
||
77 | protected $modelData = []; |
||
78 | |||
79 | /** |
||
80 | * Extra validation rules to use over the model's inherent validation requirements. |
||
81 | * @var array |
||
82 | */ |
||
83 | protected $validationRules = []; |
||
84 | |||
85 | /** |
||
86 | * A quoted string of the table name used for building queries. |
||
87 | * |
||
88 | * @var string |
||
89 | */ |
||
90 | private $quotedTable; |
||
91 | |||
92 | /** |
||
93 | * The raw table name without any quotes. |
||
94 | * |
||
95 | * @var string |
||
96 | */ |
||
97 | private $unquotedTable; |
||
98 | |||
99 | /** |
||
100 | * An array of fields that contain validation errors after an attempted save. |
||
101 | * |
||
102 | * @var array |
||
103 | */ |
||
104 | private $invalidFields; |
||
105 | |||
106 | /** |
||
107 | * An instance of the operations dispatcher. |
||
108 | * |
||
109 | * @var Operations |
||
110 | */ |
||
111 | private $dynamicOperations; |
||
112 | |||
113 | /** |
||
114 | * Location of the RecordWrapper's internal iterator. |
||
115 | * |
||
116 | * @var int |
||
117 | */ |
||
118 | private $index = 0; |
||
119 | |||
120 | /** |
||
121 | * This flag is set whenever data is manually put in the model with the setData method. |
||
122 | * |
||
123 | * @var bool |
||
124 | */ |
||
125 | private $dataSet = false; |
||
126 | |||
127 | /** |
||
128 | * The name of the class for this model obtained through reflection. |
||
129 | * |
||
130 | * @var string |
||
131 | */ |
||
132 | private $className; |
||
133 | |||
134 | /** |
||
135 | * An instance of the driver adapter for interacting with the database. |
||
136 | * |
||
137 | * @var DriverAdapter |
||
138 | */ |
||
139 | private $adapter; |
||
140 | |||
141 | /** |
||
142 | * An instance of the ORMContext through which this model is operating. |
||
143 | * |
||
144 | * @var ORMContext |
||
145 | */ |
||
146 | private $context; |
||
147 | |||
148 | /** |
||
149 | * Keys for the various fields when model is accessed as an associative array. |
||
150 | * |
||
151 | * @var array |
||
152 | */ |
||
153 | private $keys = []; |
||
154 | |||
155 | /** |
||
156 | * This flag is set after the model has been properly initialized. |
||
157 | * Useful after model is unserialized or accessed through the static interface. |
||
158 | * |
||
159 | * @var bool |
||
160 | */ |
||
161 | private $initialized = false; |
||
162 | |||
163 | /** |
||
164 | * Initialize the record wrapper and setup the adapters, drivers, tables and schemas. |
||
165 | * After initialization, this method sets the initialized flag. |
||
166 | * |
||
167 | * @return void |
||
168 | * @throws NibiiException |
||
169 | * @throws \ReflectionException |
||
170 | * @throws \ntentan\atiaa\exceptions\ConnectionException |
||
171 | */ |
||
172 | 32 | protected function initialize(): void |
|
194 | |||
195 | public function __debugInfo() |
||
196 | { |
||
197 | $data = $this->getData(); |
||
198 | return $this->hasMultipleItems() ? $data : isset($data[0]) ? $data[0] : []; |
||
199 | } |
||
200 | |||
201 | /** |
||
202 | * Return a description of the model wrapped by this wrapper. |
||
203 | * |
||
204 | * @return ModelDescription |
||
205 | * @throws NibiiException |
||
206 | * @throws \ReflectionException |
||
207 | * @throws \ntentan\atiaa\exceptions\ConnectionException |
||
208 | */ |
||
209 | 24 | public function getDescription() : ModelDescription |
|
210 | { |
||
211 | 24 | $this->initialize(); |
|
212 | |||
213 | 24 | return $this->context->getCache()->read( |
|
214 | "{$this->className}::desc", function () { |
||
215 | 24 | return $this->context->getModelDescription($this); |
|
216 | 24 | } |
|
217 | ); |
||
218 | } |
||
219 | |||
220 | /** |
||
221 | * Return the number of items stored in the model or matched by the query. |
||
222 | * Depending on the state of the model, the count method will return different values. For models that have data |
||
223 | * values set with calls to setData, this method returns the number of records that were added. On the other hand, |
||
224 | * for models that do not have data set, this method queries the database to find out the number of records that |
||
225 | * are either in the model, or for models that have been filtered, the number of records that match the filter. |
||
226 | * |
||
227 | * @param int|array|QueryParameters $query |
||
228 | * |
||
229 | * @return int |
||
230 | * @throws NibiiException |
||
231 | */ |
||
232 | 8 | public function count($query = null) |
|
233 | { |
||
234 | 8 | if ($this->dataSet) { |
|
235 | 8 | return count($this->getData()); |
|
236 | } |
||
237 | |||
238 | return $this->__call('count', [$query]); |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Retrieve an item stored in the record. |
||
243 | * This method returns items that are directly stored in the model, or lazy loads related items if needed. |
||
244 | * The key could be a field in the model's table or the name of a related model. |
||
245 | * |
||
246 | * @param string $key A key identifying the item to be retrieved. |
||
247 | * |
||
248 | * @return mixed |
||
249 | * @throws NibiiException |
||
250 | * @throws \ReflectionException |
||
251 | * @throws \ntentan\atiaa\exceptions\ConnectionException |
||
252 | */ |
||
253 | 8 | private function retrieveItem($key) |
|
254 | { |
||
255 | 8 | if ($this->hasMultipleItems()) { |
|
256 | throw new NibiiException('Current model object state contains multiple items. Please index with a numeric key to select a specific item first.'); |
||
257 | } |
||
258 | 8 | $relationships = $this->getDescription()->getRelationships(); |
|
259 | 8 | $decamelizedKey = Text::deCamelize($key); |
|
260 | 8 | if (isset($relationships[$decamelizedKey]) && !isset($this->modelData[$decamelizedKey])) { |
|
261 | 4 | return $this->fetchRelatedFields($relationships[$decamelizedKey]); |
|
262 | } |
||
263 | |||
264 | 4 | return isset($this->modelData[$decamelizedKey]) ? $this->modelData[$decamelizedKey] : null; |
|
265 | } |
||
266 | |||
267 | /** |
||
268 | * Calls dynamic methods. |
||
269 | * |
||
270 | * @param string $name |
||
271 | * @param array $arguments |
||
272 | * |
||
273 | * @return type |
||
274 | * @throws NibiiException |
||
275 | * @throws \ReflectionException |
||
276 | * @throws \ntentan\atiaa\exceptions\ConnectionException |
||
277 | */ |
||
278 | 30 | public function __call($name, $arguments) |
|
279 | { |
||
280 | 30 | $this->initialize(); |
|
281 | 30 | if ($this->dynamicOperations === null) { |
|
282 | 30 | $this->dynamicOperations = new Operations($this, $this->quotedTable); |
|
283 | } |
||
284 | |||
285 | 30 | return $this->dynamicOperations->perform($name, $arguments); |
|
286 | } |
||
287 | |||
288 | /** |
||
289 | * Set a value for a field in the model. |
||
290 | * |
||
291 | * @param string $name |
||
292 | * @param mixed $value |
||
293 | */ |
||
294 | 8 | public function __set($name, $value) |
|
295 | { |
||
296 | 8 | $this->dataSet = true; |
|
297 | 8 | $this->modelData[Text::deCamelize($name)] = $value; |
|
298 | 8 | } |
|
299 | |||
300 | 8 | public function __get($name) |
|
301 | { |
||
302 | 8 | return $this->retrieveItem($name); |
|
303 | } |
||
304 | |||
305 | 10 | public function save() |
|
306 | { |
||
307 | 10 | $return = $this->__call('save', [$this->hasMultipleItems()]); |
|
308 | 10 | $this->invalidFields = $this->dynamicOperations->getInvalidFields(); |
|
309 | |||
310 | 10 | return $return; |
|
311 | } |
||
312 | |||
313 | 22 | private function hasMultipleItems() |
|
314 | { |
||
315 | 22 | if (count($this->modelData) > 0) { |
|
316 | 20 | return is_numeric(array_keys($this->modelData)[0]); |
|
317 | } else { |
||
318 | 2 | return false; |
|
319 | } |
||
320 | } |
||
321 | |||
322 | 18 | public function getData() |
|
323 | { |
||
324 | 18 | $data = []; |
|
325 | |||
326 | 18 | if (count($this->modelData) == 0) { |
|
327 | 2 | $data = $this->modelData; |
|
328 | 16 | } elseif ($this->hasMultipleItems()) { |
|
329 | 6 | $data = $this->modelData; |
|
330 | 12 | } elseif (count($this->modelData) > 0) { |
|
331 | 12 | $data[] = $this->modelData; |
|
332 | } |
||
333 | |||
334 | 18 | return $data; |
|
335 | } |
||
336 | |||
337 | 22 | public function setData($data) |
|
338 | { |
||
339 | 22 | $this->dataSet = is_array($data) ? true : false; |
|
340 | 22 | $this->modelData = $data; |
|
341 | 22 | } |
|
342 | |||
343 | public function mergeData($data) |
||
344 | { |
||
345 | foreach ($data as $key => $value) { |
||
346 | $this->modelData[$key] = $value; |
||
347 | } |
||
348 | $this->dataSet = true; |
||
349 | } |
||
350 | |||
351 | 2 | public function offsetExists($offset) |
|
352 | { |
||
353 | 2 | return isset($this->modelData[$offset]); |
|
354 | } |
||
355 | |||
356 | 2 | public function offsetGet($offset) |
|
357 | { |
||
358 | 2 | if (is_numeric($offset)) { |
|
359 | 2 | return $this->wrap($offset); |
|
360 | } else { |
||
361 | 2 | return $this->retrieveItem($offset); |
|
362 | } |
||
363 | } |
||
364 | |||
365 | 2 | public function offsetSet($offset, $value) |
|
366 | { |
||
367 | 2 | $this->dataSet = true; |
|
368 | 2 | $this->modelData[$offset] = $value; |
|
369 | 2 | } |
|
370 | |||
371 | public function offsetUnset($offset) |
||
372 | { |
||
373 | unset($this->modelData[$offset]); |
||
374 | } |
||
375 | |||
376 | 4 | private function wrap($offset) |
|
377 | { |
||
378 | 4 | $this->initialize(); |
|
379 | 4 | if (isset($this->modelData[$offset])) { |
|
380 | 4 | $className = $this->className; |
|
381 | 4 | $newInstance = new $className(); |
|
382 | 4 | $newInstance->initialize(); |
|
383 | 4 | $newInstance->setData($this->modelData[$offset]); |
|
384 | |||
385 | 4 | return $newInstance; |
|
386 | } else { |
||
387 | return; |
||
388 | } |
||
389 | } |
||
390 | |||
391 | 4 | public function getInvalidFields() |
|
392 | { |
||
393 | 4 | return $this->invalidFields; |
|
394 | } |
||
395 | |||
396 | public function getHasMany() |
||
397 | { |
||
398 | return $this->hasMany; |
||
399 | } |
||
400 | |||
401 | public function getBelongsTo() |
||
402 | { |
||
403 | return $this->belongsTo; |
||
404 | } |
||
405 | |||
406 | 2 | public function current() |
|
407 | { |
||
408 | 2 | return $this->wrap($this->keys[$this->index]); |
|
409 | } |
||
410 | |||
411 | public function key() |
||
412 | { |
||
413 | return $this->keys[$this->index]; |
||
414 | } |
||
415 | |||
416 | 2 | public function next() |
|
417 | { |
||
418 | 2 | $this->index++; |
|
419 | 2 | } |
|
420 | |||
421 | 2 | public function rewind() |
|
422 | { |
||
423 | 2 | $this->keys = array_keys($this->modelData); |
|
424 | 2 | $this->index = 0; |
|
425 | 2 | } |
|
426 | |||
427 | 2 | public function valid() |
|
428 | { |
||
429 | 2 | return isset($this->keys[$this->index]) && isset($this->modelData[$this->keys[$this->index]]); |
|
430 | } |
||
431 | |||
432 | /** |
||
433 | * A custom validator for the record wrapper. |
||
434 | * |
||
435 | * @return mixed |
||
436 | */ |
||
437 | 10 | public function onValidate($invalidFields) : array |
|
438 | { |
||
439 | 10 | return []; |
|
440 | } |
||
441 | |||
442 | 8 | private function fetchRelatedFields(Relationship $relationship, $index = null) |
|
443 | { |
||
444 | 8 | $data = $index ? $this->modelData[$index] : $this->modelData; |
|
445 | 8 | $name = $relationship->getOptions()['name']; |
|
446 | 8 | if(isset($data[$name])) |
|
447 | { |
||
448 | return $data[$name]; |
||
449 | } |
||
450 | 8 | $model = $relationship->getModelInstance(); |
|
451 | 8 | if (empty($data)) { |
|
452 | return $model; |
||
453 | } |
||
454 | 8 | $query = $relationship->prepareQuery($data); |
|
455 | |||
456 | 8 | return $query ? $model->fetch($query) : $model; |
|
457 | } |
||
458 | |||
459 | 24 | public function getRelationships() |
|
467 | |||
468 | public function usetField($field) |
||
472 | |||
473 | /** |
||
474 | * Callback for when a record is either added or modified. |
||
475 | */ |
||
476 | 10 | public function preSaveCallback() |
|
479 | |||
480 | /** |
||
481 | * Callback for when a record has been added or modified. |
||
482 | * |
||
483 | * @param $id |
||
484 | */ |
||
485 | 6 | public function postSaveCallback() |
|
488 | |||
489 | /** |
||
490 | * Callback for when a new record is about to be created. |
||
491 | */ |
||
492 | 8 | public function preCreateCallback() |
|
495 | |||
496 | /** |
||
497 | * Callback for when a new record has been created. |
||
498 | * This callback can be most useful for obtaining the primary key of a newly created record. |
||
499 | * |
||
500 | * @param $id |
||
501 | */ |
||
502 | 4 | public function postCreateCallback($id) |
|
505 | |||
506 | /** |
||
507 | * Callback for when a record is about to be updated. |
||
508 | */ |
||
509 | 2 | public function preUpdateCallback() |
|
512 | |||
513 | /** |
||
514 | * Callback for when a record has been updated. |
||
515 | */ |
||
516 | 2 | public function postUpdateCallback() |
|
519 | |||
520 | 32 | public function getDBStoreInformation() |
|
531 | |||
532 | /** |
||
533 | * @return DriverAdapter |
||
534 | */ |
||
535 | 32 | public function getAdapter() |
|
540 | |||
541 | 4 | private function expandArrayValue($array, $relationships, $depth, $expandableModels = [], $index = null) |
|
542 | { |
||
543 | 4 | $expandableModels = empty($expandableModels) ? array_keys($relationships) : $expandableModels; |
|
544 | // if (empty($expandableModels)) { |
||
545 | // foreach ($relationships as $name => $relationship) { |
||
546 | // $array[$name] = $this->fetchRelatedFields($relationship, $index)->toArray($depth); |
||
547 | // } |
||
548 | // } else { |
||
549 | 4 | foreach ($expandableModels as $name) { |
|
550 | 4 | $array[$name] = $this->fetchRelatedFields($relationships[$name], $index)->toArray($depth, $expandableModels); |
|
551 | } |
||
552 | // } |
||
553 | |||
554 | 4 | return $array; |
|
555 | } |
||
556 | |||
557 | 10 | public function getValidationRules() : array |
|
558 | { |
||
559 | 10 | return $this->validationRules; |
|
560 | } |
||
561 | |||
562 | 20 | public function toArray($depth = 0, $expandableModels = []) |
|
563 | { |
||
564 | 20 | $relationships = $this->getDescription()->getRelationships(); |
|
565 | 20 | $array = $this->modelData; |
|
566 | 20 | if (!empty($array) && $depth > 0) { |
|
567 | 4 | if ($this->hasMultipleItems()) { |
|
568 | foreach ($array as $i => $value) { |
||
569 | $array[$i] = $this->expandArrayValue($value, $relationships, $depth - 1, $expandableModels, $i); |
||
578 | } |
||
579 |
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..