MongoCollection.php 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069
  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. if (class_exists('MongoCollection', false)) {
  16. return;
  17. }
  18. use Alcaeus\MongoDbAdapter\Helper;
  19. use Alcaeus\MongoDbAdapter\TypeConverter;
  20. use Alcaeus\MongoDbAdapter\ExceptionConverter;
  21. use MongoDB\Driver\Exception\CommandException;
  22. use MongoDB\Model\IndexInput;
  23. /**
  24. * Represents a database collection.
  25. * @link http://www.php.net/manual/en/class.mongocollection.php
  26. */
  27. class MongoCollection
  28. {
  29. use Helper\ReadPreference;
  30. use Helper\SlaveOkay;
  31. use Helper\WriteConcern;
  32. const ASCENDING = 1;
  33. const DESCENDING = -1;
  34. /**
  35. * @var MongoDB
  36. */
  37. public $db = null;
  38. /**
  39. * @var string
  40. */
  41. protected $name;
  42. /**
  43. * @var \MongoDB\Collection
  44. */
  45. protected $collection;
  46. /**
  47. * Creates a new collection
  48. *
  49. * @link http://www.php.net/manual/en/mongocollection.construct.php
  50. * @param MongoDB $db Parent database.
  51. * @param string $name Name for this collection.
  52. * @throws Exception
  53. */
  54. public function __construct(MongoDB $db, $name)
  55. {
  56. $this->checkCollectionName($name);
  57. $this->db = $db;
  58. $this->name = (string) $name;
  59. $this->setReadPreferenceFromArray($db->getReadPreference());
  60. $this->setWriteConcernFromArray($db->getWriteConcern());
  61. $this->createCollectionObject();
  62. }
  63. /**
  64. * Gets the underlying collection for this object
  65. *
  66. * @internal This part is not of the ext-mongo API and should not be used
  67. * @return \MongoDB\Collection
  68. */
  69. public function getCollection()
  70. {
  71. return $this->collection;
  72. }
  73. /**
  74. * String representation of this collection
  75. *
  76. * @link http://www.php.net/manual/en/mongocollection.--tostring.php
  77. * @return string Returns the full name of this collection.
  78. */
  79. public function __toString()
  80. {
  81. return (string) $this->db . '.' . $this->name;
  82. }
  83. /**
  84. * Gets a collection
  85. *
  86. * @link http://www.php.net/manual/en/mongocollection.get.php
  87. * @param string $name The next string in the collection name.
  88. * @return MongoCollection
  89. */
  90. public function __get($name)
  91. {
  92. // Handle w and wtimeout properties that replicate data stored in $readPreference
  93. if ($name === 'w' || $name === 'wtimeout') {
  94. return $this->getWriteConcern()[$name];
  95. }
  96. return $this->db->selectCollection($this->name . '.' . str_replace(chr(0), '', $name));
  97. }
  98. /**
  99. * @param string $name
  100. * @param mixed $value
  101. */
  102. public function __set($name, $value)
  103. {
  104. if ($name === 'w' || $name === 'wtimeout') {
  105. $this->setWriteConcernFromArray([$name => $value] + $this->getWriteConcern());
  106. $this->createCollectionObject();
  107. }
  108. }
  109. /**
  110. * Perform an aggregation using the aggregation framework
  111. *
  112. * @link http://www.php.net/manual/en/mongocollection.aggregate.php
  113. * @param array $pipeline
  114. * @param array $op
  115. * @return array
  116. */
  117. public function aggregate(array $pipeline, array $op = [])
  118. {
  119. if (! TypeConverter::isNumericArray($pipeline)) {
  120. $operators = func_get_args();
  121. $pipeline = [];
  122. $options = [];
  123. $i = 0;
  124. foreach ($operators as $operator) {
  125. $i++;
  126. if (! is_array($operator)) {
  127. trigger_error("Argument $i is not an array", E_USER_WARNING);
  128. return;
  129. }
  130. $pipeline[] = $operator;
  131. }
  132. } else {
  133. $options = $op;
  134. }
  135. if (isset($options['cursor'])) {
  136. $options['useCursor'] = true;
  137. if (isset($options['cursor']['batchSize'])) {
  138. $options['batchSize'] = $options['cursor']['batchSize'];
  139. }
  140. unset($options['cursor']);
  141. } else {
  142. $options['useCursor'] = false;
  143. }
  144. try {
  145. $cursor = $this->collection->aggregate(TypeConverter::fromLegacy($pipeline), $options);
  146. return [
  147. 'ok' => 1.0,
  148. 'result' => TypeConverter::toLegacy($cursor),
  149. 'waitedMS' => 0,
  150. ];
  151. } catch (\MongoDB\Driver\Exception\Exception $e) {
  152. throw ExceptionConverter::toLegacy($e, 'MongoResultException');
  153. }
  154. }
  155. /**
  156. * Execute an aggregation pipeline command and retrieve results through a cursor
  157. *
  158. * @link http://php.net/manual/en/mongocollection.aggregatecursor.php
  159. * @param array $pipeline
  160. * @param array $options
  161. * @return MongoCommandCursor
  162. */
  163. public function aggregateCursor(array $pipeline, array $options = [])
  164. {
  165. // Build command manually, can't use mongo-php-library here
  166. $command = [
  167. 'aggregate' => $this->name,
  168. 'pipeline' => $pipeline
  169. ];
  170. // Convert cursor option
  171. if (! isset($options['cursor'])) {
  172. $options['cursor'] = new \stdClass();
  173. }
  174. $command += $options;
  175. $cursor = new MongoCommandCursor($this->db->getConnection(), (string) $this, $command);
  176. $cursor->setReadPreference($this->getReadPreference());
  177. return $cursor;
  178. }
  179. /**
  180. * Returns this collection's name
  181. *
  182. * @link http://www.php.net/manual/en/mongocollection.getname.php
  183. * @return string
  184. */
  185. public function getName()
  186. {
  187. return $this->name;
  188. }
  189. /**
  190. * {@inheritdoc}
  191. */
  192. public function setReadPreference($readPreference, $tags = null)
  193. {
  194. $result = $this->setReadPreferenceFromParameters($readPreference, $tags);
  195. $this->createCollectionObject();
  196. return $result;
  197. }
  198. /**
  199. * {@inheritdoc}
  200. */
  201. public function setWriteConcern($wstring, $wtimeout = 0)
  202. {
  203. $result = $this->setWriteConcernFromParameters($wstring, $wtimeout);
  204. $this->createCollectionObject();
  205. return $result;
  206. }
  207. /**
  208. * Drops this collection
  209. *
  210. * @link http://www.php.net/manual/en/mongocollection.drop.php
  211. * @return array Returns the database response.
  212. */
  213. public function drop()
  214. {
  215. return TypeConverter::toLegacy($this->collection->drop());
  216. }
  217. /**
  218. * Validates this collection
  219. *
  220. * @link http://www.php.net/manual/en/mongocollection.validate.php
  221. * @param bool $scan_data Only validate indices, not the base collection.
  222. * @return array Returns the database's evaluation of this object.
  223. */
  224. public function validate($scan_data = false)
  225. {
  226. $command = [
  227. 'validate' => $this->name,
  228. 'full' => $scan_data,
  229. ];
  230. return $this->db->command($command);
  231. }
  232. /**
  233. * Inserts an array into the collection
  234. *
  235. * @link http://www.php.net/manual/en/mongocollection.insert.php
  236. * @param array|object $a
  237. * @param array $options
  238. * @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.
  239. * @throws MongoCursorException if the "w" option is set and the write fails.
  240. * @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.
  241. * @return bool|array Returns an array containing the status of the insertion if the "w" option is set.
  242. */
  243. public function insert(&$a, array $options = [])
  244. {
  245. if ($this->ensureDocumentHasMongoId($a) === null) {
  246. trigger_error(sprintf('%s(): expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
  247. return;
  248. }
  249. $this->mustBeArrayOrObject($a);
  250. try {
  251. $result = $this->collection->insertOne(
  252. TypeConverter::fromLegacy($a),
  253. $this->convertWriteConcernOptions($options)
  254. );
  255. } catch (\MongoDB\Driver\Exception\Exception $e) {
  256. throw ExceptionConverter::toLegacy($e);
  257. }
  258. if (! $result->isAcknowledged()) {
  259. return true;
  260. }
  261. return [
  262. 'ok' => 1.0,
  263. 'n' => 0,
  264. 'err' => null,
  265. 'errmsg' => null,
  266. ];
  267. }
  268. /**
  269. * Inserts multiple documents into this collection
  270. *
  271. * @link http://www.php.net/manual/en/mongocollection.batchinsert.php
  272. * @param array $a An array of arrays.
  273. * @param array $options Options for the inserts.
  274. * @throws MongoCursorException
  275. * @return mixed If "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.
  276. */
  277. public function batchInsert(array &$a, array $options = [])
  278. {
  279. if (empty($a)) {
  280. throw new \MongoException('No write ops were included in the batch');
  281. }
  282. $continueOnError = isset($options['continueOnError']) && $options['continueOnError'];
  283. foreach ($a as $key => $item) {
  284. try {
  285. if (! $this->ensureDocumentHasMongoId($a[$key])) {
  286. if ($continueOnError) {
  287. unset($a[$key]);
  288. } else {
  289. trigger_error(sprintf('%s expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
  290. return;
  291. }
  292. }
  293. } catch (MongoException $e) {
  294. if (! $continueOnError) {
  295. throw $e;
  296. }
  297. }
  298. }
  299. try {
  300. $result = $this->collection->insertMany(
  301. TypeConverter::fromLegacy(array_values($a)),
  302. $this->convertWriteConcernOptions($options)
  303. );
  304. } catch (\MongoDB\Driver\Exception\Exception $e) {
  305. throw ExceptionConverter::toLegacy($e, 'MongoResultException');
  306. }
  307. if (! $result->isAcknowledged()) {
  308. return true;
  309. }
  310. return [
  311. 'ok' => 1.0,
  312. 'connectionId' => 0,
  313. 'n' => 0,
  314. 'syncMillis' => 0,
  315. 'writtenTo' => null,
  316. 'err' => null,
  317. ];
  318. }
  319. /**
  320. * Update records based on a given criteria
  321. *
  322. * @link http://www.php.net/manual/en/mongocollection.update.php
  323. * @param array|object $criteria Description of the objects to update.
  324. * @param array|object $newobj The object with which to update the matching records.
  325. * @param array $options
  326. * @return bool|array
  327. * @throws MongoException
  328. * @throws MongoWriteConcernException
  329. */
  330. public function update($criteria, $newobj, array $options = [])
  331. {
  332. $this->mustBeArrayOrObject($criteria);
  333. $this->mustBeArrayOrObject($newobj);
  334. $this->checkKeys((array) $newobj);
  335. $multiple = isset($options['multiple']) ? $options['multiple'] : false;
  336. $isReplace = ! \MongoDB\is_first_key_operator($newobj);
  337. if ($isReplace && $multiple) {
  338. throw new \MongoWriteConcernException('multi update only works with $ operators', 9);
  339. }
  340. unset($options['multiple']);
  341. $method = $isReplace ? 'replace' : 'update';
  342. $method .= $multiple ? 'Many' : 'One';
  343. try {
  344. /** @var \MongoDB\UpdateResult $result */
  345. $result = $this->collection->$method(
  346. TypeConverter::fromLegacy($criteria),
  347. TypeConverter::fromLegacy($newobj),
  348. $this->convertWriteConcernOptions($options)
  349. );
  350. } catch (\MongoDB\Driver\Exception\Exception $e) {
  351. throw ExceptionConverter::toLegacy($e);
  352. }
  353. if (! $result->isAcknowledged()) {
  354. return true;
  355. }
  356. return [
  357. 'ok' => 1.0,
  358. 'nModified' => $result->getModifiedCount(),
  359. 'n' => $result->getMatchedCount(),
  360. 'err' => null,
  361. 'errmsg' => null,
  362. 'updatedExisting' => $result->getUpsertedCount() == 0 && $result->getModifiedCount() > 0,
  363. ];
  364. }
  365. /**
  366. * Remove records from this collection
  367. *
  368. * @link http://www.php.net/manual/en/mongocollection.remove.php
  369. * @param array $criteria Query criteria for the documents to delete.
  370. * @param array $options An array of options for the remove operation.
  371. * @throws MongoCursorException
  372. * @throws MongoCursorTimeoutException
  373. * @return bool|array Returns an array containing the status of the removal
  374. * if the "w" option is set. Otherwise, returns TRUE.
  375. */
  376. public function remove(array $criteria = [], array $options = [])
  377. {
  378. $multiple = isset($options['justOne']) ? !$options['justOne'] : true;
  379. $method = $multiple ? 'deleteMany' : 'deleteOne';
  380. try {
  381. /** @var \MongoDB\DeleteResult $result */
  382. $result = $this->collection->$method(
  383. TypeConverter::fromLegacy($criteria),
  384. $this->convertWriteConcernOptions($options)
  385. );
  386. } catch (\MongoDB\Driver\Exception\Exception $e) {
  387. throw ExceptionConverter::toLegacy($e);
  388. }
  389. if (! $result->isAcknowledged()) {
  390. return true;
  391. }
  392. return [
  393. 'ok' => 1.0,
  394. 'n' => $result->getDeletedCount(),
  395. 'err' => null,
  396. 'errmsg' => null
  397. ];
  398. }
  399. /**
  400. * Querys this collection
  401. *
  402. * @link http://www.php.net/manual/en/mongocollection.find.php
  403. * @param array $query The fields for which to search.
  404. * @param array $fields Fields of the results to return.
  405. * @return MongoCursor
  406. */
  407. public function find(array $query = [], array $fields = [])
  408. {
  409. $cursor = new MongoCursor($this->db->getConnection(), (string) $this, $query, $fields);
  410. $cursor->setReadPreference($this->getReadPreference());
  411. return $cursor;
  412. }
  413. /**
  414. * Retrieve a list of distinct values for the given key across a collection
  415. *
  416. * @link http://www.php.net/manual/ru/mongocollection.distinct.php
  417. * @param string $key The key to use.
  418. * @param array $query An optional query parameters
  419. * @return array|bool Returns an array of distinct values, or FALSE on failure
  420. */
  421. public function distinct($key, array $query = [])
  422. {
  423. try {
  424. return array_map([TypeConverter::class, 'toLegacy'], $this->collection->distinct($key, TypeConverter::fromLegacy($query)));
  425. } catch (\MongoDB\Driver\Exception\Exception $e) {
  426. return false;
  427. }
  428. }
  429. /**
  430. * Update a document and return it
  431. *
  432. * @link http://www.php.net/manual/ru/mongocollection.findandmodify.php
  433. * @param array $query The query criteria to search for.
  434. * @param array $update The update criteria.
  435. * @param array $fields Optionally only return these fields.
  436. * @param array $options An array of options to apply, such as remove the match document from the DB and return it.
  437. * @return array Returns the original document, or the modified document when new is set.
  438. */
  439. public function findAndModify(array $query, array $update = null, array $fields = null, array $options = [])
  440. {
  441. $query = TypeConverter::fromLegacy($query);
  442. try {
  443. if (isset($options['remove'])) {
  444. unset($options['remove']);
  445. $document = $this->collection->findOneAndDelete($query, $options);
  446. } else {
  447. $update = is_array($update) ? $update : [];
  448. if (isset($options['update']) && is_array($options['update'])) {
  449. $update = $options['update'];
  450. unset($options['update']);
  451. }
  452. $update = TypeConverter::fromLegacy($update);
  453. if (isset($options['new'])) {
  454. $options['returnDocument'] = \MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER;
  455. unset($options['new']);
  456. }
  457. $options['projection'] = TypeConverter::convertProjection($fields);
  458. if (! \MongoDB\is_first_key_operator($update)) {
  459. $document = $this->collection->findOneAndReplace($query, $update, $options);
  460. } else {
  461. $document = $this->collection->findOneAndUpdate($query, $update, $options);
  462. }
  463. }
  464. } catch (\MongoDB\Driver\Exception\ConnectionException $e) {
  465. throw new MongoResultException($e->getMessage(), $e->getCode(), $e);
  466. } catch (\MongoDB\Driver\Exception\Exception $e) {
  467. throw ExceptionConverter::toLegacy($e, 'MongoResultException');
  468. }
  469. if ($document) {
  470. $document = TypeConverter::toLegacy($document);
  471. }
  472. return $document;
  473. }
  474. /**
  475. * Querys this collection, returning a single element
  476. *
  477. * @link http://www.php.net/manual/en/mongocollection.findone.php
  478. * @param array $query The fields for which to search.
  479. * @param array $fields Fields of the results to return.
  480. * @param array $options
  481. * @return array|null
  482. */
  483. public function findOne($query = [], array $fields = [], array $options = [])
  484. {
  485. // Can't typehint for array since MongoGridFS extends and accepts strings
  486. if (! is_array($query)) {
  487. trigger_error(sprintf('MongoCollection::findOne(): expects parameter 1 to be an array or object, %s given', gettype($query)), E_USER_WARNING);
  488. return;
  489. }
  490. $options = ['projection' => TypeConverter::convertProjection($fields)] + $options;
  491. try {
  492. $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
  493. } catch (\MongoDB\Driver\Exception\Exception $e) {
  494. throw ExceptionConverter::toLegacy($e);
  495. }
  496. if ($document !== null) {
  497. $document = TypeConverter::toLegacy($document);
  498. }
  499. return $document;
  500. }
  501. /**
  502. * Creates an index on the given field(s), or does nothing if the index already exists
  503. *
  504. * @link http://www.php.net/manual/en/mongocollection.createindex.php
  505. * @param array $keys Field or fields to use as index.
  506. * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
  507. * @return array Returns the database response.
  508. */
  509. public function createIndex($keys, array $options = [])
  510. {
  511. if (is_string($keys)) {
  512. if (empty($keys)) {
  513. throw new MongoException('empty string passed as key field');
  514. }
  515. $keys = [$keys => 1];
  516. }
  517. if (is_object($keys)) {
  518. $keys = (array) $keys;
  519. }
  520. if (! is_array($keys) || ! count($keys)) {
  521. throw new MongoException('index specification has no elements');
  522. }
  523. if (! isset($options['name'])) {
  524. $options['name'] = $this->generateIndexName($keys);
  525. }
  526. $indexes = iterator_to_array($this->collection->listIndexes());
  527. $indexCount = count($indexes);
  528. $collectionExists = true;
  529. $indexExists = false;
  530. // listIndexes returns 0 for non-existing collections while the legacy driver returns 1
  531. if ($indexCount === 0) {
  532. $collectionExists = false;
  533. $indexCount = 1;
  534. }
  535. foreach ($indexes as $index) {
  536. if ($index->getKey() === $keys || $index->getName() === $options['name']) {
  537. $indexExists = true;
  538. break;
  539. }
  540. }
  541. try {
  542. foreach (['w', 'wTimeoutMS', 'safe', 'timeout', 'wtimeout'] as $invalidOption) {
  543. if (isset($options[$invalidOption])) {
  544. unset($options[$invalidOption]);
  545. }
  546. }
  547. $this->collection->createIndex($keys, $options);
  548. } catch (\MongoDB\Driver\Exception\Exception $e) {
  549. if (! $e instanceof CommandException || strpos($e->getMessage(), 'with a different name') === false) {
  550. throw ExceptionConverter::toLegacy($e, 'MongoResultException');
  551. }
  552. }
  553. $result = [
  554. 'createdCollectionAutomatically' => !$collectionExists,
  555. 'numIndexesBefore' => $indexCount,
  556. 'numIndexesAfter' => $indexCount,
  557. 'note' => 'all indexes already exist',
  558. 'ok' => 1.0,
  559. ];
  560. if (! $indexExists) {
  561. $result['numIndexesAfter']++;
  562. unset($result['note']);
  563. }
  564. return $result;
  565. }
  566. /**
  567. * Creates an index on the given field(s), or does nothing if the index already exists
  568. *
  569. * @link http://www.php.net/manual/en/mongocollection.ensureindex.php
  570. * @param array $keys Field or fields to use as index.
  571. * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
  572. * @return array Returns the database response.
  573. * @deprecated Use MongoCollection::createIndex() instead.
  574. */
  575. public function ensureIndex(array $keys, array $options = [])
  576. {
  577. return $this->createIndex($keys, $options);
  578. }
  579. /**
  580. * Deletes an index from this collection
  581. *
  582. * @link http://www.php.net/manual/en/mongocollection.deleteindex.php
  583. * @param string|array $keys Field or fields from which to delete the index.
  584. * @return array Returns the database response.
  585. */
  586. public function deleteIndex($keys)
  587. {
  588. if (is_string($keys)) {
  589. $indexName = $keys;
  590. if (! preg_match('#_-?1$#', $indexName)) {
  591. $indexName .= '_1';
  592. }
  593. } elseif (is_array($keys)) {
  594. $indexName = $this->generateIndexName($keys);
  595. } else {
  596. throw new \InvalidArgumentException();
  597. }
  598. try {
  599. return TypeConverter::toLegacy($this->collection->dropIndex($indexName));
  600. } catch (\MongoDB\Driver\Exception\Exception $e) {
  601. return ExceptionConverter::toResultArray($e) + ['nIndexesWas' => count($this->getIndexInfo())];
  602. }
  603. }
  604. /**
  605. * Delete all indexes for this collection
  606. *
  607. * @link http://www.php.net/manual/en/mongocollection.deleteindexes.php
  608. * @return array Returns the database response.
  609. */
  610. public function deleteIndexes()
  611. {
  612. try {
  613. return TypeConverter::toLegacy($this->collection->dropIndexes());
  614. } catch (\MongoDB\Driver\Exception\Exception $e) {
  615. return ExceptionConverter::toResultArray($e);
  616. }
  617. }
  618. /**
  619. * Returns an array of index names for this collection
  620. *
  621. * @link http://www.php.net/manual/en/mongocollection.getindexinfo.php
  622. * @return array Returns a list of index names.
  623. */
  624. public function getIndexInfo()
  625. {
  626. $convertIndex = function (\MongoDB\Model\IndexInfo $indexInfo) {
  627. $infos = [
  628. 'v' => $indexInfo->getVersion(),
  629. 'key' => $indexInfo->getKey(),
  630. 'name' => $indexInfo->getName(),
  631. 'ns' => $indexInfo->getNamespace(),
  632. ];
  633. $additionalKeys = [
  634. 'unique',
  635. 'sparse',
  636. 'partialFilterExpression',
  637. 'expireAfterSeconds',
  638. 'storageEngine',
  639. 'weights',
  640. 'default_language',
  641. 'language_override',
  642. 'textIndexVersion',
  643. 'collation',
  644. '2dsphereIndexVersion',
  645. 'bucketSize'
  646. ];
  647. foreach ($additionalKeys as $key) {
  648. if (! isset($indexInfo[$key])) {
  649. continue;
  650. }
  651. $infos[$key] = $indexInfo[$key];
  652. }
  653. return $infos;
  654. };
  655. return array_map($convertIndex, iterator_to_array($this->collection->listIndexes()));
  656. }
  657. /**
  658. * Counts the number of documents in this collection
  659. *
  660. * @link http://www.php.net/manual/en/mongocollection.count.php
  661. * @param array|stdClass $query
  662. * @param array $options
  663. * @return int Returns the number of documents matching the query.
  664. */
  665. public function count($query = [], $options = [])
  666. {
  667. try {
  668. // Handle legacy mode - limit and skip as second and third parameters, respectively
  669. if (! is_array($options)) {
  670. $limit = $options;
  671. $options = [];
  672. if ($limit !== null) {
  673. $options['limit'] = (int) $limit;
  674. }
  675. if (func_num_args() > 2) {
  676. $options['skip'] = (int) func_get_args()[2];
  677. }
  678. }
  679. return $this->collection->count(TypeConverter::fromLegacy($query), $options);
  680. } catch (\MongoDB\Driver\Exception\Exception $e) {
  681. throw ExceptionConverter::toLegacy($e);
  682. }
  683. }
  684. /**
  685. * Saves an object to this collection
  686. *
  687. * @link http://www.php.net/manual/en/mongocollection.save.php
  688. * @param array|object $a Array to save. If an object is used, it may not have protected or private properties.
  689. * @param array $options Options for the save.
  690. * @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.
  691. * @throws MongoCursorException if the "w" option is set and the write fails.
  692. * @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.
  693. * @return array|boolean If w was set, returns an array containing the status of the save.
  694. * Otherwise, returns a boolean representing if the array was not empty (an empty array will not be inserted).
  695. */
  696. public function save(&$a, array $options = [])
  697. {
  698. $id = $this->ensureDocumentHasMongoId($a);
  699. $document = (array) $a;
  700. $options['upsert'] = true;
  701. try {
  702. /** @var \MongoDB\UpdateResult $result */
  703. $result = $this->collection->replaceOne(
  704. TypeConverter::fromLegacy(['_id' => $id]),
  705. TypeConverter::fromLegacy($document),
  706. $this->convertWriteConcernOptions($options)
  707. );
  708. if (! $result->isAcknowledged()) {
  709. return true;
  710. }
  711. $resultArray = [
  712. 'ok' => 1.0,
  713. 'nModified' => $result->getModifiedCount(),
  714. 'n' => $result->getUpsertedCount() + $result->getModifiedCount(),
  715. 'err' => null,
  716. 'errmsg' => null,
  717. 'updatedExisting' => $result->getUpsertedCount() == 0 && $result->getModifiedCount() > 0,
  718. ];
  719. if ($result->getUpsertedId() !== null) {
  720. $resultArray['upserted'] = TypeConverter::toLegacy($result->getUpsertedId());
  721. }
  722. return $resultArray;
  723. } catch (\MongoDB\Driver\Exception\Exception $e) {
  724. throw ExceptionConverter::toLegacy($e);
  725. }
  726. }
  727. /**
  728. * Creates a database reference
  729. *
  730. * @link http://www.php.net/manual/en/mongocollection.createdbref.php
  731. * @param array|object $document_or_id Object to which to create a reference.
  732. * @return array Returns a database reference array.
  733. */
  734. public function createDBRef($document_or_id)
  735. {
  736. if ($document_or_id instanceof \MongoId) {
  737. $id = $document_or_id;
  738. } elseif (is_object($document_or_id)) {
  739. if (! isset($document_or_id->_id)) {
  740. return null;
  741. }
  742. $id = $document_or_id->_id;
  743. } elseif (is_array($document_or_id)) {
  744. if (! isset($document_or_id['_id'])) {
  745. return null;
  746. }
  747. $id = $document_or_id['_id'];
  748. } else {
  749. $id = $document_or_id;
  750. }
  751. return MongoDBRef::create($this->name, $id);
  752. }
  753. /**
  754. * Fetches the document pointed to by a database reference
  755. *
  756. * @link http://www.php.net/manual/en/mongocollection.getdbref.php
  757. * @param array $ref A database reference.
  758. * @return array Returns the database document pointed to by the reference.
  759. */
  760. public function getDBRef(array $ref)
  761. {
  762. return $this->db->getDBRef($ref);
  763. }
  764. /**
  765. * Performs an operation similar to SQL's GROUP BY command
  766. *
  767. * @link http://www.php.net/manual/en/mongocollection.group.php
  768. * @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.
  769. * @param array $initial Initial value of the aggregation counter object.
  770. * @param MongoCode|string $reduce A function that aggregates (reduces) the objects iterated.
  771. * @param array $condition An condition that must be true for a row to be considered.
  772. * @return array
  773. */
  774. public function group($keys, array $initial, $reduce, array $condition = [])
  775. {
  776. if (is_string($reduce)) {
  777. $reduce = new MongoCode($reduce);
  778. }
  779. $command = [
  780. 'group' => [
  781. 'ns' => $this->name,
  782. '$reduce' => (string) $reduce,
  783. 'initial' => $initial,
  784. 'cond' => $condition,
  785. ],
  786. ];
  787. if ($keys instanceof MongoCode) {
  788. $command['group']['$keyf'] = (string) $keys;
  789. } else {
  790. $command['group']['key'] = $keys;
  791. }
  792. if (array_key_exists('condition', $condition)) {
  793. $command['group']['cond'] = $condition['condition'];
  794. }
  795. if (array_key_exists('finalize', $condition)) {
  796. if ($condition['finalize'] instanceof MongoCode) {
  797. $condition['finalize'] = (string) $condition['finalize'];
  798. }
  799. $command['group']['finalize'] = $condition['finalize'];
  800. }
  801. return $this->db->command($command);
  802. }
  803. /**
  804. * Returns an array of cursors to iterator over a full collection in parallel
  805. *
  806. * @link http://www.php.net/manual/en/mongocollection.parallelcollectionscan.php
  807. * @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.
  808. * @return MongoCommandCursor[]
  809. */
  810. public function parallelCollectionScan($num_cursors)
  811. {
  812. $this->notImplemented();
  813. }
  814. protected function notImplemented()
  815. {
  816. throw new \Exception('Not implemented');
  817. }
  818. /**
  819. * @return \MongoDB\Collection
  820. */
  821. private function createCollectionObject()
  822. {
  823. $options = [
  824. 'readPreference' => $this->readPreference,
  825. 'writeConcern' => $this->writeConcern,
  826. ];
  827. if ($this->collection === null) {
  828. $this->collection = $this->db->getDb()->selectCollection($this->name, $options);
  829. } else {
  830. $this->collection = $this->collection->withOptions($options);
  831. }
  832. }
  833. /**
  834. * Converts legacy write concern options to a WriteConcern object
  835. *
  836. * @param array $options
  837. * @return array
  838. */
  839. private function convertWriteConcernOptions(array $options)
  840. {
  841. if (isset($options['safe'])) {
  842. $options['w'] = ($options['safe']) ? 1 : 0;
  843. }
  844. if (isset($options['wtimeout']) && !isset($options['wTimeoutMS'])) {
  845. $options['wTimeoutMS'] = $options['wtimeout'];
  846. }
  847. if (isset($options['w']) || !isset($options['wTimeoutMS'])) {
  848. $collectionWriteConcern = $this->getWriteConcern();
  849. $writeConcern = $this->createWriteConcernFromParameters(
  850. isset($options['w']) ? $options['w'] : $collectionWriteConcern['w'],
  851. isset($options['wTimeoutMS']) ? $options['wTimeoutMS'] : $collectionWriteConcern['wtimeout']
  852. );
  853. $options['writeConcern'] = $writeConcern;
  854. }
  855. unset($options['safe']);
  856. unset($options['w']);
  857. unset($options['wTimeout']);
  858. unset($options['wTimeoutMS']);
  859. return $options;
  860. }
  861. private function checkKeys(array $array)
  862. {
  863. foreach ($array as $key => $value) {
  864. if (empty($key) && $key !== 0 && $key !== '0') {
  865. throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
  866. }
  867. if (is_object($value) || is_array($value)) {
  868. $this->checkKeys((array) $value);
  869. }
  870. }
  871. }
  872. /**
  873. * @param array|object $document
  874. * @return MongoId
  875. */
  876. private function ensureDocumentHasMongoId(&$document)
  877. {
  878. if (is_array($document) || $document instanceof ArrayObject) {
  879. if (! isset($document['_id'])) {
  880. $document['_id'] = new \MongoId();
  881. }
  882. $this->checkKeys((array) $document);
  883. return $document['_id'];
  884. } elseif (is_object($document)) {
  885. $reflectionObject = new \ReflectionObject($document);
  886. foreach ($reflectionObject->getProperties() as $property) {
  887. if (! $property->isPublic()) {
  888. throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
  889. }
  890. }
  891. if (! isset($document->_id)) {
  892. $document->_id = new \MongoId();
  893. }
  894. $this->checkKeys((array) $document);
  895. return $document->_id;
  896. }
  897. return null;
  898. }
  899. private function checkCollectionName($name)
  900. {
  901. if (empty($name)) {
  902. throw new Exception('Collection name cannot be empty');
  903. } elseif (strpos($name, chr(0)) !== false) {
  904. throw new Exception('Collection name cannot contain null bytes');
  905. }
  906. }
  907. /**
  908. * @param array $keys Field or fields to use as index.
  909. * @return string
  910. */
  911. private function generateIndexName($keys)
  912. {
  913. $indexInput = new IndexInput(['key' => $keys]);
  914. return (string) $indexInput;
  915. }
  916. /**
  917. * @return array
  918. */
  919. public function __sleep()
  920. {
  921. return ['db', 'name'];
  922. }
  923. private function mustBeArrayOrObject($a)
  924. {
  925. if (!is_array($a) && !is_object($a)) {
  926. throw new \MongoException('document must be an array or object');
  927. }
  928. }
  929. }