1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
/** |
4
|
|
|
* Minotaur |
5
|
|
|
* |
6
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not |
7
|
|
|
* use this file except in compliance with the License. You may obtain a copy of |
8
|
|
|
* the License at |
9
|
|
|
* |
10
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0 |
11
|
|
|
* |
12
|
|
|
* Unless required by applicable law or agreed to in writing, software |
13
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
14
|
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
15
|
|
|
* License for the specific language governing permissions and limitations under |
16
|
|
|
* the License. |
17
|
|
|
* |
18
|
|
|
* @copyright 2015-2017 Appertly |
19
|
|
|
* @license Apache-2.0 |
20
|
|
|
*/ |
21
|
|
|
namespace Minotaur\Db; |
22
|
|
|
|
23
|
|
|
use ArrayIterator; |
24
|
|
|
use MongoDB\BSON\ObjectID; |
25
|
|
|
use MongoDB\Driver\Cursor; |
26
|
|
|
use MongoDB\Driver\Manager; |
27
|
|
|
use MongoDB\Driver\ReadPreference; |
28
|
|
|
use MongoDB\Driver\WriteConcern; |
29
|
|
|
use MongoDB\Driver\WriteResult; |
30
|
|
|
use Caridea\Dao\MongoDb as MongoDbDao; |
31
|
|
|
use Caridea\Event\PublisherAware; |
32
|
|
|
use Minotaur\Getter; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Abstract MongoDB DAO Service |
36
|
|
|
*/ |
37
|
|
|
abstract class AbstractMongoDao extends MongoDbDao implements EntityRepo, DbRefResolver, PublisherAware |
38
|
|
|
{ |
39
|
|
|
use MongoHelper; |
40
|
|
|
use \Caridea\Dao\Event\Publishing; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @var bool Whether to enforce optimistic locking |
44
|
|
|
*/ |
45
|
|
|
private $versioned = true; |
46
|
|
|
/** |
47
|
|
|
* @var bool Whether entities will be put in the first-level cache |
48
|
|
|
*/ |
49
|
|
|
private $caching = true; |
50
|
|
|
/** |
51
|
|
|
* @var array<string,?string> The MongoDB type map when reading records |
52
|
|
|
*/ |
53
|
|
|
private $typeMap = ['root' => null, 'document' => null]; |
54
|
|
|
/** |
55
|
|
|
* @var \MongoDB\Driver\ReadPreference The MongoDB read preference, or `null` |
56
|
|
|
*/ |
57
|
|
|
private $readPreference; |
58
|
|
|
/** |
59
|
|
|
* @var \MongoDB\Driver\ReadPreference The MongoDB write concern, or `null` |
60
|
|
|
*/ |
61
|
|
|
private $writeConcern; |
62
|
|
|
/** |
63
|
|
|
* @var array<string,mixed> First-level cache |
64
|
|
|
*/ |
65
|
|
|
private $cache = []; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Creates a new AbstractMongoDao. |
69
|
|
|
* |
70
|
|
|
* Current accepted configuration values: |
71
|
|
|
* * `versioned` – Whether to enforce optimistic locking via a version field (default: true) |
72
|
|
|
* * `caching` – Whether to cache entities by ID (default: true) |
73
|
|
|
* * `typeMapRoot` – The type used to unserialize BSON root documents |
74
|
|
|
* * `typeMapDocument` – The type used to unserialize BSON nested documents |
75
|
|
|
* * `readPreference` – Must be a `MongoDB\Driver\ReadPreference` |
76
|
|
|
* * `writeConcern` – Must be a `MongoDB\Driver\WriteConcern` |
77
|
|
|
* |
78
|
|
|
* As for the `typeMap` options, you can see |
79
|
|
|
* [Deserialization from BSON](http://php.net/manual/en/mongodb.persistence.deserialization.php#mongodb.persistence.typemaps) |
80
|
|
|
* for more information. |
81
|
|
|
* |
82
|
|
|
* @param \MongoDB\Driver\Manager $manager The MongoDB Manager |
83
|
|
|
* @param string $collection The collection to wrap |
84
|
|
|
* @param array<string,mixed> $options Optional. Map of configuration values |
85
|
|
|
*/ |
86
|
|
|
public function __construct( |
87
|
|
|
Manager $manager, |
88
|
|
|
string $collection, |
89
|
|
|
array $options = null |
90
|
|
|
) { |
91
|
|
|
parent::__construct($manager, $collection); |
92
|
|
|
if ($options !== null) { |
93
|
|
|
$this->versioned = array_key_exists('version', $options) ? |
94
|
|
|
(bool) $options['version'] : true; |
95
|
|
|
$this->caching = array_key_exists('caching', $options) ? |
96
|
|
|
(bool) $options['caching'] : true; |
97
|
|
|
if (array_key_exists('typeMapRoot', $options)) { |
98
|
|
|
$r = $options['typeMapRoot']; |
99
|
|
|
$this->typeMap['root'] = $r === null ? null : (string)$r; |
100
|
|
|
} |
101
|
|
|
if (array_key_exists('typeMapDocument', $options)) { |
102
|
|
|
$d = $options['typeMapDocument']; |
103
|
|
|
$this->typeMap['document'] = $d === null ? null : (string)$d; |
104
|
|
|
} |
105
|
|
|
$rp = $options['readPreference'] ?? null; |
106
|
|
|
if ($rp instanceof ReadPreference) { |
|
|
|
|
107
|
|
|
$this->readPreference = $rp; |
108
|
|
|
} |
109
|
|
|
$wc = $options['writeConcern'] ?? null; |
110
|
|
|
if ($wc instanceof WriteConcern) { |
|
|
|
|
111
|
|
|
$this->writeConcern = $wc; |
|
|
|
|
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
$this->publisher = new \Caridea\Event\NullPublisher(); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* @inheritDoc |
119
|
|
|
*/ |
120
|
|
|
public function countAll(array $criteria): int |
121
|
|
|
{ |
122
|
|
|
$result = $this->doExecute(function (Manager $m, string $c) use ($criteria) { |
123
|
|
|
list($db, $coll) = explode('.', $c, 2); |
124
|
|
|
$command = new \MongoDB\Driver\Command([ |
125
|
|
|
'count' => $coll, |
126
|
|
|
'query' => $criteria, |
127
|
|
|
]); |
128
|
|
|
$cursor = $m->executeCommand($db, $command, $this->readPreference); |
129
|
|
|
$cursor->setTypeMap(['root' => 'array']); |
130
|
|
|
$resa = $cursor->toArray(); |
131
|
|
|
return count($resa) > 0 ? current($resa) : null; |
132
|
|
|
}); |
133
|
|
|
|
134
|
|
|
// Older server versions may return a float |
135
|
|
|
if (!is_array($result) || !array_key_exists('n', $result) || !(is_int($result['n']) || is_float($result['n']))) { |
136
|
|
|
throw new \Caridea\Dao\Exception\Unretrievable('count command did not return a numeric "n" value'); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
return (int) $result['n']; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @inheritDoc |
144
|
|
|
*/ |
145
|
|
|
public function findOne(array $criteria) |
146
|
|
|
{ |
147
|
|
|
return $this->maybeCache( |
148
|
|
|
$this->doExecute(function (Manager $m, string $c) use ($criteria) { |
149
|
|
|
$q = new \MongoDB\Driver\Query($criteria, ['limit' => 1]); |
150
|
|
|
$res = $m->executeQuery($c, $q, $this->readPreference); |
151
|
|
|
$res->setTypeMap($this->typeMap); |
152
|
|
|
$resa = $res->toArray(); |
153
|
|
|
return count($resa) > 0 ? current($resa) : null; |
154
|
|
|
}) |
155
|
|
|
); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* @inheritDoc |
160
|
|
|
*/ |
161
|
|
|
public function findAll(array $criteria, \Caridea\Http\Pagination $pagination = null, bool $totalCount = false): iterable |
162
|
|
|
{ |
163
|
|
|
$total = null; |
164
|
|
|
if ($totalCount === true && $pagination !== null && ($pagination->getMax() != PHP_INT_MAX || $pagination->getOffset() > 0)) { |
165
|
|
|
$total = $this->countAll($criteria); |
166
|
|
|
} |
167
|
|
|
$results = $this->doExecute(function (Manager $m, string $c) use ($criteria, $pagination) { |
168
|
|
|
$qo = []; |
169
|
|
|
if ($pagination !== null) { |
170
|
|
|
if ($pagination->getMax() != PHP_INT_MAX) { |
171
|
|
|
$qo['limit'] = $pagination->getMax(); |
172
|
|
|
} |
173
|
|
|
$qo['skip'] = $pagination->getOffset(); |
174
|
|
|
$sorts = []; |
175
|
|
|
foreach ($pagination->getOrder() as $k => $v) { |
176
|
|
|
$sorts[$k] = $v ? 1 : -1; |
177
|
|
|
} |
178
|
|
|
if (count($sorts) > 0) { |
179
|
|
|
$qo['sort'] = $sorts; |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
$q = new \MongoDB\Driver\Query($criteria, $qo); |
183
|
|
|
$res = $m->executeQuery($c, $q, $this->readPreference); |
184
|
|
|
$res->setTypeMap($this->typeMap); |
185
|
|
|
return $res; |
186
|
|
|
}); |
187
|
|
|
return $total === null ? $results : new CursorSubset($results, $total); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @inheritDoc |
192
|
|
|
*/ |
193
|
|
|
public function findById($id) |
194
|
|
|
{ |
195
|
|
|
try { |
196
|
|
|
$mid = $this->toId($id); |
197
|
|
|
} catch (\MongoDB\Driver\Exception\InvalidArgumentException $e) { |
|
|
|
|
198
|
|
|
if ($e->getMessage() === 'Invalid BSON ID provided') { |
199
|
|
|
throw new \Caridea\Dao\Exception\Unretrievable('Could not find document', 0, $e); |
200
|
|
|
} |
201
|
|
|
throw $e; |
202
|
|
|
} |
203
|
|
|
return $this->getFromCache((string)$id) ?? |
204
|
|
|
$this->findOne(['_id' => $mid]); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* @inheritDoc |
209
|
|
|
*/ |
210
|
|
|
public function get($id) |
211
|
|
|
{ |
212
|
|
|
return $this->ensure($id, $this->findById($id)); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* @inheritDoc |
217
|
|
|
*/ |
218
|
|
|
public function getAll(iterable $ids): iterable |
219
|
|
|
{ |
220
|
|
|
$ids = is_array($ids) ? $ids : iterator_to_array($ids); |
221
|
|
|
if (count($ids) === 0) { |
222
|
|
|
return []; |
223
|
|
|
} |
224
|
|
|
try { |
225
|
|
|
$mids = $this->toIds($ids); |
226
|
|
|
} catch (\MongoDB\Driver\Exception\InvalidArgumentException $e) { |
|
|
|
|
227
|
|
|
if ($e->getMessage() === 'Invalid BSON ID provided') { |
228
|
|
|
throw new \Caridea\Dao\Exception\Unretrievable('Could not load documents', 0, $e); |
229
|
|
|
} |
230
|
|
|
throw $e; |
231
|
|
|
} |
232
|
|
|
$cmp = array_flip(array_map(function ($a) { |
233
|
|
|
return (string) $a; |
234
|
|
|
}, $ids)); |
235
|
|
|
$fromCache = array_intersect_key($this->cache, $cmp); |
236
|
|
|
if (count($fromCache) === count($ids)) { |
237
|
|
|
return $fromCache; |
238
|
|
|
} elseif (count($fromCache) > 0) { |
239
|
|
|
$mids = array_filter($mids, function ($a) { |
240
|
|
|
return !array_key_exists((string)$a, $this->cache); |
241
|
|
|
}); |
242
|
|
|
if(count($mids) < 2){ |
|
|
|
|
243
|
|
|
return array_merge( |
244
|
|
|
$fromCache, |
245
|
|
|
$this->maybeCacheAll($this->findAll(['_id' => $mids])) |
246
|
|
|
); |
247
|
|
|
} |
248
|
|
|
return array_merge( |
249
|
|
|
$fromCache, |
250
|
|
|
$this->maybeCacheAll($this->findAll(['_id' => ['$in' => array_values($mids)]])) |
251
|
|
|
); |
252
|
|
|
} else { |
253
|
|
|
return $this->maybeCacheAll($this->findAll(['_id' => ['$in' => array_values($mids)]])); |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* @inheritDoc |
259
|
|
|
*/ |
260
|
|
|
public function getInstanceMap(iterable $entities): array |
261
|
|
|
{ |
262
|
|
|
$instances = []; |
263
|
|
|
foreach ($entities as $entity) { |
264
|
|
|
$instances[(string) Getter::getId($entity)] = $entity; |
265
|
|
|
} |
266
|
|
|
return $instances; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Gets the read preference. |
271
|
|
|
* |
272
|
|
|
* If no read preference was specified at creation, this method returns the |
273
|
|
|
* read preference as returned by the `Manager`. |
274
|
|
|
* |
275
|
|
|
* @return - The read preference, or `null` |
|
|
|
|
276
|
|
|
*/ |
277
|
|
|
public function getReadPreference(): ReadPreference |
278
|
|
|
{ |
279
|
|
|
return $this->readPreference ?? $this->manager->getReadPreference(); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* Gets the write concern. |
284
|
|
|
* |
285
|
|
|
* If no write concern was specified at creation, this method returns the |
286
|
|
|
* write concern as returned by the `Manager`. |
287
|
|
|
* |
288
|
|
|
* @return - The write concern |
|
|
|
|
289
|
|
|
*/ |
290
|
|
|
public function getWriteConcern(): WriteConcern |
291
|
|
|
{ |
292
|
|
|
return $this->writeConcern ?? $this->manager->getWriteConcern(); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* @inheritDoc |
297
|
|
|
*/ |
298
|
|
|
public function isResolvable(string $ref): bool |
299
|
|
|
{ |
300
|
|
|
return strstr($this->collection, '.') === ".$ref"; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* @inheritDoc |
305
|
|
|
*/ |
306
|
|
|
public function resolve(array $ref) |
307
|
|
|
{ |
308
|
|
|
if (!array_key_exists('$ref', $ref) || !array_key_exists('$id', $ref)) { |
309
|
|
|
throw new \InvalidArgumentException('Not a DBRef. Needs both $ref and $id keys.'); |
310
|
|
|
} |
311
|
|
|
if (!$this->isResolvable($ref['$ref'])) { |
312
|
|
|
throw new \InvalidArgumentException("Unsupported reference type: " . $ref['$ref']); |
313
|
|
|
} |
314
|
|
|
return $this->findById($ref['$id']); |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* @inheritDoc |
319
|
|
|
*/ |
320
|
|
|
public function resolveAll(iterable $refs): iterable |
321
|
|
|
{ |
322
|
|
|
$types = []; |
323
|
|
|
$ids = []; |
324
|
|
|
foreach ($refs as $ref) { |
325
|
|
|
$types[$ref['$ref']] = true; |
326
|
|
|
$ids[] = $ref['$id']; |
327
|
|
|
} |
328
|
|
|
foreach ($types as $type => $_) { |
329
|
|
|
if (!$this->isResolvable($type)) { |
330
|
|
|
throw new \InvalidArgumentException("Unsupported reference type: " . $type); |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
return $this->getAll($ids); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Creates a record. |
338
|
|
|
* |
339
|
|
|
* @param $record - The record to insert, ready to go |
340
|
|
|
* @return - Whatever MongoDB returns |
|
|
|
|
341
|
|
|
* @throws \Caridea\Dao\Exception\Unreachable If the connection fails |
342
|
|
|
* @throws \Caridea\Dao\Exception\Violating If a constraint is violated |
343
|
|
|
* @throws \Caridea\Dao\Exception\Generic If any other database problem occurs |
344
|
|
|
*/ |
345
|
|
|
protected function doCreate(array $record): WriteResult |
346
|
|
|
{ |
347
|
|
|
if ($this->versioned) { |
348
|
|
|
$record['version'] = 0; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
return $this->doExecute(function (Manager $m, string $c) use ($record) { |
352
|
|
|
$bulk = new \MongoDB\Driver\BulkWrite(); |
353
|
|
|
$bulk->insert($record); |
354
|
|
|
return $m->executeBulkWrite($c, $bulk, $this->writeConcern); |
355
|
|
|
}); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Creates a record using a MongoDB `Persistable`. |
360
|
|
|
* |
361
|
|
|
* @param $record - The document to insert, ready to go |
362
|
|
|
* @return - Whatever MongoDB returns |
|
|
|
|
363
|
|
|
* @throws \Caridea\Dao\Exception\Unreachable If the connection fails |
364
|
|
|
* @throws \Caridea\Dao\Exception\Violating If a constraint is violated |
365
|
|
|
* @throws \Caridea\Dao\Exception\Generic If any other database problem occurs |
366
|
|
|
*/ |
367
|
|
|
protected function doPersist(\MongoDB\BSON\Persistable $record): WriteResult |
368
|
|
|
{ |
369
|
|
|
$this->preInsert($record); |
370
|
|
|
$wr = $this->doExecute(function (Manager $m, string $c) use ($record) { |
371
|
|
|
$bulk = new \MongoDB\Driver\BulkWrite(); |
372
|
|
|
$bulk->insert($record); |
373
|
|
|
return $m->executeBulkWrite($c, $bulk, $this->writeConcern); |
374
|
|
|
}); |
375
|
|
|
$this->postInsert($record); |
376
|
|
|
return $wr; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
/** |
380
|
|
|
* Updates a record. |
381
|
|
|
* |
382
|
|
|
* @param $entity - The entity to update |
383
|
|
|
* @param $version - Optional version for optimistic lock checking |
384
|
|
|
* @return - Whatever MongoDB returns |
|
|
|
|
385
|
|
|
* @throws \Caridea\Dao\Exception\Unreachable If the connection fails |
386
|
|
|
* @throws \Caridea\Dao\Exception\Conflicting If optimistic/pessimistic lock fails |
387
|
|
|
* @throws \Caridea\Dao\Exception\Violating If a constraint is violated |
388
|
|
|
* @throws \Caridea\Dao\Exception\Generic If any other database problem occurs |
389
|
|
|
*/ |
390
|
|
|
protected function doUpdateModifiable(Entity\Modifiable $entity, int $version = null): ?WriteResult |
391
|
|
|
{ |
392
|
|
|
if (!$entity->isDirty()) { |
393
|
|
|
return null; |
394
|
|
|
} |
395
|
|
|
$mid = Getter::getId($entity); |
396
|
|
|
$ops = $entity->getChanges(); |
397
|
|
|
|
398
|
|
|
if ($this->versioned) { |
399
|
|
|
if ($version !== null) { |
400
|
|
|
$orig = $this->findOne(['_id' => $mid]); |
401
|
|
|
if ($version < (int) Getter::get($orig, 'version')) { |
402
|
|
|
throw new \Caridea\Dao\Exception\Conflicting("Document version conflict"); |
403
|
|
|
} |
404
|
|
|
} |
405
|
|
|
$ops['$inc']['version'] = 1; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
$this->preUpdate($entity); |
409
|
|
|
unset($this->cache[(string) $mid]); |
410
|
|
|
$wr = $this->doExecute(function (Manager $m, string $c) use ($mid, $ops) { |
411
|
|
|
$bulk = new \MongoDB\Driver\BulkWrite(); |
412
|
|
|
$bulk->update(['_id' => $mid], $ops); |
413
|
|
|
return $m->executeBulkWrite($c, $bulk, $this->writeConcern); |
414
|
|
|
}); |
415
|
|
|
$this->postUpdate($entity); |
416
|
|
|
return $wr; |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
/** |
420
|
|
|
* Updates a record. |
421
|
|
|
* |
422
|
|
|
* @param \MongoDB\BSON\ObjectID|string $id The document identifier, either a string or `ObjectID` |
423
|
|
|
* @param array<string,array<string,mixed>> $operations The operations to send to MongoDB |
|
|
|
|
424
|
|
|
* @param $version - Optional version for optimistic lock checking |
425
|
|
|
* @return - Whatever MongoDB returns |
|
|
|
|
426
|
|
|
* @throws \Caridea\Dao\Exception\Unreachable If the connection fails |
427
|
|
|
* @throws \Caridea\Dao\Exception\Unretrievable If the document doesn't exist |
428
|
|
|
* @throws \Caridea\Dao\Exception\Conflicting If optimistic/pessimistic lock fails |
429
|
|
|
* @throws \Caridea\Dao\Exception\Violating If a constraint is violated |
430
|
|
|
* @throws \Caridea\Dao\Exception\Generic If any other database problem occurs |
431
|
|
|
*/ |
432
|
|
|
protected function doUpdate($id, array $ops, int $version = null): WriteResult |
433
|
|
|
{ |
434
|
|
|
// ensure record exists |
435
|
|
|
$mid = $this->toId($id); |
436
|
|
|
$orig = $this->get($id); |
437
|
|
|
|
438
|
|
|
// check optimistic locking |
439
|
|
|
if ($this->versioned) { |
440
|
|
|
if ($version !== null) { |
441
|
|
|
if ($version < (int) Getter::get($orig, 'version')) { |
442
|
|
|
throw new \Caridea\Dao\Exception\Conflicting("Document version conflict"); |
443
|
|
|
} |
444
|
|
|
} |
445
|
|
|
$ops['$inc']['version'] = 1; |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
// do update operation |
449
|
|
|
unset($this->cache[(string)$id]); |
450
|
|
|
return $this->doExecute(function (Manager $m, string $c) use ($mid, $ops) { |
451
|
|
|
$bulk = new \MongoDB\Driver\BulkWrite(); |
452
|
|
|
$bulk->update(['_id' => $mid], $ops); |
453
|
|
|
return $m->executeBulkWrite($c, $bulk, $this->writeConcern); |
454
|
|
|
}); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* Deletes a record. |
459
|
|
|
* |
460
|
|
|
* @param \MongoDB\BSON\ObjectID|string $id The document identifier, either a string or `ObjectID` |
461
|
|
|
* @return - Whatever MongoDB returns |
|
|
|
|
462
|
|
|
* @throws \Caridea\Dao\Exception\Unreachable If the connection fails |
463
|
|
|
* @throws \Caridea\Dao\Exception\Unretrievable If the document doesn't exist |
464
|
|
|
* @throws \Caridea\Dao\Exception\Generic If any other database problem occurs |
465
|
|
|
*/ |
466
|
|
|
protected function doDelete($id): WriteResult |
467
|
|
|
{ |
468
|
|
|
$mid = $this->toId($id); |
469
|
|
|
$entity = $this->get($mid); |
470
|
|
|
unset($this->cache[(string)$id]); |
471
|
|
|
$this->preDelete($entity); |
472
|
|
|
$wr = $this->doExecute(function (Manager $m, string $c) use ($mid) { |
473
|
|
|
$bulk = new \MongoDB\Driver\BulkWrite(); |
474
|
|
|
$bulk->delete(['_id' => $mid], ['limit' => 1]); |
475
|
|
|
return $m->executeBulkWrite($c, $bulk, $this->writeConcern); |
476
|
|
|
}); |
477
|
|
|
$this->postDelete($entity); |
478
|
|
|
return $wr; |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
/** |
482
|
|
|
* Executes an aggregation command. |
483
|
|
|
* |
484
|
|
|
* @param iterable<array<string,mixed>> $pipeline The aggregation pipeline operations |
|
|
|
|
485
|
|
|
* @param array<string,mixed> $options Any aggregation options |
486
|
|
|
* @see https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/ |
487
|
|
|
* @since 0.7.2 |
488
|
|
|
*/ |
489
|
|
|
protected function doAggregate(iterable $pipeline, array $options): \MongoDB\Driver\Cursor |
490
|
|
|
{ |
491
|
|
|
return $this->doExecute(function (Manager $m, string $c) use ($pipeline, $options) { |
492
|
|
|
list($db, $coll) = explode('.', $c, 2); |
493
|
|
|
$cmd = [ |
494
|
|
|
'aggregate' => $coll, |
495
|
|
|
'pipeline' => $options, |
496
|
|
|
]; |
497
|
|
|
foreach ($options as $k => $v) { |
498
|
|
|
$cmd[$k] = $v; |
499
|
|
|
} |
500
|
|
|
$command = new \MongoDB\Driver\Command($cmd); |
501
|
|
|
return $m->executeCommand($db, $command, $this->readPreference); |
502
|
|
|
}); |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
/** |
506
|
|
|
* Executes a projection. |
507
|
|
|
* |
508
|
|
|
* @param array<string,mixed> $criteria Field to value pairs |
509
|
|
|
* @param array<string,mixed> $projections Field name to projection value |
510
|
|
|
* (either boolean or projection operator) |
511
|
|
|
* @param $pagination - Optional pagination parameters |
512
|
|
|
* @param $totalCount - Return a `CursorSubset` that includes the total |
513
|
|
|
* number of records. This is only done if `$pagination` is not using |
514
|
|
|
* the defaults. |
515
|
|
|
* @return - The projection cursor |
|
|
|
|
516
|
|
|
* @throws \Caridea\Dao\Exception\Unreachable If the connection fails |
517
|
|
|
* @throws \Caridea\Dao\Exception\Unretrievable If the result cannot be returned |
518
|
|
|
* @throws \Caridea\Dao\Exception\Generic If any other database problem occurs |
519
|
|
|
*/ |
520
|
|
|
protected function doProjection(array $criteria, array $projections, \Caridea\Http\Pagination $pagination = null, bool $totalCount = false): iterable |
521
|
|
|
{ |
522
|
|
|
$total = null; |
523
|
|
|
if ($totalCount === true && $pagination !== null && ($pagination->getMax() != PHP_INT_MAX || $pagination->getOffset() > 0)) { |
524
|
|
|
$total = $this->countAll($criteria); |
525
|
|
|
} |
526
|
|
|
$results = $this->doExecute(function (Manager $m, string $c) use ($criteria, $projections, $pagination) { |
527
|
|
|
$qo = []; |
528
|
|
|
if ($pagination !== null) { |
529
|
|
|
if ($pagination->getMax() != PHP_INT_MAX) { |
530
|
|
|
$qo['limit'] = $pagination->getMax(); |
531
|
|
|
} |
532
|
|
|
$qo['skip'] = $pagination->getOffset(); |
533
|
|
|
$sorts = []; |
534
|
|
|
foreach ($pagination->getOrder() as $k => $v) { |
535
|
|
|
$sorts[$k] = $v ? 1 : -1; |
536
|
|
|
} |
537
|
|
|
if (count($sorts) > 0) { |
538
|
|
|
$qo['sort'] = $sorts; |
539
|
|
|
} |
540
|
|
|
} |
541
|
|
|
if (!$projections->isEmpty()) { |
|
|
|
|
542
|
|
|
$qo['projection'] = $projections; |
543
|
|
|
} |
544
|
|
|
$q = new \MongoDB\Driver\Query($criteria, $qo); |
545
|
|
|
return $m->executeQuery($c, $q, $this->readPreference); |
546
|
|
|
}); |
547
|
|
|
return $total === null ? $results : new CursorSubset($results, $total); |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
/** |
551
|
|
|
* Possibly add the entity to the cache. |
552
|
|
|
* |
553
|
|
|
* @param $entity - The entity to possibly cache |
554
|
|
|
* @return - The same entity that came in |
|
|
|
|
555
|
|
|
*/ |
556
|
|
|
protected function maybeCache($entity) |
557
|
|
|
{ |
558
|
|
|
if ($this->caching && $entity !== null) { |
559
|
|
|
$id = (string) Getter::getId($entity); |
560
|
|
|
if (!array_key_exists($id, $this->cache)) { |
561
|
|
|
$this->cache[$id] = $entity; |
562
|
|
|
} |
563
|
|
|
} |
564
|
|
|
return $entity; |
565
|
|
|
} |
566
|
|
|
|
567
|
|
|
/** |
568
|
|
|
* Possibly add entities to the cache. |
569
|
|
|
* |
570
|
|
|
* @param $entities - The entities to possibly cache |
571
|
|
|
* @return - The same entities that came in |
|
|
|
|
572
|
|
|
*/ |
573
|
|
|
protected function maybeCacheAll(iterable $entities): iterable |
574
|
|
|
{ |
575
|
|
|
if ($this->caching) { |
576
|
|
|
$results = $entities instanceof \MongoDB\Driver\Cursor ? |
|
|
|
|
577
|
|
|
$entities->toArray() : (is_array($entities) ? $entities : iterator_to_array($entities, false)); |
578
|
|
|
foreach ($results as $entity) { |
579
|
|
|
if ($entity !== null) { |
580
|
|
|
$id = (string) Getter::getId($entity); |
581
|
|
|
if (!array_key_exists($id, $this->cache)) { |
582
|
|
|
$this->cache[$id] = $entity; |
583
|
|
|
} |
584
|
|
|
} |
585
|
|
|
} |
586
|
|
|
return $results; |
587
|
|
|
} else { |
588
|
|
|
return $entities; |
589
|
|
|
} |
590
|
|
|
} |
591
|
|
|
|
592
|
|
|
/** |
593
|
|
|
* Gets an entry from the cache |
594
|
|
|
* |
595
|
|
|
* @param $id - The cache key |
596
|
|
|
* @return - The entity found or null |
|
|
|
|
597
|
|
|
*/ |
598
|
|
|
protected function getFromCache(string $id) |
599
|
|
|
{ |
600
|
|
|
return $this->cache[$id] ?? null; |
601
|
|
|
} |
602
|
|
|
} |
603
|
|
|
|
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.json
file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.json
to be in the root folder of your repository.Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the
require
orrequire-dev
section?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceof
checks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.