1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace JamesMoss\Flywheel; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* Query |
7
|
|
|
* |
8
|
|
|
* Builds an executes a query whichs searches and sorts documents from a |
9
|
|
|
* repository. |
10
|
|
|
*/ |
11
|
|
|
class QueryExecuter |
12
|
|
|
{ |
13
|
|
|
protected $repo; |
14
|
|
|
protected $predicate; |
15
|
|
|
protected $limit; |
16
|
|
|
protected $orderBy; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Constructor |
20
|
|
|
* |
21
|
|
|
* @param Repository $repo The repo to execute against |
22
|
|
|
* @param Predicate $pred The predicate to use. |
23
|
|
|
* @param array $limit The count and offset. |
24
|
|
|
* @param array $orderBy An array of field names to order by |
25
|
|
|
*/ |
26
|
|
|
public function __construct(Repository $repo, Predicate $pred, array $limit, array $orderBy) |
27
|
|
|
{ |
28
|
|
|
$this->repo = $repo; |
29
|
|
|
$this->predicate = $pred; |
30
|
|
|
$this->limit = $limit; |
31
|
|
|
$this->orderBy = $orderBy; |
32
|
|
|
} |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Runs the query. |
36
|
|
|
* |
37
|
|
|
* @return Result The documents returned from this query. |
38
|
|
|
*/ |
39
|
|
|
public function run() |
40
|
|
|
{ |
41
|
|
|
$documents = $this->repo->findAll(); |
42
|
|
|
|
43
|
|
|
if ($predicates = $this->predicate->getAll()) { |
44
|
|
|
$documents = $this->filter($documents, $predicates); |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
if ($this->orderBy) { |
|
|
|
|
48
|
|
|
$sorts = array(); |
49
|
|
|
foreach ($this->orderBy as $order) { |
50
|
|
|
$parts = explode(' ', $order, 2); |
51
|
|
|
// TODO - validate parts |
52
|
|
|
$sorts[] = array( |
53
|
|
|
$parts[0], |
54
|
|
|
isset($parts[1]) && $parts[1] == 'DESC' ? SORT_DESC : SORT_ASC |
55
|
|
|
); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
$documents = $this->sort($documents, $sorts); |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
$totalCount = count($documents); |
62
|
|
|
|
63
|
|
|
if ($this->limit) { |
|
|
|
|
64
|
|
|
list($count, $offset) = $this->limit; |
65
|
|
|
$documents = array_slice($documents, $offset, $count); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
return new Result(array_values($documents), $totalCount); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
protected function getFieldValue($doc, $field, &$found = false) |
72
|
|
|
{ |
73
|
|
|
$found = false; |
74
|
|
|
|
75
|
|
|
if ($field === '__id') { |
76
|
|
|
$found = true; |
77
|
|
|
|
78
|
|
|
return $doc->getId(); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
if (false !== strpos($field, '.')) { |
82
|
|
|
return $doc->getNestedProperty($field, $found); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
if (!property_exists($doc, $field)) { |
86
|
|
|
return false; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
$found = true; |
90
|
|
|
|
91
|
|
|
return $doc->{$field}; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
protected function matchDocument($doc, $field, $operator, $value) |
95
|
|
|
{ |
96
|
|
|
$docVal = $this->getFieldValue($doc, $field, $found); |
97
|
|
|
|
98
|
|
|
if (!$found) { |
99
|
|
|
return false; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
switch (true) { |
103
|
|
|
case ($operator === '==' && $docVal == $value): return true; |
|
|
|
|
104
|
|
|
case ($operator === '===' && $docVal === $value): return true; |
|
|
|
|
105
|
|
|
case ($operator === '!=' && $docVal != $value): return true; |
|
|
|
|
106
|
|
|
case ($operator === '!==' && $docVal !== $value): return true; |
|
|
|
|
107
|
|
|
case ($operator === '>' && $docVal > $value): return true; |
|
|
|
|
108
|
|
|
case ($operator === '>=' && $docVal >= $value): return true; |
|
|
|
|
109
|
|
|
case ($operator === '<' && $docVal < $value): return true; |
|
|
|
|
110
|
|
|
case ($operator === '>=' && $docVal >= $value): return true; |
|
|
|
|
111
|
|
|
case ($operator === 'IN' && in_array($docVal, (array)$value)): return true; |
|
|
|
|
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
return false; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
protected function filter($documents, $predicates) |
118
|
|
|
{ |
119
|
|
|
$result = []; |
120
|
|
|
$originalDocs = $documents; |
121
|
|
|
|
122
|
|
|
$andPredicates = array_filter($predicates, function($pred) { |
123
|
|
|
return $pred[0] !== Predicate::LOGICAL_OR; |
124
|
|
|
}); |
125
|
|
|
|
126
|
|
|
$orPredicates = array_filter($predicates, function($pred) { |
127
|
|
|
return $pred[0] === Predicate::LOGICAL_OR; |
128
|
|
|
}); |
129
|
|
|
|
130
|
|
|
foreach($andPredicates as $predicate) { |
131
|
|
View Code Duplication |
if (is_array($predicate[1])) { |
|
|
|
|
132
|
|
|
$documents = $this->filter($documents, $predicate[1]); |
133
|
|
|
} else { |
134
|
|
|
list($type, $field, $operator, $value) = $predicate; |
|
|
|
|
135
|
|
|
|
136
|
|
|
$documents = array_values(array_filter($documents, function ($doc) use ($field, $operator, $value) { |
137
|
|
|
return $this->matchDocument($doc, $field, $operator, $value); |
138
|
|
|
})); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
$result = $documents; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
foreach($orPredicates as $predicate) { |
145
|
|
View Code Duplication |
if (is_array($predicate[1])) { |
|
|
|
|
146
|
|
|
$documents = $this->filter($originalDocs, $predicate[1]); |
147
|
|
|
} else { |
148
|
|
|
list($type, $field, $operator, $value) = $predicate; |
|
|
|
|
149
|
|
|
|
150
|
|
|
$documents = array_values(array_filter($originalDocs, function ($doc) use ($field, $operator, $value) { |
151
|
|
|
return $this->matchDocument($doc, $field, $operator, $value); |
152
|
|
|
})); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
$result = array_unique(array_merge($result, $documents), SORT_REGULAR); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
return $result; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Sorts an array of documents by multiple fields if needed. |
163
|
|
|
* |
164
|
|
|
* @param array $array An array of Documents. |
165
|
|
|
* @param array $args The fields to sort by. |
166
|
|
|
* |
167
|
|
|
* @return array The sorted array of documents. |
168
|
|
|
*/ |
169
|
|
|
protected function sort(array $array, array $args) |
170
|
|
|
{ |
171
|
|
|
$c = count($args); |
172
|
|
|
|
173
|
|
|
usort($array, function ($a, $b) use ($args, $c) { |
174
|
|
|
$i = 0; |
175
|
|
|
$cmp = 0; |
176
|
|
|
while ($cmp == 0 && $i < $c) { |
177
|
|
|
$keyName = $args[$i][0]; |
178
|
|
|
if($keyName == 'id' || $keyName == '__id') { |
179
|
|
|
$valueA = $a->getId(); |
180
|
|
|
$valueB = $b->getId(); |
181
|
|
|
} else { |
182
|
|
|
$valueA = $this->getFieldValue($a, $keyName, $found); |
183
|
|
|
if ($found === false) { |
184
|
|
|
$valueA = null; |
185
|
|
|
} |
186
|
|
|
$valueB = $this->getFieldValue($b, $keyName, $found); |
187
|
|
|
if ($found === false) { |
188
|
|
|
$valueB = null; |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
if (is_string($valueA)) { |
193
|
|
|
$cmp = strcmp($valueA, $valueB); |
194
|
|
|
} elseif (is_bool($valueA)) { |
195
|
|
|
$cmp = $valueA - $valueB; |
196
|
|
|
} else { |
197
|
|
|
$cmp = ($valueA == $valueB) ? 0 : (($valueA < $valueB) ? -1 : 1); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
if ($args[$i][1] === SORT_DESC) { |
201
|
|
|
$cmp *= -1; |
202
|
|
|
} |
203
|
|
|
$i++; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
return $cmp; |
207
|
|
|
}); |
208
|
|
|
|
209
|
|
|
return $array; |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.