MongoCursor.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. <?php
  2. /*
  3. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  4. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  5. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  6. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  7. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  8. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  9. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  10. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  11. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  12. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  13. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  14. */
  15. use Alcaeus\MongoDbAdapter\TypeConverter;
  16. use MongoDB\Collection;
  17. use MongoDB\Driver\Cursor;
  18. use MongoDB\Driver\ReadPreference;
  19. use MongoDB\Operation\Find;
  20. /**
  21. * Result object for database query.
  22. * @link http://www.php.net/manual/en/class.mongocursor.php
  23. */
  24. class MongoCursor implements Iterator
  25. {
  26. /**
  27. * @var bool
  28. */
  29. public static $slaveOkay = false;
  30. /**
  31. * @var int
  32. */
  33. static $timeout = 30000;
  34. /**
  35. * @var MongoClient
  36. */
  37. private $connection;
  38. /**
  39. * @var string
  40. */
  41. private $ns;
  42. /**
  43. * @var array
  44. */
  45. private $query;
  46. /**
  47. * @var
  48. */
  49. private $filter;
  50. /**
  51. * @var Collection
  52. */
  53. private $collection;
  54. /**
  55. * @var Cursor
  56. */
  57. private $cursor;
  58. /**
  59. * @var IteratorIterator
  60. */
  61. private $iterator;
  62. private $allowPartialResults;
  63. private $awaitData;
  64. private $batchSize;
  65. private $flags;
  66. private $hint;
  67. private $limit;
  68. private $maxTimeMS;
  69. private $noCursorTimeout;
  70. private $oplogReplay;
  71. private $options = [];
  72. private $projection;
  73. private $readPreference = [];
  74. private $skip;
  75. private $snapshot;
  76. private $sort;
  77. private $tailable;
  78. /**
  79. * Create a new cursor
  80. * @link http://www.php.net/manual/en/mongocursor.construct.php
  81. * @param MongoClient $connection Database connection.
  82. * @param string $ns Full name of database and collection.
  83. * @param array $query Database query.
  84. * @param array $fields Fields to return.
  85. * @return MongoCursor Returns the new cursor
  86. */
  87. public function __construct(MongoClient $connection, $ns, array $query = array(), array $fields = array())
  88. {
  89. $this->connection = $connection;
  90. $this->ns = $ns;
  91. $this->query = $query;
  92. $this->projection = $fields;
  93. $nsParts = explode('.', $ns);
  94. $db = array_shift($nsParts);
  95. $this->collection = $connection->selectCollection($db, implode('.', $nsParts))->getCollection();
  96. }
  97. /**
  98. * Adds a top-level key/value pair to a query
  99. * @link http://www.php.net/manual/en/mongocursor.addoption.php
  100. * @param string $key Fieldname to add.
  101. * @param mixed $value Value to add.
  102. * @throws MongoCursorException
  103. * @return MongoCursor Returns this cursor
  104. */
  105. public function addOption($key, $value)
  106. {
  107. $this->errorIfOpened();
  108. $this->options[$key] = $value;
  109. return $this;
  110. }
  111. /**
  112. * (PECL mongo &gt;= 1.2.11)<br/>
  113. * Sets whether this cursor will wait for a while for a tailable cursor to return more data
  114. * @param bool $wait [optional] <p>If the cursor should wait for more data to become available.</p>
  115. * @return MongoCursor Returns this cursor.
  116. */
  117. public function awaitData($wait = true)
  118. {
  119. $this->errorIfOpened();
  120. $this->awaitData = $wait;
  121. return $this;
  122. }
  123. /**
  124. * Limits the number of elements returned in one batch.
  125. *
  126. * @link http://docs.php.net/manual/en/mongocursor.batchsize.php
  127. * @param int $batchSize The number of results to return per batch
  128. * @return MongoCursor Returns this cursor.
  129. */
  130. public function batchSize($batchSize)
  131. {
  132. $this->errorIfOpened();
  133. $this->batchSize = $batchSize;
  134. return $this;
  135. }
  136. /**
  137. * Counts the number of results for this query
  138. * @link http://www.php.net/manual/en/mongocursor.count.php
  139. * @param bool $foundOnly Send cursor limit and skip information to the count function, if applicable.
  140. * @return int The number of documents returned by this cursor's query.
  141. */
  142. public function count($foundOnly = false)
  143. {
  144. if ($foundOnly && $this->cursor !== null) {
  145. return iterator_count($this->ensureIterator());
  146. }
  147. $optionNames = ['hint', 'limit', 'maxTimeMS', 'skip'];
  148. $options = $foundOnly ? $this->applyOptions($this->options, $optionNames) : $this->options;
  149. return $this->collection->count($this->query, $options);
  150. }
  151. /**
  152. * Returns the current element
  153. * @link http://www.php.net/manual/en/mongocursor.current.php
  154. * @return array
  155. */
  156. public function current()
  157. {
  158. $document = $this->ensureIterator()->current();
  159. if ($document !== null) {
  160. $document = TypeConverter::convertObjectToLegacyArray($document);
  161. }
  162. return $document;
  163. }
  164. /**
  165. * Checks if there are documents that have not been sent yet from the database for this cursor
  166. * @link http://www.php.net/manual/en/mongocursor.dead.php
  167. * @return boolean Returns if there are more results that have not been sent to the client, yet.
  168. */
  169. public function dead()
  170. {
  171. return $this->ensureCursor()->isDead();
  172. }
  173. /**
  174. * Execute the query
  175. * @link http://www.php.net/manual/en/mongocursor.doquery.php
  176. * @throws MongoConnectionException if it cannot reach the database.
  177. * @return void
  178. */
  179. protected function doQuery()
  180. {
  181. $options = $this->applyOptions($this->options);
  182. $this->cursor = $this->collection->find($this->query, $options);
  183. }
  184. /**
  185. * Return an explanation of the query, often useful for optimization and debugging
  186. * @link http://www.php.net/manual/en/mongocursor.explain.php
  187. * @return array Returns an explanation of the query.
  188. */
  189. public function explain()
  190. {
  191. $this->notImplemented();
  192. }
  193. /**
  194. * Sets the fields for a query
  195. * @link http://www.php.net/manual/en/mongocursor.fields.php
  196. * @param array $f Fields to return (or not return).
  197. * @throws MongoCursorException
  198. * @return MongoCursor
  199. */
  200. public function fields(array $f)
  201. {
  202. $this->errorIfOpened();
  203. $this->projection = $f;
  204. return $this;
  205. }
  206. /**
  207. * Return the next object to which this cursor points, and advance the cursor
  208. * @link http://www.php.net/manual/en/mongocursor.getnext.php
  209. * @throws MongoConnectionException
  210. * @throws MongoCursorTimeoutException
  211. * @return array Returns the next object
  212. */
  213. public function getNext()
  214. {
  215. $this->next();
  216. return $this->current();
  217. }
  218. /**
  219. * Get the read preference for this query
  220. * @link http://www.php.net/manual/en/mongocursor.getreadpreference.php
  221. * @return array
  222. */
  223. public function getReadPreference()
  224. {
  225. return $this->readPreference;
  226. }
  227. /**
  228. * Checks if there are any more elements in this cursor
  229. * @link http://www.php.net/manual/en/mongocursor.hasnext.php
  230. * @throws MongoConnectionException
  231. * @throws MongoCursorTimeoutException
  232. * @return bool Returns true if there is another element
  233. */
  234. public function hasNext()
  235. {
  236. $this->errorIfOpened();
  237. $this->notImplemented();
  238. }
  239. /**
  240. * Gives the database a hint about the query
  241. * @link http://www.php.net/manual/en/mongocursor.hint.php
  242. * @param array|string $keyPattern Indexes to use for the query.
  243. * @throws MongoCursorException
  244. * @return MongoCursor Returns this cursor
  245. */
  246. public function hint($keyPattern)
  247. {
  248. $this->errorIfOpened();
  249. $this->hint = $keyPattern;
  250. return $this;
  251. }
  252. /**
  253. * Sets whether this cursor will timeout
  254. * @link http://www.php.net/manual/en/mongocursor.immortal.php
  255. * @param bool $liveForever If the cursor should be immortal.
  256. * @throws MongoCursorException
  257. * @return MongoCursor Returns this cursor
  258. */
  259. public function immortal($liveForever = true)
  260. {
  261. $this->errorIfOpened();
  262. $this->noCursorTimeout = $liveForever;
  263. return $this;
  264. }
  265. /**
  266. * Gets the query, fields, limit, and skip for this cursor
  267. * @link http://www.php.net/manual/en/mongocursor.info.php
  268. * @return array The query, fields, limit, and skip for this cursor as an associative array.
  269. */
  270. public function info()
  271. {
  272. $info = [
  273. 'ns' => $this->ns,
  274. 'limit' => $this->limit,
  275. 'batchSize' => $this->batchSize,
  276. 'skip' => $this->skip,
  277. 'flags' => $this->flags,
  278. 'query' => $this->query,
  279. 'fields' => $this->projection,
  280. 'started_iterating' => $this->cursor !== null,
  281. ];
  282. if ($info['started_iterating']) {
  283. switch ($this->cursor->getServer()->getType()) {
  284. case \MongoDB\Driver\Server::TYPE_ARBITER:
  285. $typeString = 'ARBITER';
  286. break;
  287. case \MongoDB\Driver\Server::TYPE_MONGOS:
  288. $typeString = 'MONGOS';
  289. break;
  290. case \MongoDB\Driver\Server::TYPE_PRIMARY:
  291. $typeString = 'PRIMARY';
  292. break;
  293. case \MongoDB\Driver\Server::TYPE_SECONDARY:
  294. $typeString = 'SECONDARY';
  295. break;
  296. default:
  297. $typeString = 'STANDALONE';
  298. }
  299. $info = array_merge($info, [
  300. 'id' => (string) $this->cursor->getId(),
  301. 'at' => null, // @todo Complete info for cursor that is iterating
  302. 'numReturned' => null, // @todo Complete info for cursor that is iterating
  303. 'server' => null, // @todo Complete info for cursor that is iterating
  304. 'host' => $this->cursor->getServer()->getHost(),
  305. 'port' => $this->cursor->getServer()->getPort(),
  306. 'connection_type_desc' => $typeString,
  307. ]);
  308. }
  309. return $info;
  310. }
  311. /**
  312. * Returns the current result's _id
  313. * @link http://www.php.net/manual/en/mongocursor.key.php
  314. * @return string The current result's _id as a string.
  315. */
  316. public function key()
  317. {
  318. return $this->ensureIterator()->key();
  319. }
  320. /**
  321. * Limits the number of results returned
  322. * @link http://www.php.net/manual/en/mongocursor.limit.php
  323. * @param int $num The number of results to return.
  324. * @throws MongoCursorException
  325. * @return MongoCursor Returns this cursor
  326. */
  327. public function limit($num)
  328. {
  329. $this->errorIfOpened();
  330. $this->limit = $num;
  331. return $this;
  332. }
  333. /**
  334. * @param int $ms
  335. * @return $this
  336. * @throws MongoCursorException
  337. */
  338. public function maxTimeMS($ms)
  339. {
  340. $this->errorIfOpened();
  341. $this->maxTimeMS = $ms;
  342. return $this;
  343. }
  344. /**
  345. * Advances the cursor to the next result
  346. * @link http://www.php.net/manual/en/mongocursor.next.php
  347. * @throws MongoConnectionException
  348. * @throws MongoCursorTimeoutException
  349. * @return void
  350. */
  351. public function next()
  352. {
  353. $this->ensureIterator()->next();
  354. }
  355. /**
  356. * @link http://www.php.net/manual/en/mongocursor.partial.php
  357. * @param bool $okay [optional] <p>If receiving partial results is okay.</p>
  358. * @return MongoCursor Returns this cursor.
  359. */
  360. public function partial($okay = true)
  361. {
  362. $this->allowPartialResults = $okay;
  363. return $this;
  364. }
  365. /**
  366. * Clears the cursor
  367. * @link http://www.php.net/manual/en/mongocursor.reset.php
  368. * @return void
  369. */
  370. public function reset()
  371. {
  372. $this->cursor = null;
  373. $this->iterator = null;
  374. }
  375. /**
  376. * Returns the cursor to the beginning of the result set
  377. * @throws MongoConnectionException
  378. * @throws MongoCursorTimeoutException
  379. * @return void
  380. */
  381. public function rewind()
  382. {
  383. // Note: rewinding the cursor means recreating it internally
  384. $this->reset();
  385. $this->ensureIterator()->rewind();
  386. }
  387. /**
  388. * @link http://www.php.net/manual/en/mongocursor.setflag.php
  389. * @param int $flag
  390. * @param bool $set
  391. * @return MongoCursor
  392. */
  393. public function setFlag($flag, $set = true)
  394. {
  395. $this->notImplemented();
  396. }
  397. /**
  398. * @link http://www.php.net/manual/en/mongocursor.setreadpreference.php
  399. * @param string $readPreference
  400. * @param array $tags
  401. * @return MongoCursor Returns this cursor.
  402. */
  403. public function setReadPreference($readPreference, array $tags = [])
  404. {
  405. $availableReadPreferences = [
  406. MongoClient::RP_PRIMARY,
  407. MongoClient::RP_PRIMARY_PREFERRED,
  408. MongoClient::RP_SECONDARY,
  409. MongoClient::RP_SECONDARY_PREFERRED,
  410. MongoClient::RP_NEAREST
  411. ];
  412. if (! in_array($readPreference, $availableReadPreferences)) {
  413. trigger_error("The value '$readPreference' is not valid as read preference type", E_WARNING);
  414. return $this;
  415. }
  416. if ($readPreference == MongoClient::RP_PRIMARY && count($tags)) {
  417. trigger_error("You can't use read preference tags with a read preference of PRIMARY", E_WARNING);
  418. return $this;
  419. }
  420. $this->readPreference = [
  421. 'type' => $readPreference,
  422. 'tagsets' => $tags
  423. ];
  424. return $this;
  425. }
  426. /**
  427. * Skips a number of results
  428. * @link http://www.php.net/manual/en/mongocursor.skip.php
  429. * @param int $num The number of results to skip.
  430. * @throws MongoCursorException
  431. * @return MongoCursor Returns this cursor
  432. */
  433. public function skip($num)
  434. {
  435. $this->errorIfOpened();
  436. $this->skip = $num;
  437. return $this;
  438. }
  439. /**
  440. * Sets whether this query can be done on a slave
  441. * This method will override the static class variable slaveOkay.
  442. * @link http://www.php.net/manual/en/mongocursor.slaveOkay.php
  443. * @param boolean $okay If it is okay to query the slave.
  444. * @throws MongoCursorException
  445. * @return MongoCursor Returns this cursor
  446. */
  447. public function slaveOkay($okay = true)
  448. {
  449. $this->errorIfOpened();
  450. static::$slaveOkay = $okay;
  451. }
  452. /**
  453. * Use snapshot mode for the query
  454. * @link http://www.php.net/manual/en/mongocursor.snapshot.php
  455. * @throws MongoCursorException
  456. * @return MongoCursor Returns this cursor
  457. */
  458. public function snapshot()
  459. {
  460. $this->errorIfOpened();
  461. $this->snapshot = true;
  462. return $this;
  463. }
  464. /**
  465. * Sorts the results by given fields
  466. * @link http://www.php.net/manual/en/mongocursor.sort.php
  467. * @param array $fields An array of fields by which to sort. Each element in the array has as key the field name, and as value either 1 for ascending sort, or -1 for descending sort
  468. * @throws MongoCursorException
  469. * @return MongoCursor Returns the same cursor that this method was called on
  470. */
  471. public function sort(array $fields)
  472. {
  473. $this->errorIfOpened();
  474. $this->sort = $fields;
  475. return $this;
  476. }
  477. /**
  478. * Sets whether this cursor will be left open after fetching the last results
  479. * @link http://www.php.net/manual/en/mongocursor.tailable.php
  480. * @param bool $tail If the cursor should be tailable.
  481. * @return MongoCursor Returns this cursor
  482. */
  483. public function tailable($tail = true)
  484. {
  485. $this->errorIfOpened();
  486. $this->tailable = $tail;
  487. return $this;
  488. }
  489. /**
  490. * Sets a client-side timeout for this query
  491. * @link http://www.php.net/manual/en/mongocursor.timeout.php
  492. * @param int $ms The number of milliseconds for the cursor to wait for a response. By default, the cursor will wait forever.
  493. * @throws MongoCursorTimeoutException
  494. * @return MongoCursor Returns this cursor
  495. */
  496. public function timeout($ms)
  497. {
  498. $this->notImplemented();
  499. }
  500. /**
  501. * Checks if the cursor is reading a valid result.
  502. * @link http://www.php.net/manual/en/mongocursor.valid.php
  503. * @return boolean If the current result is not null.
  504. */
  505. public function valid()
  506. {
  507. return $this->ensureIterator()->valid();
  508. }
  509. /**
  510. * Applies all options set on the cursor, overwriting any options that have already been set
  511. *
  512. * @param array $options Existing options array
  513. * @param array $optionNames Array of option names to be applied (will be read from properties)
  514. * @return array
  515. */
  516. private function applyOptions($options, $optionNames = null)
  517. {
  518. if ($optionNames === null) {
  519. $optionNames = [
  520. 'allowPartialResults',
  521. 'batchSize',
  522. 'cursorType',
  523. 'limit',
  524. 'maxTimeMS',
  525. 'modifiers',
  526. 'noCursorTimeout',
  527. 'oplogReplay',
  528. 'projection',
  529. 'readPreference',
  530. 'skip',
  531. 'sort',
  532. ];
  533. }
  534. foreach ($optionNames as $option) {
  535. $converter = 'convert' . ucfirst($option);
  536. $value = method_exists($this, $converter) ? $this->$converter() : $this->$option;
  537. if ($value === null) {
  538. continue;
  539. }
  540. $options[$option] = $value;
  541. }
  542. return $options;
  543. }
  544. /**
  545. * @return int|null
  546. */
  547. private function convertCursorType()
  548. {
  549. if (! $this->tailable) {
  550. return null;
  551. }
  552. return $this->awaitData ? Find::TAILABLE_AWAIT : Find::TAILABLE;
  553. }
  554. private function convertModifiers()
  555. {
  556. $modifiers = array_key_exists('modifiers', $this->options) ? $this->options['modifiers'] : [];
  557. foreach (['hint', 'snapshot'] as $modifier) {
  558. if ($this->$modifier === null) {
  559. continue;
  560. }
  561. $modifiers['$' . $modifier] = $this->$modifier;
  562. }
  563. return $modifiers;
  564. }
  565. /**
  566. * @return ReadPreference|null
  567. */
  568. private function convertReadPreference()
  569. {
  570. $type = array_key_exists('type', $this->readPreference) ? $this->readPreference['type'] : null;
  571. if ($type === null) {
  572. return static::$slaveOkay ? new ReadPreference(ReadPreference::RP_SECONDARY_PREFERRED) : null;
  573. }
  574. switch ($type) {
  575. case MongoClient::RP_PRIMARY_PREFERRED:
  576. $mode = ReadPreference::RP_PRIMARY_PREFERRED;
  577. break;
  578. case MongoClient::RP_SECONDARY:
  579. $mode = ReadPreference::RP_SECONDARY;
  580. break;
  581. case MongoClient::RP_SECONDARY_PREFERRED:
  582. $mode = ReadPreference::RP_SECONDARY_PREFERRED;
  583. break;
  584. case MongoClient::RP_NEAREST:
  585. $mode = ReadPreference::RP_NEAREST;
  586. break;
  587. default:
  588. $mode = ReadPreference::RP_PRIMARY;
  589. }
  590. $tagSets = array_key_exists('tagsets', $this->readPreference) ? $this->readPreference['tagsets'] : [];
  591. return new ReadPreference($mode, $tagSets);
  592. }
  593. /**
  594. * @return Cursor
  595. */
  596. private function ensureCursor()
  597. {
  598. if ($this->cursor === null) {
  599. $this->doQuery();
  600. }
  601. return $this->cursor;
  602. }
  603. private function errorIfOpened()
  604. {
  605. if ($this->cursor === null) {
  606. return;
  607. }
  608. throw new MongoCursorException('cannot modify cursor after beginning iteration.');
  609. }
  610. /**
  611. * @return IteratorIterator
  612. */
  613. private function ensureIterator()
  614. {
  615. if ($this->iterator === null) {
  616. $this->iterator = new IteratorIterator($this->ensureCursor());
  617. }
  618. return $this->iterator;
  619. }
  620. protected function notImplemented()
  621. {
  622. throw new \Exception('Not implemented');
  623. }
  624. }