Total Complexity | 44 |
Total Lines | 295 |
Duplicated Lines | 0 % |
Changes | 4 | ||
Bugs | 1 | Features | 1 |
Complex classes like Query 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.
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 Query, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
24 | final class Query |
||
25 | { |
||
26 | /** |
||
27 | * @var string |
||
28 | */ |
||
29 | public const TYPE = 'type'; |
||
30 | |||
31 | /** |
||
32 | * @var string |
||
33 | */ |
||
34 | public const CREATED_AT = 'created_at'; |
||
35 | |||
36 | /** |
||
37 | * @var string |
||
38 | */ |
||
39 | public const TRANSACTION_HASH = 'transaction_hash'; |
||
40 | |||
41 | /** |
||
42 | * @var string |
||
43 | */ |
||
44 | public const OBJECT_ID = 'object_id'; |
||
45 | |||
46 | /** |
||
47 | * @var string |
||
48 | */ |
||
49 | public const USER_ID = 'blame_id'; |
||
50 | |||
51 | /** |
||
52 | * @var string |
||
53 | */ |
||
54 | public const ID = 'id'; |
||
55 | |||
56 | /** |
||
57 | * @var string |
||
58 | */ |
||
59 | public const DISCRIMINATOR = 'discriminator'; |
||
60 | |||
61 | private array $filters = []; |
||
62 | |||
63 | private array $orderBy = []; |
||
64 | |||
65 | private Connection $connection; |
||
66 | |||
67 | private string $table; |
||
68 | |||
69 | private int $offset = 0; |
||
70 | |||
71 | private int $limit = 0; |
||
72 | |||
73 | private DateTimeZone $timezone; |
||
74 | |||
75 | public function __construct(string $table, Connection $connection, string $timezone) |
||
76 | { |
||
77 | $this->connection = $connection; |
||
78 | $this->table = $table; |
||
79 | $this->timezone = new DateTimeZone($timezone); |
||
80 | |||
81 | foreach ($this->getSupportedFilters() as $filterType) { |
||
82 | $this->filters[$filterType] = []; |
||
83 | } |
||
84 | } |
||
85 | |||
86 | /** |
||
87 | * @return array<Entry> |
||
88 | */ |
||
89 | public function execute(): array |
||
90 | { |
||
91 | $queryBuilder = $this->buildQueryBuilder(); |
||
92 | $statement = $queryBuilder->executeQuery(); |
||
93 | |||
94 | $result = []; |
||
95 | \assert($statement instanceof Result); |
||
96 | foreach ($statement->fetchAllAssociative() as $row) { |
||
97 | \assert(\is_string($row['created_at'])); |
||
98 | $row['created_at'] = new DateTimeImmutable($row['created_at'], $this->timezone); |
||
99 | $result[] = Entry::fromArray($row); |
||
100 | } |
||
101 | |||
102 | return $result; |
||
103 | } |
||
104 | |||
105 | public function count(): int |
||
106 | { |
||
107 | $queryBuilder = $this->buildQueryBuilder(); |
||
108 | |||
109 | try { |
||
110 | $queryBuilder |
||
111 | ->resetQueryPart('select') |
||
112 | ->resetQueryPart('orderBy') |
||
113 | ->setMaxResults(null) |
||
114 | ->setFirstResult(0) |
||
115 | ->select('COUNT(id)') |
||
116 | ; |
||
117 | |||
118 | /** @var false|int $result */ |
||
119 | $result = $queryBuilder->executeQuery()->fetchOne(); |
||
120 | } catch (Exception) { |
||
121 | $result = false; |
||
122 | } |
||
123 | |||
124 | return (int) $result; |
||
125 | } |
||
126 | |||
127 | public function addFilter(FilterInterface $filter): self |
||
128 | { |
||
129 | $this->checkFilter($filter->getName()); |
||
130 | $this->filters[$filter->getName()][] = $filter; |
||
131 | |||
132 | return $this; |
||
133 | } |
||
134 | |||
135 | public function addOrderBy(string $field, string $direction = 'DESC'): self |
||
136 | { |
||
137 | $this->checkFilter($field); |
||
138 | |||
139 | if (!\in_array($direction, ['ASC', 'DESC'], true)) { |
||
140 | throw new InvalidArgumentException('Invalid sort direction, allowed value: ASC, DESC'); |
||
141 | } |
||
142 | |||
143 | $this->orderBy[$field] = $direction; |
||
144 | |||
145 | return $this; |
||
146 | } |
||
147 | |||
148 | public function resetOrderBy(): self |
||
149 | { |
||
150 | $this->orderBy = []; |
||
151 | |||
152 | return $this; |
||
153 | } |
||
154 | |||
155 | public function limit(int $limit, int $offset = 0): self |
||
156 | { |
||
157 | if (0 > $limit) { |
||
158 | throw new InvalidArgumentException('Limit cannot be negative.'); |
||
159 | } |
||
160 | |||
161 | if (0 > $offset) { |
||
162 | throw new InvalidArgumentException('Offset cannot be negative.'); |
||
163 | } |
||
164 | |||
165 | $this->limit = $limit; |
||
166 | $this->offset = $offset; |
||
167 | |||
168 | return $this; |
||
169 | } |
||
170 | |||
171 | public function getSupportedFilters(): array |
||
172 | { |
||
173 | return array_keys(SchemaHelper::getAuditTableIndices('fake')); |
||
174 | } |
||
175 | |||
176 | public function getFilters(): array |
||
177 | { |
||
178 | return $this->filters; |
||
179 | } |
||
180 | |||
181 | public function getOrderBy(): array |
||
182 | { |
||
183 | return $this->orderBy; |
||
184 | } |
||
185 | |||
186 | /** |
||
187 | * @return array<int> |
||
188 | */ |
||
189 | public function getLimit(): array |
||
190 | { |
||
191 | return [$this->limit, $this->offset]; |
||
192 | } |
||
193 | |||
194 | private function buildQueryBuilder(): QueryBuilder |
||
195 | { |
||
196 | $queryBuilder = $this->connection->createQueryBuilder(); |
||
197 | $queryBuilder |
||
198 | ->select('*') |
||
199 | ->from($this->table, 'at') |
||
200 | ; |
||
201 | |||
202 | // build WHERE clause(s) |
||
203 | $queryBuilder = $this->buildWhere($queryBuilder); |
||
204 | |||
205 | // build ORDER BY part |
||
206 | $queryBuilder = $this->buildOrderBy($queryBuilder); |
||
207 | |||
208 | // build LIMIT part |
||
209 | return $this->buildLimit($queryBuilder); |
||
210 | } |
||
211 | |||
212 | private function groupFilters(array $filters): array |
||
213 | { |
||
214 | $grouped = []; |
||
215 | |||
216 | foreach ($filters as $filter) { |
||
217 | if (!isset($grouped[$filter::class])) { |
||
218 | $grouped[$filter::class] = []; |
||
219 | } |
||
220 | |||
221 | $grouped[$filter::class][] = $filter; |
||
222 | } |
||
223 | |||
224 | return $grouped; |
||
225 | } |
||
226 | |||
227 | private function mergeSimpleFilters(array $filters): SimpleFilter |
||
249 | } |
||
250 | |||
251 | private function buildWhere(QueryBuilder $queryBuilder): QueryBuilder |
||
252 | { |
||
253 | foreach ($this->filters as $rawFilters) { |
||
254 | if (0 === (is_countable($rawFilters) ? \count($rawFilters) : 0)) { |
||
255 | continue; |
||
256 | } |
||
257 | |||
258 | // group filters by class |
||
259 | $grouped = $this->groupFilters($rawFilters); |
||
260 | |||
261 | foreach ($grouped as $class => $filters) { |
||
262 | switch ($class) { |
||
263 | case SimpleFilter::class: |
||
264 | $filters = [$this->mergeSimpleFilters($filters)]; |
||
265 | |||
266 | break; |
||
267 | |||
268 | case RangeFilter::class: |
||
269 | case DateRangeFilter::class: |
||
270 | break; |
||
271 | } |
||
272 | |||
273 | foreach ($filters as $filter) { |
||
274 | $data = $filter->getSQL(); |
||
275 | |||
276 | $queryBuilder->andWhere($data['sql']); |
||
277 | |||
278 | foreach ($data['params'] as $name => $value) { |
||
279 | if (\is_array($value)) { |
||
280 | // TODO: replace Connection::PARAM_STR_ARRAY with ArrayParameterType::STRING when dropping support of doctrine/dbal < 3.6 |
||
281 | $queryBuilder->setParameter($name, $value, Connection::PARAM_STR_ARRAY); // @phpstan-ignore-line |
||
|
|||
282 | } else { |
||
283 | $queryBuilder->setParameter($name, $value); |
||
284 | } |
||
285 | } |
||
286 | } |
||
287 | } |
||
288 | } |
||
289 | |||
290 | return $queryBuilder; |
||
291 | } |
||
292 | |||
293 | private function buildOrderBy(QueryBuilder $queryBuilder): QueryBuilder |
||
294 | { |
||
295 | foreach ($this->orderBy as $field => $direction) { |
||
296 | $queryBuilder->addOrderBy($field, $direction); |
||
297 | } |
||
298 | |||
299 | return $queryBuilder; |
||
300 | } |
||
301 | |||
302 | private function buildLimit(QueryBuilder $queryBuilder): QueryBuilder |
||
313 | } |
||
314 | |||
315 | private function checkFilter(string $filter): void |
||
316 | { |
||
317 | if (!\in_array($filter, $this->getSupportedFilters(), true)) { |
||
318 | throw new InvalidArgumentException(sprintf('Unsupported "%s" filter, allowed filters: %s.', $filter, implode(', ', $this->getSupportedFilters()))); |
||
319 | } |
||
320 | } |
||
321 | } |
||
322 |
This class constant has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.