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