1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace XHGui\Searcher; |
4
|
|
|
|
5
|
|
|
use Exception; |
6
|
|
|
use MongoCursor; |
7
|
|
|
use MongoDate; |
8
|
|
|
use MongoDb; |
9
|
|
|
use MongoId; |
10
|
|
|
use XHGui\Db\Mapper; |
11
|
|
|
use XHGui\Options\SearchOptions; |
12
|
|
|
use XHGui\Profile; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* A Searcher for a MongoDB backend. |
16
|
|
|
*/ |
17
|
|
|
class MongoSearcher implements SearcherInterface |
18
|
|
|
{ |
19
|
|
|
protected $_collection; |
20
|
|
|
|
21
|
|
|
protected $_watches; |
22
|
|
|
|
23
|
|
|
protected $_mapper; |
24
|
|
|
|
25
|
|
|
public function __construct(MongoDb $db) |
26
|
|
|
{ |
27
|
|
|
$this->_collection = $db->results; |
|
|
|
|
28
|
|
|
$this->_watches = $db->watches; |
|
|
|
|
29
|
|
|
$this->_mapper = new Mapper(); |
30
|
|
|
} |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* {@inheritdoc} |
34
|
|
|
*/ |
35
|
|
|
public function latest() |
36
|
|
|
{ |
37
|
|
|
$cursor = $this->_collection->find() |
38
|
|
|
->sort(['meta.request_date' => -1]) |
39
|
|
|
->limit(1); |
40
|
|
|
$result = $cursor->getNext(); |
41
|
|
|
|
42
|
|
|
return $this->_wrap($result); |
|
|
|
|
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* {@inheritdoc} |
47
|
|
|
*/ |
48
|
|
|
public function query($conditions, $limit, $fields = []) |
49
|
|
|
{ |
50
|
|
|
$result = $this->_collection->find($conditions, $fields) |
51
|
|
|
->limit($limit); |
52
|
|
|
|
53
|
|
|
return iterator_to_array($result); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* {@inheritdoc} |
58
|
|
|
*/ |
59
|
|
|
public function get($id) |
60
|
|
|
{ |
61
|
|
|
return $this->_wrap($this->_collection->findOne([ |
|
|
|
|
62
|
|
|
'_id' => new MongoId($id), |
63
|
|
|
])); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* {@inheritdoc} |
68
|
|
|
*/ |
69
|
|
|
public function getForUrl($url, $options, $conditions = []) |
70
|
|
|
{ |
71
|
|
|
$conditions = array_merge( |
72
|
|
|
(array)$conditions, |
73
|
|
|
['simple_url' => $url] |
74
|
|
|
); |
75
|
|
|
$options = array_merge($options, [ |
76
|
|
|
'conditions' => $conditions, |
77
|
|
|
]); |
78
|
|
|
|
79
|
|
|
return $this->paginate($options); |
|
|
|
|
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* {@inheritdoc} |
84
|
|
|
*/ |
85
|
|
|
public function getPercentileForUrl($percentile, $url, $search = []) |
86
|
|
|
{ |
87
|
|
|
$result = $this->_mapper->convert([ |
88
|
|
|
'conditions' => $search + ['simple_url' => $url], |
89
|
|
|
]); |
90
|
|
|
$match = $result['conditions']; |
91
|
|
|
|
92
|
|
|
$col = '$meta.request_date'; |
93
|
|
View Code Duplication |
if (!empty($search['limit']) && $search['limit'][0] === 'P') { |
|
|
|
|
94
|
|
|
$col = '$meta.request_ts'; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
$pipeline = [ |
98
|
|
|
['$match' => $match], |
99
|
|
|
[ |
100
|
|
|
'$project' => [ |
101
|
|
|
'date' => $col, |
102
|
|
|
'profile.main()' => 1, |
103
|
|
|
], |
104
|
|
|
], |
105
|
|
|
[ |
106
|
|
|
'$group' => [ |
107
|
|
|
'_id' => '$date', |
108
|
|
|
'row_count' => ['$sum' => 1], |
109
|
|
|
'wall_times' => ['$push' => '$profile.main().wt'], |
110
|
|
|
'cpu_times' => ['$push' => '$profile.main().cpu'], |
111
|
|
|
'mu_times' => ['$push' => '$profile.main().mu'], |
112
|
|
|
'pmu_times' => ['$push' => '$profile.main().pmu'], |
113
|
|
|
], |
114
|
|
|
], |
115
|
|
|
[ |
116
|
|
|
'$project' => [ |
117
|
|
|
'date' => '$date', |
118
|
|
|
'row_count' => '$row_count', |
119
|
|
|
'raw_index' => [ |
120
|
|
|
'$multiply' => [ |
121
|
|
|
'$row_count', |
122
|
|
|
$percentile / 100, |
123
|
|
|
], |
124
|
|
|
], |
125
|
|
|
'wall_times' => '$wall_times', |
126
|
|
|
'cpu_times' => '$cpu_times', |
127
|
|
|
'mu_times' => '$mu_times', |
128
|
|
|
'pmu_times' => '$pmu_times', |
129
|
|
|
], |
130
|
|
|
], |
131
|
|
|
['$sort' => ['_id' => 1]], |
132
|
|
|
]; |
133
|
|
|
|
134
|
|
|
$results = $this->_collection->aggregate( |
135
|
|
|
$pipeline, |
136
|
|
|
['cursor' => ['batchSize' => 0]] |
137
|
|
|
); |
138
|
|
|
|
139
|
|
|
if (empty($results['result'])) { |
140
|
|
|
return []; |
141
|
|
|
} |
142
|
|
|
$keys = [ |
143
|
|
|
'wall_times' => 'wt', |
144
|
|
|
'cpu_times' => 'cpu', |
145
|
|
|
'mu_times' => 'mu', |
146
|
|
|
'pmu_times' => 'pmu', |
147
|
|
|
]; |
148
|
|
|
foreach ($results['result'] as &$result) { |
149
|
|
|
$result['date'] = ($result['_id'] instanceof MongoDate) ? date('Y-m-d H:i:s', $result['_id']->sec) : $result['_id']; |
150
|
|
|
unset($result['_id']); |
151
|
|
|
$index = max(round($result['raw_index']) - 1, 0); |
152
|
|
|
foreach ($keys as $key => $out) { |
153
|
|
|
sort($result[$key]); |
154
|
|
|
$result[$out] = $result[$key][$index] ?? null; |
155
|
|
|
unset($result[$key]); |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
return $results['result']; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* {@inheritdoc} |
164
|
|
|
*/ |
165
|
|
|
public function getAvgsForUrl($url, $search = []) |
166
|
|
|
{ |
167
|
|
|
$match = ['meta.simple_url' => $url]; |
168
|
|
|
if (isset($search['date_start'])) { |
169
|
|
|
$match['meta.request_date']['$gte'] = (string)$search['date_start']; |
170
|
|
|
} |
171
|
|
|
if (isset($search['date_end'])) { |
172
|
|
|
$match['meta.request_date']['$lte'] = (string)$search['date_end']; |
173
|
|
|
} |
174
|
|
|
$results = $this->_collection->aggregate( |
175
|
|
|
[ |
176
|
|
|
['$match' => $match], |
177
|
|
|
[ |
178
|
|
|
'$project' => [ |
179
|
|
|
'date' => '$meta.request_date', |
180
|
|
|
'profile.main()' => 1, |
181
|
|
|
], |
182
|
|
|
], |
183
|
|
|
[ |
184
|
|
|
'$group' => [ |
185
|
|
|
'_id' => '$date', |
186
|
|
|
'avg_wt' => ['$avg' => '$profile.main().wt'], |
187
|
|
|
'avg_cpu' => ['$avg' => '$profile.main().cpu'], |
188
|
|
|
'avg_mu' => ['$avg' => '$profile.main().mu'], |
189
|
|
|
'avg_pmu' => ['$avg' => '$profile.main().pmu'], |
190
|
|
|
], |
191
|
|
|
], |
192
|
|
|
['$sort' => ['_id' => 1]], |
193
|
|
|
], |
194
|
|
|
['cursor' => ['batchSize' => 0]] |
195
|
|
|
); |
196
|
|
|
if (empty($results['result'])) { |
197
|
|
|
return []; |
198
|
|
|
} |
199
|
|
|
foreach ($results['result'] as $i => $result) { |
200
|
|
|
$results['result'][$i]['date'] = $result['_id']; |
201
|
|
|
unset($results['result'][$i]['_id']); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
return $results['result']; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* {@inheritdoc} |
209
|
|
|
*/ |
210
|
|
|
public function getAll(SearchOptions $options): array |
211
|
|
|
{ |
212
|
|
|
return $this->paginate($options->toArray()); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* {@inheritdoc} |
217
|
|
|
*/ |
218
|
|
|
public function delete($id): void |
219
|
|
|
{ |
220
|
|
|
$this->_collection->remove(['_id' => new MongoId($id)], []); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
public function truncate() |
224
|
|
|
{ |
225
|
|
|
$this->_collection->remove(); |
226
|
|
|
|
227
|
|
|
return $this; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* {@inheritdoc} |
232
|
|
|
*/ |
233
|
|
|
public function saveWatch(array $data): bool |
234
|
|
|
{ |
235
|
|
|
if (empty($data['name'])) { |
236
|
|
|
return false; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
if (!empty($data['removed']) && isset($data['_id'])) { |
240
|
|
|
$this->_watches->remove( |
241
|
|
|
['_id' => new MongoId($data['_id'])], |
242
|
|
|
['w' => 1] |
243
|
|
|
); |
244
|
|
|
|
245
|
|
|
return true; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
if (empty($data['_id'])) { |
249
|
|
|
$this->_watches->insert( |
250
|
|
|
$data, |
251
|
|
|
['w' => 1] |
252
|
|
|
); |
253
|
|
|
|
254
|
|
|
return true; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$data['_id'] = new MongoId($data['_id']); |
258
|
|
|
$this->_watches->update( |
259
|
|
|
['_id' => $data['_id']], |
260
|
|
|
$data, |
261
|
|
|
['w' => 1] |
262
|
|
|
); |
263
|
|
|
|
264
|
|
|
return true; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* {@inheritdoc} |
269
|
|
|
*/ |
270
|
|
|
public function getAllWatches(): array |
271
|
|
|
{ |
272
|
|
|
$cursor = $this->_watches->find(); |
273
|
|
|
|
274
|
|
|
return array_values(iterator_to_array($cursor)); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
public function truncateWatches() |
278
|
|
|
{ |
279
|
|
|
$this->_watches->remove(); |
280
|
|
|
|
281
|
|
|
return $this; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* {@inheritdoc} |
286
|
|
|
*/ |
287
|
|
|
private function paginate(array $options): array |
288
|
|
|
{ |
289
|
|
|
$opts = $this->_mapper->convert($options); |
290
|
|
|
|
291
|
|
|
$totalRows = $this->_collection->find( |
292
|
|
|
$opts['conditions'], |
293
|
|
|
['_id' => 1] |
294
|
|
|
)->count(); |
295
|
|
|
|
296
|
|
|
$totalPages = max(ceil($totalRows / $opts['perPage']), 1); |
297
|
|
|
$page = 1; |
298
|
|
|
if (isset($options['page'])) { |
299
|
|
|
$page = min(max($options['page'], 1), $totalPages); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
$projection = false; |
303
|
|
|
if (isset($options['projection'])) { |
304
|
|
|
if ($options['projection'] === true) { |
305
|
|
|
$projection = ['meta' => 1, 'profile.main()' => 1]; |
306
|
|
|
} else { |
307
|
|
|
$projection = $options['projection']; |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
if ($projection === false) { |
312
|
|
|
$cursor = $this->_collection->find($opts['conditions']) |
313
|
|
|
->sort($opts['sort']) |
314
|
|
|
->skip((int)($page - 1) * $opts['perPage']) |
315
|
|
|
->limit($opts['perPage']); |
316
|
|
|
} else { |
317
|
|
|
$cursor = $this->_collection->find($opts['conditions'], $projection) |
318
|
|
|
->sort($opts['sort']) |
319
|
|
|
->skip((int)($page - 1) * $opts['perPage']) |
320
|
|
|
->limit($opts['perPage']); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
return [ |
324
|
|
|
'results' => $this->_wrap($cursor), |
325
|
|
|
'sort' => $opts['sort'], |
326
|
|
|
'direction' => $opts['direction'], |
327
|
|
|
'page' => $page, |
328
|
|
|
'perPage' => $opts['perPage'], |
329
|
|
|
'totalPages' => $totalPages, |
330
|
|
|
]; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* Converts arrays + MongoCursors into Profile instances. |
335
|
|
|
* |
336
|
|
|
* @param array|MongoCursor $data the data to transform |
337
|
|
|
* @return Profile|Profile[] the transformed/wrapped results |
338
|
|
|
*/ |
339
|
|
|
private function _wrap($data) |
340
|
|
|
{ |
341
|
|
|
if ($data === null) { |
342
|
|
|
throw new Exception('No profile data found.'); |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
if (is_array($data)) { |
346
|
|
|
return new Profile($data); |
347
|
|
|
} |
348
|
|
|
$results = []; |
349
|
|
|
foreach ($data as $row) { |
350
|
|
|
$results[] = new Profile($row); |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
return $results; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* {@inheritdoc} |
358
|
|
|
*/ |
359
|
|
|
public function stats(): array |
360
|
|
|
{ |
361
|
|
|
return [ |
362
|
|
|
'profiles' => 0, |
363
|
|
|
'latest' => 0, |
364
|
|
|
'bytes' => 0, |
365
|
|
|
]; |
366
|
|
|
} |
367
|
|
|
} |
368
|
|
|
|
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.