Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like QueryBuilder 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 QueryBuilder, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
26 | class QueryBuilder implements Countable |
||
27 | { |
||
28 | const COL_HAVING = 'having'; |
||
29 | const COL_WHERE = 'where'; |
||
30 | |||
31 | /** |
||
32 | * The type of the model we're building a query for |
||
33 | * @var string |
||
34 | */ |
||
35 | protected $type; |
||
36 | |||
37 | /** |
||
38 | * The columns that the model provided us |
||
39 | * @var array |
||
40 | */ |
||
41 | protected $columns = array('id' => 'id'); |
||
42 | |||
43 | /** |
||
44 | * Extra columns that are generated from the SQL query (this should be a comma separated string or null) |
||
45 | * @var string|null |
||
46 | */ |
||
47 | protected $extraColumns = null; |
||
48 | |||
49 | /** |
||
50 | * The conditions to include in WHERE |
||
51 | * @var string[] |
||
52 | */ |
||
53 | protected $whereConditions = array(); |
||
54 | |||
55 | /** |
||
56 | * The conditions to include in HAVING |
||
57 | * @var string[] |
||
58 | */ |
||
59 | protected $havingConditions = array(); |
||
60 | |||
61 | /** |
||
62 | * The MySQL value parameters |
||
63 | * @var array |
||
64 | */ |
||
65 | protected $parameters = array(); |
||
66 | |||
67 | /** |
||
68 | * The MySQL value parameters for pagination |
||
69 | * @var array |
||
70 | */ |
||
71 | protected $paginationParameters = array(); |
||
72 | |||
73 | /** |
||
74 | * Extra MySQL query string to pass |
||
75 | * @var string |
||
76 | */ |
||
77 | protected $extras = ''; |
||
78 | |||
79 | /** |
||
80 | * Extra MySQL query groupby string to pass |
||
81 | * @var string |
||
82 | */ |
||
83 | protected $groupQuery = ''; |
||
84 | |||
85 | /** |
||
86 | * A column based on which we should sort the results |
||
87 | * @var string|null |
||
88 | */ |
||
89 | private $sortBy = null; |
||
90 | |||
91 | /** |
||
92 | * Whether to reverse the results |
||
93 | * @var bool |
||
94 | */ |
||
95 | private $reverseSort = false; |
||
96 | |||
97 | /** |
||
98 | * The currently selected column |
||
99 | * @var string|null |
||
100 | */ |
||
101 | private $currentColumn = null; |
||
102 | |||
103 | /** |
||
104 | * Either 'where' or 'having' |
||
105 | * @var string |
||
106 | */ |
||
107 | private $currentColumnMode = ''; |
||
108 | |||
109 | /** |
||
110 | * The currently selected column without the table name (unless it was |
||
111 | * explicitly provided) |
||
112 | * @var string|null |
||
113 | */ |
||
114 | protected $currentColumnRaw = null; |
||
115 | |||
116 | /** |
||
117 | * A column to consider the name of the model |
||
118 | * @var string|null |
||
119 | */ |
||
120 | private $nameColumn = null; |
||
121 | |||
122 | /** |
||
123 | * The page to return |
||
124 | * @var int|null |
||
125 | */ |
||
126 | private $page = null; |
||
127 | |||
128 | /** |
||
129 | * Whether the ID of the first/last element has been provided |
||
130 | * @var bool |
||
131 | */ |
||
132 | private $limited = false; |
||
133 | |||
134 | /** |
||
135 | * The number of elements on every page |
||
136 | * @var int |
||
137 | */ |
||
138 | protected $resultsPerPage = 30; |
||
139 | |||
140 | /** |
||
141 | * Create a new QueryBuilder |
||
142 | * |
||
143 | * A new query builder should be created on a static getQueryBuilder() |
||
144 | * method on each model. The options array can contain the following |
||
145 | * properties: |
||
146 | * |
||
147 | * - `columns`: An associative array - the key of each entry is the column |
||
148 | * name that will be used by other methods, while the value is |
||
149 | * is the column name that is used in the database structure |
||
150 | * |
||
151 | * - `activeStatuses`: If the model has a status column, this should be |
||
152 | * a list of values that make the entry be considered |
||
153 | * "active" |
||
154 | * |
||
155 | * - `name`: The name of the column which represents the name of the object |
||
156 | * |
||
157 | * @param string $type The type of the Model (e.g. "Player" or "Match") |
||
158 | * @param array $options The options to pass to the builder (see above) |
||
159 | */ |
||
160 | 27 | public function __construct($type, $options = array()) |
|
172 | |||
173 | /** |
||
174 | * Add a new column to select by in the query. |
||
175 | * |
||
176 | * @param string $alias An alias that can be used in the query builder |
||
177 | * @param string|null $columnName The name of the column we're accessing |
||
178 | * |
||
179 | * @return $this |
||
180 | */ |
||
181 | 5 | public function selectColumn($alias, $columnName = null) |
|
191 | |||
192 | /** |
||
193 | * Select a column |
||
194 | * |
||
195 | * `$queryBuilder->where('username')->equals('administrator');` |
||
196 | * |
||
197 | * @param string $column The column to select |
||
198 | * @return static |
||
199 | */ |
||
200 | public function where($column) |
||
204 | |||
205 | 3 | /** |
|
206 | * Select an alias from an aggregate function |
||
207 | 3 | * |
|
208 | * `$queryBuilder->where('activity')->greaterThan(0);` |
||
209 | 3 | * |
|
210 | * @param string $column The column to select |
||
211 | * @return static |
||
212 | */ |
||
213 | public function having($column) |
||
217 | |||
218 | 2 | /** |
|
219 | * Request that a column equals a string (case-insensitive) |
||
220 | 2 | * |
|
221 | * @param string $string The string that the column's value should equal to |
||
222 | 2 | * @return static |
|
223 | */ |
||
224 | public function equals($string) |
||
230 | 1 | ||
231 | /** |
||
232 | 1 | * Request that a column doesNOT equals a string (case-insensitive) |
|
233 | * |
||
234 | 1 | * @param string $string The string that the column's value should equal to |
|
235 | * @return static |
||
236 | */ |
||
237 | public function notEquals($string) |
||
243 | |||
244 | /** |
||
245 | * Request that a column is not null |
||
246 | * |
||
247 | * @return static |
||
248 | */ |
||
249 | public function isNotNull() |
||
255 | |||
256 | /** |
||
257 | * Request that a column is null |
||
258 | * |
||
259 | * @return static |
||
260 | */ |
||
261 | public function isNull() |
||
267 | |||
268 | /** |
||
269 | * Request that a column is greater than a quantity |
||
270 | * |
||
271 | * @param string $quantity The quantity to test against |
||
272 | * @return static |
||
273 | */ |
||
274 | public function greaterThan($quantity) |
||
280 | |||
281 | /** |
||
282 | * Request that a column is less than a quantity |
||
283 | * |
||
284 | * @param string $quantity The quantity to test against |
||
285 | * @return static |
||
286 | */ |
||
287 | public function lessThan($quantity) |
||
293 | |||
294 | /** |
||
295 | * Request that a timestamp is before the specified time |
||
296 | * |
||
297 | * @param string|TimeDate $time The timestamp to compare to |
||
298 | 2 | * @param bool $inclusive Whether to include the given timestamp |
|
299 | * @param bool $reverse Whether to reverse the results |
||
300 | 2 | * |
|
301 | 2 | * @return static |
|
302 | */ |
||
303 | public function isBefore($time, $inclusive = false, $reverse = false) |
||
307 | 2 | ||
308 | /** |
||
309 | 2 | * Request that a timestamp is after the specified time |
|
310 | * |
||
311 | * @param string|TimeDate $time The timestamp to compare to |
||
312 | * @param bool $inclusive Whether to include the given timestamp |
||
313 | * @param bool $reverse Whether to reverse the results |
||
314 | * |
||
315 | * @return static |
||
316 | */ |
||
317 | public function isAfter($time, $inclusive = false, $reverse = false) |
||
330 | |||
331 | /** |
||
332 | 1 | * Request that a column equals a number |
|
333 | * |
||
334 | 1 | * @param int|Model|null $number The number that the column's value should |
|
335 | * equal to. If a Model is provided, use the |
||
336 | * model's ID, while null values are ignored. |
||
337 | * @return static |
||
338 | 1 | */ |
|
339 | 1 | View Code Duplication | public function is($number) |
353 | |||
354 | 3 | public function isLike($string) |
|
364 | 3 | ||
365 | /** |
||
366 | * Request that a column equals one of some strings |
||
367 | * |
||
368 | * @todo Improve for PDO |
||
369 | * @param string[] $strings The list of accepted values for the column |
||
370 | * @return static |
||
371 | */ |
||
372 | public function isOneOf($strings) |
||
384 | |||
385 | /** |
||
386 | * Request that a column value starts with a string (case-insensitive) |
||
387 | * |
||
388 | * @param string $string The substring that the column's value should start with |
||
389 | * @return static |
||
390 | */ |
||
391 | public function startsWith($string) |
||
397 | |||
398 | /** |
||
399 | * Request that a specific model is not returned |
||
400 | * |
||
401 | * @param Model|int $model The ID or model you don't want to get |
||
402 | * @return static |
||
403 | */ |
||
404 | 3 | View Code Duplication | public function except($model) |
415 | |||
416 | /** |
||
417 | * Return the results sorted by the value of a column |
||
418 | * |
||
419 | * @param string $column The column based on which the results should be ordered |
||
420 | * @return static |
||
421 | */ |
||
422 | 2 | public function sortBy($column) |
|
432 | |||
433 | /** |
||
434 | * Reverse the order |
||
435 | 2 | * |
|
436 | * Note: This only works if you have specified a column in the sortBy() method |
||
437 | 2 | * |
|
438 | * @return static |
||
439 | 2 | */ |
|
440 | public function reverse() |
||
446 | |||
447 | /** |
||
448 | 2 | * Specify the number of results per page |
|
449 | * |
||
450 | 2 | * @param int $count The number of results |
|
451 | * @return static |
||
452 | 2 | */ |
|
453 | public function limit($count) |
||
459 | |||
460 | /** |
||
461 | * Only show results from a specific page. This will |
||
462 | * |
||
463 | 1 | * @param int|null $page The page number (or null to show all pages - counting starts from 0) |
|
464 | * @return static |
||
465 | 1 | */ |
|
466 | public function fromPage($page) |
||
480 | |||
481 | /** |
||
482 | * End with a specific result |
||
483 | * |
||
484 | * @param int|Model $model The model (or database ID) after the first result |
||
485 | * @param bool $inclusive Whether to include the provided model |
||
486 | * @param bool $reverse Whether to reverse the results |
||
487 | * @return static |
||
488 | */ |
||
489 | public function endAt($model, $inclusive = false, $reverse = false) |
||
493 | |||
494 | /** |
||
495 | * Start with a specific result |
||
496 | * |
||
497 | * @param int|Model $model The model (or database ID) before the first result |
||
498 | * @param bool $inclusive Whether to include the provided model |
||
499 | * @param bool $reverse Whether to reverse the results |
||
500 | * @return static |
||
501 | */ |
||
502 | public function startAt($model, $inclusive = false, $reverse = false) |
||
529 | |||
530 | /** |
||
531 | 1 | * Request that only "active" Models should be returned |
|
532 | * |
||
533 | 1 | * @return static |
|
534 | */ |
||
535 | 1 | public function active() |
|
545 | |||
546 | /** |
||
547 | * Make sure that Models invisible to a player are not returned |
||
548 | * |
||
549 | * Note that this method does not take PermissionModel::canBeSeenBy() into |
||
550 | * consideration for performance purposes, so you will have to override this |
||
551 | * in your query builder if necessary. |
||
552 | * |
||
553 | * @param Player $player The player in question |
||
554 | * @param bool $showDeleted false to hide deleted models even from admins |
||
555 | 1 | * @return static |
|
556 | */ |
||
557 | 1 | public function visibleTo($player, $showDeleted = false) |
|
575 | 1 | ||
576 | 1 | /** |
|
577 | * Perform the query and get back the results in an array of names |
||
578 | * |
||
579 | 1 | * @return string[] An array of the type $id => $name |
|
580 | */ |
||
581 | 1 | public function getNames() |
|
591 | |||
592 | /** |
||
593 | * Perform the query and get back the results in a list of arrays |
||
594 | * |
||
595 | * @todo Play with partial models? |
||
596 | * @param string|string[] $columns The column(s) that should be returned |
||
597 | * @return array[] |
||
598 | */ |
||
599 | public function getArray($columns) |
||
609 | 27 | ||
610 | 27 | /** |
|
611 | * An alias for QueryBuilder::getModels(), with fast fetching on by default |
||
612 | 27 | * and no return of results |
|
613 | * |
||
614 | 27 | * @param bool $fastFetch Whether to perform one query to load all |
|
615 | * the model data instead of fetching them |
||
616 | * one by one |
||
617 | * @return void |
||
618 | */ |
||
619 | 27 | public function addToCache($fastFetch = true) |
|
623 | 25 | ||
624 | /** |
||
625 | 3 | * Perform the query and get the results as Models |
|
626 | * |
||
627 | * @todo Fix fast fetch for queries with multiple tables |
||
628 | * @param bool $fastFetch Whether to perform one query to load all |
||
629 | * the model data instead of fetching them |
||
630 | * one by one (ignores cache) |
||
631 | * @return array |
||
632 | */ |
||
633 | public function getModels($fastFetch = false) |
||
654 | |||
655 | /** |
||
656 | * Count the results |
||
657 | * |
||
658 | * @return int |
||
659 | */ |
||
660 | public function count() |
||
673 | |||
674 | /** |
||
675 | 1 | * Count the number of pages that all the models could be separated into |
|
676 | */ |
||
677 | 1 | public function countPages() |
|
681 | |||
682 | /** |
||
683 | * Find if there is any result |
||
684 | * |
||
685 | * @return bool |
||
686 | */ |
||
687 | public function any() |
||
696 | |||
697 | /** |
||
698 | 5 | * Get the amount of results that are returned per page |
|
699 | * @return int |
||
700 | 1 | */ |
|
701 | public function getResultsPerPage() |
||
705 | 5 | ||
706 | /** |
||
707 | * Select a column to perform opeations on |
||
708 | * |
||
709 | * This is identical to the `where()` method, except that the column is |
||
710 | * specified as a MySQL column and not as a column name given by the model |
||
711 | * |
||
712 | * @param string $column The column to select |
||
713 | * @param string $mode Whether this column is static or is a column from an aggregate function; Either 'having' or 'where' |
||
714 | 5 | * |
|
715 | * @return static |
||
716 | 5 | */ |
|
717 | protected function column($column, $mode = self::COL_WHERE) |
||
733 | 5 | ||
734 | /** |
||
735 | * Add a condition for the column |
||
736 | * @param string $condition The MySQL condition |
||
737 | * @param mixed $value Value(s) to pass to MySQL |
||
738 | * @return void |
||
739 | */ |
||
740 | protected function addColumnCondition($condition, $value) |
||
760 | |||
761 | /** |
||
762 | 27 | * Get the MySQL extra parameters |
|
763 | * |
||
764 | 27 | * @param bool $respectPagination Whether to respect pagination or not; useful for when pagination should be ignored such as count |
|
765 | * @return string |
||
766 | */ |
||
767 | protected function createQueryParams($respectPagination = true) |
||
782 | 27 | ||
783 | /** |
||
784 | 27 | * Get the query parameters |
|
785 | * |
||
786 | 27 | * @return array |
|
787 | */ |
||
788 | protected function getParameters() |
||
792 | |||
793 | /** |
||
794 | * Get the alias used for the table in the FROM clause |
||
795 | 27 | * |
|
796 | * @return null|string |
||
797 | 27 | */ |
|
798 | 27 | protected function getFromAlias() |
|
802 | 3 | ||
803 | /** |
||
804 | * Get the table of the model |
||
805 | * |
||
806 | * @return string |
||
807 | 27 | */ |
|
808 | protected function getTable() |
||
814 | |||
815 | 3 | /** |
|
816 | * Get a MySQL query string in the requested format |
||
817 | 3 | * @param string|string[] $columns The columns that should be included |
|
818 | 3 | * (without the ID, if an array is provided) |
|
819 | 3 | * @return string The query |
|
820 | */ |
||
821 | 3 | protected function createQuery($columns = array()) |
|
835 | |||
836 | /** |
||
837 | * Generate the columns for the query |
||
838 | 27 | * @param string[] $columns The columns that should be included (without the ID) |
|
839 | * @return string |
||
840 | 27 | */ |
|
841 | private function createQueryColumns($columns = array()) |
||
859 | 27 | ||
860 | 3 | /** |
|
861 | * Generates all the WHERE conditions for the query |
||
862 | * @return string |
||
863 | 3 | */ |
|
864 | 3 | private function createQueryConditions($mode) |
|
878 | |||
879 | /** |
||
880 | 27 | * Generates the sorting instructions for the query |
|
881 | * @return string |
||
882 | */ |
||
883 | private function createQueryOrder() |
||
901 | |||
902 | /** |
||
903 | * Generates the pagination instructions for the query |
||
904 | * @return string |
||
905 | */ |
||
906 | private function createQueryPagination() |
||
928 | |||
929 | /** |
||
930 | * Set the current column we're working on |
||
931 | * |
||
932 | * @param string $column The column we're selecting |
||
933 | * @param string $mode Either 'where' or 'having' |
||
934 | * |
||
935 | * @return $this |
||
936 | */ |
||
937 | private function grabColumn($column, $mode) |
||
947 | } |
||
948 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.