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
Idable
provides a methodequalsId
that 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.