MongoCollection.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  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\Helper;
  16. use Alcaeus\MongoDbAdapter\TypeConverter;
  17. /**
  18. * Represents a database collection.
  19. * @link http://www.php.net/manual/en/class.mongocollection.php
  20. */
  21. class MongoCollection
  22. {
  23. use Helper\ReadPreference;
  24. use Helper\SlaveOkay;
  25. use Helper\WriteConcern;
  26. const ASCENDING = 1;
  27. const DESCENDING = -1;
  28. /**
  29. * @var MongoDB
  30. */
  31. public $db = NULL;
  32. /**
  33. * @var string
  34. */
  35. protected $name;
  36. /**
  37. * @var \MongoDB\Collection
  38. */
  39. protected $collection;
  40. /**
  41. * Creates a new collection
  42. *
  43. * @link http://www.php.net/manual/en/mongocollection.construct.php
  44. * @param MongoDB $db Parent database.
  45. * @param string $name Name for this collection.
  46. * @throws Exception
  47. * @return MongoCollection
  48. */
  49. public function __construct(MongoDB $db, $name)
  50. {
  51. $this->db = $db;
  52. $this->name = $name;
  53. $this->setReadPreferenceFromArray($db->getReadPreference());
  54. $this->setWriteConcernFromArray($db->getWriteConcern());
  55. $this->createCollectionObject();
  56. }
  57. /**
  58. * Gets the underlying collection for this object
  59. *
  60. * @internal This part is not of the ext-mongo API and should not be used
  61. * @return \MongoDB\Collection
  62. */
  63. public function getCollection()
  64. {
  65. return $this->collection;
  66. }
  67. /**
  68. * String representation of this collection
  69. *
  70. * @link http://www.php.net/manual/en/mongocollection.--tostring.php
  71. * @return string Returns the full name of this collection.
  72. */
  73. public function __toString()
  74. {
  75. return (string) $this->db . '.' . $this->name;
  76. }
  77. /**
  78. * Gets a collection
  79. *
  80. * @link http://www.php.net/manual/en/mongocollection.get.php
  81. * @param string $name The next string in the collection name.
  82. * @return MongoCollection
  83. */
  84. public function __get($name)
  85. {
  86. // Handle w and wtimeout properties that replicate data stored in $readPreference
  87. if ($name === 'w' || $name === 'wtimeout') {
  88. return $this->getWriteConcern()[$name];
  89. }
  90. return $this->db->selectCollection($this->name . '.' . $name);
  91. }
  92. /**
  93. * @param string $name
  94. * @param mixed $value
  95. */
  96. public function __set($name, $value)
  97. {
  98. if ($name === 'w' || $name === 'wtimeout') {
  99. $this->setWriteConcernFromArray([$name => $value] + $this->getWriteConcern());
  100. $this->createCollectionObject();
  101. }
  102. }
  103. /**
  104. * Perform an aggregation using the aggregation framework
  105. *
  106. * @link http://www.php.net/manual/en/mongocollection.aggregate.php
  107. * @param array $pipeline
  108. * @param array $op
  109. * @return array
  110. */
  111. public function aggregate(array $pipeline, array $op = [])
  112. {
  113. if (! TypeConverter::isNumericArray($pipeline)) {
  114. $pipeline = [];
  115. $options = [];
  116. $i = 0;
  117. foreach (func_get_args() as $operator) {
  118. $i++;
  119. if (! is_array($operator)) {
  120. trigger_error("Argument $i is not an array", E_WARNING);
  121. return;
  122. }
  123. $pipeline[] = $operator;
  124. }
  125. } else {
  126. $options = $op;
  127. }
  128. $command = [
  129. 'aggregate' => $this->name,
  130. 'pipeline' => $pipeline
  131. ];
  132. $command += $options;
  133. return $this->db->command($command, [], $hash);
  134. }
  135. /**
  136. * Execute an aggregation pipeline command and retrieve results through a cursor
  137. *
  138. * @link http://php.net/manual/en/mongocollection.aggregatecursor.php
  139. * @param array $pipeline
  140. * @param array $options
  141. * @return MongoCommandCursor
  142. */
  143. public function aggregateCursor(array $pipeline, array $options = [])
  144. {
  145. // Build command manually, can't use mongo-php-library here
  146. $command = [
  147. 'aggregate' => $this->name,
  148. 'pipeline' => $pipeline
  149. ];
  150. // Convert cursor option
  151. if (! isset($options['cursor']) || $options['cursor'] === true || $options['cursor'] === []) {
  152. // Cursor option needs to be an object convert bools and empty arrays since those won't be handled by TypeConverter
  153. $options['cursor'] = new \stdClass;
  154. }
  155. $command += $options;
  156. $cursor = new MongoCommandCursor($this->db->getConnection(), (string)$this, $command);
  157. $cursor->setReadPreference($this->getReadPreference());
  158. return $cursor;
  159. }
  160. /**
  161. * Returns this collection's name
  162. *
  163. * @link http://www.php.net/manual/en/mongocollection.getname.php
  164. * @return string
  165. */
  166. public function getName()
  167. {
  168. return $this->name;
  169. }
  170. /**
  171. * {@inheritdoc}
  172. */
  173. public function setReadPreference($readPreference, $tags = null)
  174. {
  175. $result = $this->setReadPreferenceFromParameters($readPreference, $tags);
  176. $this->createCollectionObject();
  177. return $result;
  178. }
  179. /**
  180. * {@inheritdoc}
  181. */
  182. public function setWriteConcern($wstring, $wtimeout = 0)
  183. {
  184. $result = $this->setWriteConcernFromParameters($wstring, $wtimeout);
  185. $this->createCollectionObject();
  186. return $result;
  187. }
  188. /**
  189. * Drops this collection
  190. *
  191. * @link http://www.php.net/manual/en/mongocollection.drop.php
  192. * @return array Returns the database response.
  193. */
  194. public function drop()
  195. {
  196. return $this->collection->drop();
  197. }
  198. /**
  199. * Validates this collection
  200. *
  201. * @link http://www.php.net/manual/en/mongocollection.validate.php
  202. * @param bool $scan_data Only validate indices, not the base collection.
  203. * @return array Returns the database's evaluation of this object.
  204. */
  205. public function validate($scan_data = FALSE)
  206. {
  207. $command = [
  208. 'validate' => $this->name,
  209. 'full' => $scan_data,
  210. ];
  211. return $this->db->command($command, [], $hash);
  212. }
  213. /**
  214. * Inserts an array into the collection
  215. *
  216. * @link http://www.php.net/manual/en/mongocollection.insert.php
  217. * @param array|object $a
  218. * @param array $options
  219. * @throws MongoException if the inserted document is empty or if it contains zero-length keys. Attempting to insert an object with protected and private properties will cause a zero-length key error.
  220. * @throws MongoCursorException if the "w" option is set and the write fails.
  221. * @throws MongoCursorTimeoutException if the "w" option is set to a value greater than one and the operation takes longer than MongoCursor::$timeout milliseconds to complete. This does not kill the operation on the server, it is a client-side timeout. The operation in MongoCollection::$wtimeout is milliseconds.
  222. * @return bool|array Returns an array containing the status of the insertion if the "w" option is set.
  223. */
  224. public function insert($a, array $options = [])
  225. {
  226. return $this->collection->insertOne(TypeConverter::convertLegacyArrayToObject($a), $options);
  227. }
  228. /**
  229. * Inserts multiple documents into this collection
  230. *
  231. * @link http://www.php.net/manual/en/mongocollection.batchinsert.php
  232. * @param array $a An array of arrays.
  233. * @param array $options Options for the inserts.
  234. * @throws MongoCursorException
  235. * @return mixed f "safe" is set, returns an associative array with the status of the inserts ("ok") and any error that may have occured ("err"). Otherwise, returns TRUE if the batch insert was successfully sent, FALSE otherwise.
  236. */
  237. public function batchInsert(array $a, array $options = [])
  238. {
  239. return $this->collection->insertMany($a, $options);
  240. }
  241. /**
  242. * Update records based on a given criteria
  243. *
  244. * @link http://www.php.net/manual/en/mongocollection.update.php
  245. * @param array $criteria Description of the objects to update.
  246. * @param array $newobj The object with which to update the matching records.
  247. * @param array $options
  248. * @throws MongoCursorException
  249. * @return boolean
  250. */
  251. public function update(array $criteria , array $newobj, array $options = [])
  252. {
  253. $multiple = ($options['multiple']) ? $options['multiple'] : false;
  254. $method = $multiple ? 'updateMany' : 'updateOne';
  255. return $this->collection->$method($criteria, $newobj, $options);
  256. }
  257. /**
  258. * Remove records from this collection
  259. *
  260. * @link http://www.php.net/manual/en/mongocollection.remove.php
  261. * @param array $criteria Query criteria for the documents to delete.
  262. * @param array $options An array of options for the remove operation.
  263. * @throws MongoCursorException
  264. * @throws MongoCursorTimeoutException
  265. * @return bool|array Returns an array containing the status of the removal
  266. * if the "w" option is set. Otherwise, returns TRUE.
  267. */
  268. public function remove(array $criteria = [], array $options = [])
  269. {
  270. $multiple = isset($options['justOne']) ? !$options['justOne'] : false;
  271. $method = $multiple ? 'deleteMany' : 'deleteOne';
  272. return $this->collection->$method($criteria, $options);
  273. }
  274. /**
  275. * Querys this collection
  276. *
  277. * @link http://www.php.net/manual/en/mongocollection.find.php
  278. * @param array $query The fields for which to search.
  279. * @param array $fields Fields of the results to return.
  280. * @return MongoCursor
  281. */
  282. public function find(array $query = [], array $fields = [])
  283. {
  284. $cursor = new MongoCursor($this->db->getConnection(), (string)$this, $query, $fields);
  285. $cursor->setReadPreference($this->getReadPreference());
  286. return $cursor;
  287. }
  288. /**
  289. * Retrieve a list of distinct values for the given key across a collection
  290. *
  291. * @link http://www.php.net/manual/ru/mongocollection.distinct.php
  292. * @param string $key The key to use.
  293. * @param array $query An optional query parameters
  294. * @return array|bool Returns an array of distinct values, or FALSE on failure
  295. */
  296. public function distinct($key, array $query = [])
  297. {
  298. return array_map([TypeConverter::class, 'convertToLegacyType'], $this->collection->distinct($key, $query));
  299. }
  300. /**
  301. * Update a document and return it
  302. * @link http://www.php.net/manual/ru/mongocollection.findandmodify.php
  303. * @param array $query The query criteria to search for.
  304. * @param array $update The update criteria.
  305. * @param array $fields Optionally only return these fields.
  306. * @param array $options An array of options to apply, such as remove the match document from the DB and return it.
  307. * @return array Returns the original document, or the modified document when new is set.
  308. */
  309. public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
  310. {
  311. $query = TypeConverter::convertLegacyArrayToObject($query);
  312. if (isset($options['remove'])) {
  313. unset($options['remove']);
  314. $document = $this->collection->findOneAndDelete($query, $options);
  315. } else {
  316. $update = is_array($update) ? TypeConverter::convertLegacyArrayToObject($update) : [];
  317. if (isset($options['new'])) {
  318. $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
  319. unset($options['new']);
  320. }
  321. $options['projection'] = is_array($fields) ? TypeConverter::convertLegacyArrayToObject($fields) : [];
  322. $document = $this->collection->findOneAndUpdate($query, $update, $options);
  323. }
  324. if ($document) {
  325. $document = TypeConverter::convertObjectToLegacyArray($document);
  326. }
  327. return $document;
  328. }
  329. /**
  330. * Querys this collection, returning a single element
  331. * @link http://www.php.net/manual/en/mongocollection.findone.php
  332. * @param array $query The fields for which to search.
  333. * @param array $fields Fields of the results to return.
  334. * @return array|null
  335. */
  336. public function findOne(array $query = [], array $fields = [])
  337. {
  338. $document = $this->collection->findOne(TypeConverter::convertLegacyArrayToObject($query), ['projection' => $fields]);
  339. if ($document !== null) {
  340. $document = TypeConverter::convertObjectToLegacyArray($document);
  341. }
  342. return $document;
  343. }
  344. /**
  345. * Creates an index on the given field(s), or does nothing if the index already exists
  346. * @link http://www.php.net/manual/en/mongocollection.createindex.php
  347. * @param array $keys Field or fields to use as index.
  348. * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
  349. * @return array Returns the database response.
  350. *
  351. * @todo This method does not yet return the correct result
  352. */
  353. public function createIndex(array $keys, array $options = [])
  354. {
  355. // Note: this is what the result array should look like
  356. // $expected = [
  357. // 'createdCollectionAutomatically' => true,
  358. // 'numIndexesBefore' => 1,
  359. // 'numIndexesAfter' => 2,
  360. // 'ok' => 1.0
  361. // ];
  362. return $this->collection->createIndex($keys, $options);
  363. }
  364. /**
  365. * @deprecated Use MongoCollection::createIndex() instead.
  366. * Creates an index on the given field(s), or does nothing if the index already exists
  367. * @link http://www.php.net/manual/en/mongocollection.ensureindex.php
  368. * @param array $keys Field or fields to use as index.
  369. * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
  370. * @return boolean always true
  371. */
  372. public function ensureIndex(array $keys, array $options = [])
  373. {
  374. $this->createIndex($keys, $options);
  375. return true;
  376. }
  377. /**
  378. * Deletes an index from this collection
  379. * @link http://www.php.net/manual/en/mongocollection.deleteindex.php
  380. * @param string|array $keys Field or fields from which to delete the index.
  381. * @return array Returns the database response.
  382. */
  383. public function deleteIndex($keys)
  384. {
  385. if (is_string($keys)) {
  386. $indexName = $keys;
  387. } elseif (is_array($keys)) {
  388. $indexName = self::toIndexString($keys);
  389. } else {
  390. throw new \InvalidArgumentException();
  391. }
  392. return TypeConverter::convertObjectToLegacyArray($this->collection->dropIndex($indexName));
  393. }
  394. /**
  395. * Delete all indexes for this collection
  396. * @link http://www.php.net/manual/en/mongocollection.deleteindexes.php
  397. * @return array Returns the database response.
  398. */
  399. public function deleteIndexes()
  400. {
  401. return TypeConverter::convertObjectToLegacyArray($this->collection->dropIndexes());
  402. }
  403. /**
  404. * Returns an array of index names for this collection
  405. * @link http://www.php.net/manual/en/mongocollection.getindexinfo.php
  406. * @return array Returns a list of index names.
  407. */
  408. public function getIndexInfo()
  409. {
  410. $convertIndex = function(\MongoDB\Model\IndexInfo $indexInfo) {
  411. return [
  412. 'v' => $indexInfo->getVersion(),
  413. 'key' => $indexInfo->getKey(),
  414. 'name' => $indexInfo->getName(),
  415. 'ns' => $indexInfo->getNamespace(),
  416. ];
  417. };
  418. return array_map($convertIndex, iterator_to_array($this->collection->listIndexes()));
  419. }
  420. /**
  421. * Counts the number of documents in this collection
  422. * @link http://www.php.net/manual/en/mongocollection.count.php
  423. * @param array|stdClass $query
  424. * @return int Returns the number of documents matching the query.
  425. */
  426. public function count($query = [])
  427. {
  428. return $this->collection->count($query);
  429. }
  430. /**
  431. * Saves an object to this collection
  432. *
  433. * @link http://www.php.net/manual/en/mongocollection.save.php
  434. * @param array|object $a Array to save. If an object is used, it may not have protected or private properties.
  435. * @param array $options Options for the save.
  436. * @throws MongoException if the inserted document is empty or if it contains zero-length keys. Attempting to insert an object with protected and private properties will cause a zero-length key error.
  437. * @throws MongoCursorException if the "w" option is set and the write fails.
  438. * @throws MongoCursorTimeoutException if the "w" option is set to a value greater than one and the operation takes longer than MongoCursor::$timeout milliseconds to complete. This does not kill the operation on the server, it is a client-side timeout. The operation in MongoCollection::$wtimeout is milliseconds.
  439. * @return array|boolean If w was set, returns an array containing the status of the save.
  440. * Otherwise, returns a boolean representing if the array was not empty (an empty array will not be inserted).
  441. */
  442. public function save($a, array $options = [])
  443. {
  444. if (is_object($a)) {
  445. $a = (array)$a;
  446. }
  447. if ( ! array_key_exists('_id', $a)) {
  448. $id = new \MongoId();
  449. } else {
  450. $id = $a['_id'];
  451. unset($a['_id']);
  452. }
  453. $filter = ['_id' => $id];
  454. $filter = TypeConverter::convertLegacyArrayToObject($filter);
  455. $a = TypeConverter::convertLegacyArrayToObject($a);
  456. return $this->collection->updateOne($filter, ['$set' => $a], ['upsert' => true]);
  457. }
  458. /**
  459. * Creates a database reference
  460. *
  461. * @link http://www.php.net/manual/en/mongocollection.createdbref.php
  462. * @param array $a Object to which to create a reference.
  463. * @return array Returns a database reference array.
  464. */
  465. public function createDBRef(array $a)
  466. {
  467. return \MongoDBRef::create($this->name, $a['_id']);
  468. }
  469. /**
  470. * Fetches the document pointed to by a database reference
  471. *
  472. * @link http://www.php.net/manual/en/mongocollection.getdbref.php
  473. * @param array $ref A database reference.
  474. * @return array Returns the database document pointed to by the reference.
  475. */
  476. public function getDBRef(array $ref)
  477. {
  478. return \MongoDBRef::get($this->db, $ref);
  479. }
  480. /**
  481. * @param mixed $keys
  482. * @static
  483. * @return string
  484. */
  485. protected static function toIndexString($keys)
  486. {
  487. $result = '';
  488. foreach ($keys as $name => $direction) {
  489. $result .= sprintf('%s_%d', $name, $direction);
  490. }
  491. return $result;
  492. }
  493. /**
  494. * Performs an operation similar to SQL's GROUP BY command
  495. *
  496. * @link http://www.php.net/manual/en/mongocollection.group.php
  497. * @param mixed $keys Fields to group by. If an array or non-code object is passed, it will be the key used to group results.
  498. * @param array $initial Initial value of the aggregation counter object.
  499. * @param MongoCode $reduce A function that aggregates (reduces) the objects iterated.
  500. * @param array $condition An condition that must be true for a row to be considered.
  501. * @return array
  502. */
  503. public function group($keys, array $initial, $reduce, array $condition = [])
  504. {
  505. if (is_string($reduce)) {
  506. $reduce = new MongoCode($reduce);
  507. }
  508. if ( ! $reduce instanceof MongoCode) {
  509. throw new \InvalidArgumentExcption('reduce parameter should be a string or MongoCode instance.');
  510. }
  511. $command = [
  512. 'group' => [
  513. 'ns' => $this->name,
  514. '$reduce' => (string)$reduce,
  515. 'initial' => $initial,
  516. 'cond' => $condition,
  517. ],
  518. ];
  519. if ($keys instanceof MongoCode) {
  520. $command['group']['$keyf'] = (string)$keys;
  521. } else {
  522. $command['group']['key'] = $keys;
  523. }
  524. if (array_key_exists('condition', $condition)) {
  525. $command['group']['cond'] = $condition['condition'];
  526. }
  527. if (array_key_exists('finalize', $condition)) {
  528. if ($condition['finalize'] instanceof MongoCode) {
  529. $condition['finalize'] = (string)$condition['finalize'];
  530. }
  531. $command['group']['finalize'] = $condition['finalize'];
  532. }
  533. return $this->db->command($command, [], $hash);
  534. }
  535. /**
  536. * Returns an array of cursors to iterator over a full collection in parallel
  537. *
  538. * @link http://www.php.net/manual/en/mongocollection.parallelcollectionscan.php
  539. * @param int $num_cursors The number of cursors to request from the server. Please note, that the server can return less cursors than you requested.
  540. * @return MongoCommandCursor[]
  541. */
  542. public function parallelCollectionScan($num_cursors)
  543. {
  544. $this->notImplemented();
  545. }
  546. protected function notImplemented()
  547. {
  548. throw new \Exception('Not implemented');
  549. }
  550. /**
  551. * @return \MongoDB\Collection
  552. */
  553. private function createCollectionObject()
  554. {
  555. $options = [
  556. 'readPreference' => $this->readPreference,
  557. 'writeConcern' => $this->writeConcern,
  558. ];
  559. if ($this->collection === null) {
  560. $this->collection = $this->db->getDb()->selectCollection($this->name, $options);
  561. } else {
  562. $this->collection = $this->collection->withOptions($options);
  563. }
  564. }
  565. }