Complex classes like DetectsChanges 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 DetectsChanges, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 10 | trait DetectsChanges |
||
| 11 | { |
||
| 12 | protected $oldAttributes = []; |
||
| 13 | |||
| 14 | 216 | protected static function bootDetectsChanges() |
|
| 33 | 8 | ||
| 34 | public function attributesToBeLogged(): array |
||
| 60 | |||
| 61 | 60 | public function shouldLogOnlyDirty(): bool |
|
| 69 | |||
| 70 | 8 | public function shouldLogUnguarded(): bool |
|
| 86 | |||
| 87 | 140 | public function attributeValuesToBeLogged(string $processingEvent): array |
|
| 88 | 140 | { |
|
| 89 | 140 | if (! count($this->attributesToBeLogged())) { |
|
| 90 | 140 | return []; |
|
| 91 | } |
||
| 92 | |||
| 93 | 140 | $properties['attributes'] = static::logChanges( |
|
| 94 | 88 | $processingEvent == 'retrieved' |
|
| 95 | ? $this |
||
| 96 | 88 | : ( |
|
| 97 | $this->exists |
||
| 98 | 88 | ? $this->fresh() ?? $this |
|
| 99 | : $this |
||
| 100 | ) |
||
| 101 | 140 | ); |
|
| 102 | 56 | ||
| 103 | 56 | if (static::eventsToBeRecorded()->contains('updated') && $processingEvent == 'updated') { |
|
| 104 | 56 | $nullProperties = array_fill_keys(array_keys($properties['attributes']), null); |
|
| 105 | |||
| 106 | 56 | $properties['old'] = array_merge($nullProperties, $this->oldAttributes); |
|
| 107 | 20 | ||
| 108 | $this->oldAttributes = []; |
||
| 109 | } |
||
| 110 | 52 | ||
| 111 | 56 | if ($this->shouldLogOnlyDirty() && isset($properties['old'])) { |
|
| 112 | $properties['attributes'] = array_udiff_assoc( |
||
| 113 | 56 | $properties['attributes'], |
|
| 114 | 56 | $properties['old'], |
|
| 115 | 56 | function ($new, $old) { |
|
| 116 | if ($old === null || $new === null) { |
||
| 117 | return $new === $old ? 0 : 1; |
||
| 118 | 140 | } |
|
| 119 | |||
| 120 | return $new <=> $old; |
||
| 121 | 164 | } |
|
| 122 | ); |
||
| 123 | 164 | $properties['old'] = collect($properties['old']) |
|
| 124 | 164 | ->only(array_keys($properties['attributes'])) |
|
| 125 | ->all(); |
||
| 126 | 164 | } |
|
| 127 | 140 | ||
| 128 | 24 | return $properties; |
|
| 129 | 140 | } |
|
| 130 | 24 | ||
| 131 | 24 | public static function logChanges(Model $model): array |
|
| 132 | 24 | { |
|
| 133 | 24 | $changes = []; |
|
| 134 | $attributes = $model->attributesToBeLogged(); |
||
| 135 | |||
| 136 | 140 | foreach ($attributes as $attribute) { |
|
| 137 | if (Str::contains($attribute, '.')) { |
||
| 138 | $changes += self::getRelatedModelAttributeValue($model, $attribute); |
||
| 139 | 140 | ||
| 140 | 140 | continue; |
|
| 141 | } |
||
| 142 | 32 | ||
| 143 | 32 | if (Str::contains($attribute, '->')) { |
|
| 144 | Arr::set( |
||
| 145 | $changes, |
||
| 146 | str_replace('->', '.', $attribute), |
||
| 147 | static::getModelAttributeJsonValue($model, $attribute) |
||
| 148 | ); |
||
| 149 | 164 | ||
| 150 | continue; |
||
| 151 | } |
||
| 152 | 24 | ||
| 153 | $changes[$attribute] = $model->getAttribute($attribute); |
||
| 154 | 24 | ||
| 155 | if (is_null($changes[$attribute])) { |
||
| 156 | continue; |
||
| 157 | } |
||
| 158 | 24 | ||
| 159 | if ($model->isDateAttribute($attribute)) { |
||
| 160 | 24 | $changes[$attribute] = $model->serializeDate( |
|
| 161 | $model->asDateTime($changes[$attribute]) |
||
| 162 | 24 | ); |
|
| 163 | } |
||
| 164 | 24 | ||
| 165 | if ($model->hasCast($attribute)) { |
||
| 166 | $cast = $model->getCasts()[$attribute]; |
||
| 167 | 24 | ||
| 168 | if ($model->isCustomDateTimeCast($cast)) { |
||
| 169 | 24 | $changes[$attribute] = $model->asDateTime($changes[$attribute])->format(explode(':', $cast, 2)[1]); |
|
| 170 | 24 | } |
|
| 171 | 24 | } |
|
| 172 | } |
||
| 173 | 24 | ||
| 174 | return $changes; |
||
| 175 | } |
||
| 176 | |||
| 177 | protected static function getRelatedModelAttributeValue(Model $model, string $attribute): array |
||
| 178 | { |
||
| 179 | if (substr_count($attribute, '.') > 1) { |
||
| 180 | throw CouldNotLogChanges::invalidAttribute($attribute); |
||
| 181 | } |
||
| 182 | |||
| 183 | [$relatedModelName, $relatedAttribute] = explode('.', $attribute); |
||
| 184 | |||
| 185 | $relatedModelName = Str::camel($relatedModelName); |
||
| 186 | |||
| 187 | $relatedModel = $model->$relatedModelName ?? $model->$relatedModelName(); |
||
| 188 | |||
| 189 | return ["{$relatedModelName}.{$relatedAttribute}" => $relatedModel->$relatedAttribute ?? null]; |
||
| 190 | } |
||
| 191 | |||
| 192 | protected static function getModelAttributeJsonValue(Model $model, string $attribute) |
||
| 200 | } |
||
| 201 |
This check looks for methods that are used by a trait but not required by it.
To illustrate, let’s look at the following code example
The trait
Idableprovides a methodequalsIdthat in turn relies on the methodgetId(). If this method does not exist on a class mixing in this trait, the method will fail.Adding the
getId()as an abstract method to the trait will make sure it is available.