Complex classes like RelatedPlusTrait 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 RelatedPlusTrait, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
27 | trait RelatedPlusTrait |
||
28 | { |
||
29 | use CustomOrderTrait, HelperMethodTrait; |
||
30 | |||
31 | /** |
||
32 | * Get the table associated with the model. |
||
33 | * |
||
34 | * @return string |
||
35 | */ |
||
36 | abstract public function getTable(); |
||
37 | |||
38 | /** |
||
39 | * Boot method for trait |
||
40 | * |
||
41 | */ |
||
42 | public static function bootRelatedPlusTrait() |
||
43 | { |
||
44 | static::saving(function ($model) { |
||
45 | if (!empty($model->nullable)) { |
||
46 | foreach ($model->attributes as $key => $value) { |
||
47 | if (isset($model->nullable[$key])) { |
||
48 | $model->{$key} = empty(trim($value)) ? null : $value; |
||
49 | } |
||
50 | } |
||
51 | } |
||
52 | }); |
||
53 | } |
||
54 | |||
55 | /** |
||
56 | * Add joins for one or more relations |
||
57 | * This determines the foreign key relations automatically to prevent the need to figure out the columns. |
||
58 | * Usages: |
||
59 | * $query->modelJoin('customers') |
||
60 | * $query->modelJoin('customer.client') |
||
61 | * |
||
62 | * @param Builder $query |
||
63 | * @param string $relationName |
||
64 | * @param string $operator |
||
65 | * @param string $type |
||
66 | * @param bool $where |
||
67 | * @param bool $relatedSelect |
||
68 | * @param string|null $direction |
||
69 | * |
||
70 | * @return Builder |
||
71 | */ |
||
72 | public function scopeModelJoin( |
||
73 | Builder $query, |
||
74 | $relationName, |
||
75 | $operator = '=', |
||
76 | $type = 'left', |
||
77 | $where = false, |
||
78 | $relatedSelect = true, |
||
79 | $direction = null |
||
80 | ) { |
||
81 | $connection = $this->connection; |
||
82 | |||
83 | foreach ($this->parseRelationNames($relationName) as $relation) { |
||
84 | $tableName = $relation->getRelated()->getTable(); |
||
85 | // if using a 'table' AS 'tableAlias' in a from statement, otherwise alias will be the table name |
||
86 | $from = explode(' ', $relation->getQuery()->getQuery()->from); |
||
87 | $tableAlias = array_pop($from); |
||
88 | |||
89 | /** @var Model $query */ |
||
90 | if (empty($query->getQuery()->columns)) { |
||
91 | $query->select($this->getTable() . ".*"); |
||
92 | } |
||
93 | if ($relatedSelect) { |
||
94 | foreach (Schema::connection($connection)->getColumnListing($tableName) as $relatedColumn) { |
||
95 | $query->addSelect( |
||
96 | new Expression("`$tableAlias`.`$relatedColumn` AS `$tableAlias.$relatedColumn`") |
||
97 | ); |
||
98 | } |
||
99 | } |
||
100 | $query->relationJoin($tableName, $tableAlias, $relation, $operator, $type, $where, $direction); |
||
101 | } |
||
102 | |||
103 | return $query; |
||
104 | } |
||
105 | |||
106 | /** |
||
107 | * Join a model |
||
108 | * |
||
109 | * @param Builder $query |
||
110 | * @param string $tableName |
||
111 | * @param string $tableAlias |
||
112 | * @param Relation $relation |
||
113 | * @param string $operator |
||
114 | * @param string $type |
||
115 | * @param boolean $where |
||
116 | * @param null $direction |
||
117 | * @return Builder |
||
118 | */ |
||
119 | public function scopeRelationJoin( |
||
120 | Builder $query, |
||
121 | $tableName, |
||
122 | $tableAlias, |
||
123 | $relation, |
||
124 | $operator, |
||
125 | $type, |
||
126 | $where, |
||
127 | $direction = null |
||
128 | ) { |
||
129 | if ($tableAlias !== '' && $tableName !== $tableAlias) { |
||
130 | $fullTableName = $tableName . ' AS ' . $tableAlias; |
||
131 | } else { |
||
132 | $fullTableName = $tableName; |
||
133 | } |
||
134 | |||
135 | return $query->join($fullTableName, function (JoinClause $join) use ( |
||
136 | $tableName, |
||
137 | $tableAlias, |
||
138 | $relation, |
||
139 | $operator, |
||
140 | $direction |
||
141 | ) { |
||
142 | // If a HasOne relation and ordered - ie join to the latest/earliest |
||
143 | if (class_basename($relation) === 'HasOne' && !empty($relation->toBase()->orders)) { |
||
144 | return $this->hasOneJoin($relation, $join); |
||
145 | } else { |
||
146 | return $this->hasManyJoin($relation, $join, $tableName, $tableAlias, $operator, $direction); |
||
147 | } |
||
148 | }, null, null, $type, $where); |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * Join a HasOne relation which is ordered |
||
153 | * |
||
154 | * @param Relation $relation |
||
155 | * @param JoinClause $join |
||
156 | * @return JoinClause |
||
157 | */ |
||
158 | private function hasOneJoin($relation, $join) |
||
159 | { |
||
160 | // Get first relation order (should only be one) |
||
161 | $order = $relation->toBase()->orders[0]; |
||
162 | |||
163 | return $join->on($order['column'], $this->hasOneJoinSql($relation, $order)); |
||
164 | } |
||
165 | |||
166 | /** |
||
167 | * Get join sql for a HasOne relation |
||
168 | * |
||
169 | * @param Relation $relation |
||
170 | * @param array $order |
||
171 | * @return Expression |
||
172 | */ |
||
173 | public function hasOneJoinSql($relation, $order) |
||
174 | { |
||
175 | // Build subquery for getting first/last record in related table |
||
176 | $subQuery = $this |
||
177 | ->joinOne( |
||
178 | $relation->getRelated()->newQuery(), |
||
179 | $relation, |
||
180 | $order['column'], |
||
181 | $order['direction'] |
||
182 | ) |
||
183 | ->setBindings($relation->getBindings()); |
||
184 | |||
185 | return DB::raw('(' . $this->toSqlWithBindings($subQuery) . ')'); |
||
186 | } |
||
187 | |||
188 | /** |
||
189 | * Adds a where for a relation's join columns and and min/max for a given column |
||
190 | * |
||
191 | * @param Builder $query |
||
192 | * @param Relation $relation |
||
193 | * @param string $column |
||
194 | * @param string $direction |
||
195 | * @return Builder |
||
196 | */ |
||
197 | public function joinOne($query, $relation, $column, $direction) |
||
198 | { |
||
199 | // Get join fields |
||
200 | $joinColumns = $this->getJoinColumns($relation); |
||
201 | |||
202 | return $this->selectMinMax( |
||
203 | $query->whereColumn($joinColumns->first, '=', $joinColumns->second), |
||
204 | $column, |
||
205 | $direction |
||
206 | ); |
||
207 | } |
||
208 | |||
209 | /** |
||
210 | * Get the join columns for a relation |
||
211 | * |
||
212 | * @param Relation|BelongsTo|HasOneOrMany $relation |
||
213 | * @return \stdClass |
||
214 | */ |
||
215 | protected function getJoinColumns($relation) |
||
216 | { |
||
217 | // Get keys with table names |
||
218 | if ($relation instanceof BelongsTo) { |
||
219 | $first = $relation->getOwnerKey(); |
||
220 | $second = $relation->getForeignKey(); |
||
221 | } else { |
||
222 | $first = $relation->getQualifiedParentKeyName(); |
||
223 | $second = $relation->getQualifiedForeignKeyName(); |
||
224 | } |
||
225 | |||
226 | return (object)['first' => $first, 'second' => $second]; |
||
227 | } |
||
228 | |||
229 | /** |
||
230 | * Adds a select for a min or max on the given column, depending on direction given |
||
231 | * |
||
232 | * @param Builder $query |
||
233 | * @param string $column |
||
234 | * @param string $direction |
||
235 | * @return Builder |
||
236 | */ |
||
237 | public function selectMinMax($query, $column, $direction) |
||
238 | { |
||
239 | $column = $this->addBackticks($column); |
||
240 | |||
241 | /** @var Model $query */ |
||
242 | if ($direction == 'asc') { |
||
243 | return $query->select(DB::raw('MIN(' . $column . ')')); |
||
244 | } else { |
||
245 | return $query->select(DB::raw('MAX(' . $column . ')')); |
||
246 | } |
||
247 | } |
||
248 | |||
249 | /** |
||
250 | * Join a HasMany Relation |
||
251 | * |
||
252 | * @param Relation $relation |
||
253 | * @param JoinClause $join |
||
254 | * @param string $tableName |
||
255 | * @param string $tableAlias |
||
256 | * @param string $operator |
||
257 | * @param string $direction |
||
258 | * @return Builder|JoinClause |
||
259 | */ |
||
260 | private function hasManyJoin($relation, $join, $tableName, $tableAlias, $operator, $direction) |
||
261 | { |
||
262 | // Get relation join columns |
||
263 | $joinColumns = $this->getJoinColumns($relation); |
||
264 | |||
265 | $first = $joinColumns->first; |
||
266 | $second = $joinColumns->second; |
||
267 | if ($tableName !== $tableAlias) { |
||
268 | $first = str_replace($tableName, $tableAlias, $first); |
||
269 | $second = str_replace($tableName, $tableAlias, $second); |
||
270 | } |
||
271 | |||
272 | $join->on($first, $operator, $second); |
||
273 | |||
274 | // Add any where clauses from the relationship |
||
275 | $join = $this->addWhereConstraints($join, $relation, $tableAlias); |
||
276 | |||
277 | if (!is_null($direction) && get_class($relation) === HasMany::class) { |
||
278 | $join = $this->hasManyJoinWhere($join, $first, $relation, $tableAlias, $direction); |
||
|
|||
279 | } |
||
280 | |||
281 | return $join; |
||
282 | } |
||
283 | |||
284 | /** |
||
285 | * Add wheres if they exist for a relation |
||
286 | * |
||
287 | * @param Builder|JoinClause $builder |
||
288 | * @param Relation|BelongsTo|HasOneOrMany $relation |
||
289 | * @param string $table |
||
290 | * @return Builder|JoinClause $builder |
||
291 | */ |
||
292 | protected function addWhereConstraints($builder, $relation, $table) |
||
293 | { |
||
294 | // Get where clauses from the relationship |
||
295 | $wheres = collect($relation->toBase()->wheres) |
||
296 | ->where('type', 'Basic') |
||
297 | ->map(function ($where) use ($table) { |
||
298 | // Add table name to column if it is absent |
||
299 | return [$this->columnWithTableName($table, $where['column']), $where['operator'], $where['value']]; |
||
300 | })->toArray(); |
||
301 | |||
302 | if (!empty($wheres)) { |
||
303 | $builder->where($wheres); |
||
304 | } |
||
305 | |||
306 | return $builder; |
||
307 | } |
||
308 | |||
309 | /** |
||
310 | * If the relation is one-to-many, just get the first related record |
||
311 | * |
||
312 | * @param JoinClause $joinClause |
||
313 | * @param string $column |
||
314 | * @param HasMany|Relation $relation |
||
315 | * @param string $table |
||
316 | * @param string $direction |
||
317 | * |
||
318 | * @return JoinClause |
||
319 | */ |
||
320 | public function hasManyJoinWhere(JoinClause $joinClause, $column, $relation, $table, $direction) |
||
321 | { |
||
322 | return $joinClause->where( |
||
323 | $column, |
||
324 | function ($subQuery) use ($table, $direction, $relation, $column) { |
||
325 | $subQuery = $this->joinOne( |
||
326 | $subQuery->from($table), |
||
327 | $relation, |
||
328 | $column, |
||
329 | $direction |
||
330 | ); |
||
331 | |||
332 | // Add any where statements with the relationship |
||
333 | $subQuery = $this->addWhereConstraints($subQuery, $relation, $table); |
||
334 | |||
335 | // Add any order statements with the relationship |
||
336 | return $this->addOrder($subQuery, $relation, $table); |
||
337 | } |
||
338 | ); |
||
339 | } |
||
340 | |||
341 | /** |
||
342 | * Add orderBy if orders exist for a relation |
||
343 | * |
||
344 | * @param Builder|JoinClause $builder |
||
345 | * @param Relation|BelongsTo|HasOneOrMany $relation |
||
346 | * @param string $table |
||
347 | * @return Builder |
||
348 | */ |
||
349 | protected function addOrder($builder, $relation, $table) |
||
350 | { |
||
351 | /** @var Model $builder */ |
||
352 | if (!empty($relation->toBase()->orders)) { |
||
353 | // Get where clauses from the relationship |
||
354 | foreach ($relation->toBase()->orders as $order) { |
||
355 | $builder->orderBy($this->columnWithTableName($table, $order['column']), $order['direction']); |
||
356 | } |
||
357 | } |
||
358 | |||
359 | return $builder; |
||
360 | } |
||
361 | |||
362 | /** |
||
363 | * Set the order of a model |
||
364 | * |
||
365 | * @param Builder $query |
||
366 | * @param string $orderField |
||
367 | * @param string $direction |
||
368 | * @return Builder |
||
369 | */ |
||
370 | public function scopeOrderByCustom(Builder $query, $orderField, $direction) |
||
378 | |||
379 | /** |
||
380 | * Check $order_fields and $order_defaults are set |
||
381 | * |
||
382 | * @param string $orderField |
||
383 | * @param string $direction |
||
384 | * @return bool |
||
385 | */ |
||
386 | private function hasFieldsAndDefaults($orderField, $direction) |
||
399 | |||
400 | /** |
||
401 | * Join a related table if not already joined |
||
402 | * |
||
403 | * @param Builder $query |
||
404 | * @param string $table |
||
405 | * @return Builder |
||
406 | */ |
||
407 | private function joinRelatedTable($query, $table) |
||
408 | { |
||
409 | if (isset($this->order_relations[$table]) && |
||
410 | !$this->hasJoin($query, $table, $this->order_relations[$table])) { |
||
411 | $columnRelations = $this->order_relations[$table]; |
||
412 | |||
424 | |||
425 | /** |
||
426 | * Switch a query to be a subquery of a model |
||
427 | * |
||
428 | * @param Builder $query |
||
429 | * @param Builder $model |
||
430 | * @return Builder |
||
431 | */ |
||
432 | public function scopeSetSubquery(Builder $query, $model) |
||
441 | |||
442 | /** |
||
443 | * Use a model method to add columns or joins if in the order options |
||
444 | * |
||
445 | * @param Builder $query |
||
446 | * @param string $order |
||
447 | * @return Builder |
||
448 | */ |
||
449 | public function scopeOrderByWith(Builder $query, $order) |
||
473 | |||
474 | /** |
||
475 | * Add where statements for the model search fields |
||
476 | * |
||
477 | * @param Builder $query |
||
478 | * @param string $searchText |
||
479 | * @return Builder |
||
480 | */ |
||
481 | public function scopeSearch(Builder $query, $searchText = '') |
||
496 | |||
497 | /** |
||
498 | * Add where statements for search fields to search for searchText |
||
499 | * |
||
500 | * @param Builder $query |
||
501 | * @param string $searchText |
||
502 | * @return Builder |
||
503 | */ |
||
504 | private function checkSearchFields($query, $searchText) |
||
518 | |||
519 | /** |
||
520 | * Add where statement for a search field |
||
521 | * |
||
522 | * @param Builder $query |
||
523 | * @param string $table |
||
524 | * @param string $searchField |
||
525 | * @param array $searchFieldParameters |
||
526 | * @param string $searchText |
||
527 | * @return Builder |
||
528 | */ |
||
529 | private function checkSearchField($query, $table, $searchField, $searchFieldParameters, $searchText) |
||
543 | |||
544 | /** |
||
545 | * Add where condition to search a relation |
||
546 | * |
||
547 | * @param Builder $query |
||
548 | * @param array $searchFieldParameters |
||
549 | * @param string $searchColumn |
||
550 | * @param string $searchText |
||
551 | * @return Builder |
||
552 | */ |
||
553 | private function searchRelation(Builder $query, $searchFieldParameters, $searchColumn, $searchText) |
||
575 | |||
576 | /** |
||
577 | * Add where condition to search current model |
||
578 | * |
||
579 | * @param Builder $query |
||
580 | * @param array $searchFieldParameters |
||
581 | * @param string $table |
||
582 | * @param string $searchColumn |
||
583 | * @param string $searchText |
||
584 | * @return Builder |
||
585 | */ |
||
586 | public function searchThis(Builder $query, $searchFieldParameters, $table, $searchColumn, $searchText) |
||
597 | } |
||
598 |
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.