Skip to content

Commit 2a75f10

Browse files
committed
Refactors
1 parent 0930355 commit 2a75f10

File tree

1 file changed

+129
-100
lines changed

1 file changed

+129
-100
lines changed

src/Query/Grammar.php

Lines changed: 129 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ class Grammar
2626
'not_contains' => 'notContains',
2727
];
2828

29-
/**
30-
* The query wrapper.
31-
*/
32-
protected ?string $wrapper = null;
33-
3429
/**
3530
* Get all the available operators.
3631
*/
@@ -54,131 +49,199 @@ public function wrap(string $query, ?string $prefix = '(', ?string $suffix = ')'
5449
*/
5550
public function compile(Builder $query): string
5651
{
57-
if ($this->queryMustBeWrapped($query)) {
58-
$this->wrapper = 'and';
59-
}
52+
$filter = $this->compileFilters($query);
6053

61-
$filter = $this->compileRaws($query)
54+
return $this->wrapFilterIfNeeded($query, $filter);
55+
}
56+
57+
/**
58+
* Compile all filters for the query.
59+
*/
60+
protected function compileFilters(Builder $query): string
61+
{
62+
return $this->compileRaws($query)
6263
.$this->compileWheres($query)
6364
.$this->compileOrWheres($query);
65+
}
66+
67+
/**
68+
* Wrap the filter in logical operators if needed.
69+
*/
70+
protected function wrapFilterIfNeeded(Builder $query, string $filter): string
71+
{
72+
if ($query->isNested()) {
73+
return $filter;
74+
}
75+
76+
// Special case: if we have exactly one AND and one OR, wrap in OR
77+
if ($this->shouldWrapEntireQueryInOr($query)) {
78+
return $this->compileOr($filter);
79+
}
80+
81+
// If we have multiple filter types, multiple AND conditions, or multiple raw filters, wrap in AND
82+
if ($this->hasMultipleFilterTypes($query) || $this->hasMultipleAndConditions($query) || $this->hasMultipleRawFilters($query)) {
83+
return $this->compileAnd($filter);
84+
}
85+
86+
// If we only have OR conditions and more than one, wrap in OR
87+
if ($this->shouldWrapInOr($query)) {
88+
return $this->compileOr($filter);
89+
}
6490

65-
return match ($this->wrapper) {
66-
'and' => $this->compileAnd($filter),
67-
'or' => $this->compileOr($filter),
68-
default => $filter,
69-
};
91+
return $filter;
7092
}
7193

7294
/**
73-
* Determine if the query must be wrapped in an encapsulating statement.
95+
* Determine if the query has multiple filter types.
7496
*/
75-
protected function queryMustBeWrapped(Builder $query): bool
97+
protected function hasMultipleFilterTypes(Builder $query): bool
7698
{
77-
return ! $query->isNested() && $this->hasMultipleFilters($query);
99+
$filterCount = 0;
100+
101+
foreach (['and', 'or', 'raw'] as $type) {
102+
if (! empty($query->filters[$type])) {
103+
$filterCount++;
104+
}
105+
}
106+
107+
return $filterCount > 1;
78108
}
79109

80110
/**
81-
* Assembles all the "raw" filters on the query.
111+
* Determine if the query has multiple AND conditions.
82112
*/
83-
protected function compileRaws(Builder $builder): string
113+
protected function hasMultipleAndConditions(Builder $query): bool
84114
{
85-
return $this->concatenate($builder->filters['raw']);
115+
return count($query->filters['and'] ?? []) > 1;
86116
}
87117

88118
/**
89-
* Assembles all where clauses in the current wheres property.
119+
* Determine if the query has multiple raw filters.
90120
*/
91-
protected function compileWheres(Builder $builder, string $type = 'and'): string
121+
protected function hasMultipleRawFilters(Builder $query): bool
92122
{
93-
$filter = '';
123+
return count($query->filters['raw'] ?? []) > 1;
124+
}
94125

95-
foreach ($builder->filters[$type] ?? [] as $where) {
96-
$filter .= $this->compileWhere($where);
97-
}
126+
/**
127+
* Determine if the entire query should be wrapped in an OR statement.
128+
*/
129+
protected function shouldWrapEntireQueryInOr(Builder $query): bool
130+
{
131+
return count($query->filters['and'] ?? []) === 1
132+
&& count($query->filters['or'] ?? []) === 1
133+
&& empty($query->filters['raw']);
134+
}
98135

99-
return $filter;
136+
/**
137+
* Determine if the query should be wrapped in an OR statement.
138+
*/
139+
protected function shouldWrapInOr(Builder $query): bool
140+
{
141+
return ! empty($query->filters['or'])
142+
&& count($query->filters['or']) > 1
143+
&& empty($query->filters['and'])
144+
&& empty($query->filters['raw']);
145+
}
146+
147+
/**
148+
* Assembles all the "raw" filters on the query.
149+
*/
150+
protected function compileRaws(Builder $query): string
151+
{
152+
return $this->concatenate($query->filters['raw'] ?? []);
153+
}
154+
155+
/**
156+
* Assembles all where clauses in the current wheres property.
157+
*/
158+
protected function compileWheres(Builder $query): string
159+
{
160+
return $this->compileFilterType($query, 'and');
100161
}
101162

102163
/**
103164
* Assembles all or where clauses in the current orWheres property.
104165
*/
105166
protected function compileOrWheres(Builder $query): string
106167
{
107-
$filter = $this->compileWheres($query, 'or');
168+
$filter = $this->compileFilterType($query, 'or');
108169

109-
if (! $this->hasMultipleFilters($query)) {
170+
// If we're going to wrap the entire query in OR, don't wrap OR clauses separately
171+
if ($this->shouldWrapEntireQueryInOr($query)) {
110172
return $filter;
111173
}
112174

113-
// Here we will detect whether the entire query can be
114-
// wrapped inside of an "or" statement by checking
115-
// how many filter statements exist for each type.
116-
if ($this->queryCanBeWrappedInSingleOrStatement($query)) {
117-
$this->wrapper = 'or';
118-
} else {
119-
$filter = $this->compileOr($filter);
175+
// If we have OR clauses and other filter types (mixed query),
176+
// wrap the OR clauses in their own OR statement
177+
if (! empty($filter) && $this->hasMultipleFilterTypes($query)) {
178+
return $this->compileOr($filter);
120179
}
121180

122181
return $filter;
123182
}
124183

125184
/**
126-
* Determine if the query can be wrapped in a single or statement.
185+
* Compile filters of a specific type.
127186
*/
128-
protected function queryCanBeWrappedInSingleOrStatement(Builder $query): bool
187+
protected function compileFilterType(Builder $query, string $type): string
129188
{
130-
return $this->has($query, 'or', '>=', 1)
131-
&& $this->has($query, 'and', '<=', 1)
132-
&& $this->has($query, 'raw', '=', 0);
189+
$filter = '';
190+
191+
foreach ($query->filters[$type] ?? [] as $where) {
192+
$filter .= $this->compileWhere($where);
193+
}
194+
195+
return $filter;
133196
}
134197

135198
/**
136199
* Concatenates filters into a single string.
137200
*/
138201
public function concatenate(array $bindings = []): string
139202
{
140-
// Filter out empty query segments.
141203
return implode(
142-
array_filter($bindings, [$this, 'bindingValueIsNotEmpty'])
204+
array_filter($bindings, fn (mixed $value) => ! empty($value))
143205
);
144206
}
145207

146208
/**
147-
* Determine if the binding value is not empty.
209+
* Assembles a single where query.
210+
*
211+
* @throws UnexpectedValueException
148212
*/
149-
protected function bindingValueIsNotEmpty(string $value): bool
213+
protected function compileWhere(array $where): string
150214
{
151-
return ! empty($value);
215+
$method = $this->makeCompileMethod($where['operator']);
216+
217+
// Some operators like 'has' and 'notHas' don't require a value
218+
if (in_array($where['operator'], ['*', '!*'])) {
219+
return $this->{$method}($where['field']);
220+
}
221+
222+
return $this->{$method}($where['field'], $where['value']);
152223
}
153224

154225
/**
155-
* Determine if the query is using multiple filters.
226+
* Make the compile method name for the operator.
227+
*
228+
* @throws UnexpectedValueException
156229
*/
157-
protected function hasMultipleFilters(Builder $query): bool
230+
protected function makeCompileMethod(string $operator): string
158231
{
159-
return $this->has($query, ['and', 'or', 'raw'], '>', 1);
232+
if (! $this->operatorExists($operator)) {
233+
throw new UnexpectedValueException("Invalid LDAP filter operator ['$operator']");
234+
}
235+
236+
return 'compile'.ucfirst($this->operators[$operator]);
160237
}
161238

162239
/**
163-
* Determine if the query contains the given filter statement type.
240+
* Determine if the operator exists.
164241
*/
165-
protected function has(Builder $query, array|string $type, string $operator = '>=', int $count = 1): bool
242+
protected function operatorExists(string $operator): bool
166243
{
167-
$types = (array) $type;
168-
169-
$filters = 0;
170-
171-
foreach ($types as $type) {
172-
$filters += count($query->filters[$type] ?? []);
173-
}
174-
175-
return match ($operator) {
176-
'>' => $filters > $count,
177-
'>=' => $filters >= $count,
178-
'<' => $filters < $count,
179-
'<=' => $filters <= $count,
180-
default => $filters == $count,
181-
};
244+
return array_key_exists($operator, $this->operators);
182245
}
183246

184247
/**
@@ -356,38 +419,4 @@ public function compileNot(string $query): string
356419
{
357420
return $query ? $this->wrap($query, '(!') : '';
358421
}
359-
360-
/**
361-
* Assembles a single where query.
362-
*
363-
* @throws UnexpectedValueException
364-
*/
365-
protected function compileWhere(array $where): string
366-
{
367-
$method = $this->makeCompileMethod($where['operator']);
368-
369-
return $this->{$method}($where['field'], $where['value']);
370-
}
371-
372-
/**
373-
* Make the compile method name for the operator.
374-
*
375-
* @throws UnexpectedValueException
376-
*/
377-
protected function makeCompileMethod(string $operator): string
378-
{
379-
if (! $this->operatorExists($operator)) {
380-
throw new UnexpectedValueException("Invalid LDAP filter operator ['$operator']");
381-
}
382-
383-
return 'compile'.ucfirst($this->operators[$operator]);
384-
}
385-
386-
/**
387-
* Determine if the operator exists.
388-
*/
389-
protected function operatorExists(string $operator): bool
390-
{
391-
return array_key_exists($operator, $this->operators);
392-
}
393422
}

0 commit comments

Comments
 (0)