ActiveRecord.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\mongodb;
  8. use MongoDB\BSON\Binary;
  9. use MongoDB\BSON\Type;
  10. use Yii;
  11. use yii\base\InvalidConfigException;
  12. use yii\db\BaseActiveRecord;
  13. use yii\db\StaleObjectException;
  14. use yii\helpers\ArrayHelper;
  15. use yii\helpers\Inflector;
  16. use yii\helpers\StringHelper;
  17. /**
  18. * ActiveRecord is the base class for classes representing Mongo documents in terms of objects.
  19. *
  20. * @author Paul Klimov <klimov.paul@gmail.com>
  21. * @since 2.0
  22. */
  23. abstract class ActiveRecord extends BaseActiveRecord
  24. {
  25. /**
  26. * Returns the Mongo connection used by this AR class.
  27. * By default, the "mongodb" application component is used as the Mongo connection.
  28. * You may override this method if you want to use a different database connection.
  29. * @return Connection the database connection used by this AR class.
  30. */
  31. public static function getDb()
  32. {
  33. return Yii::$app->get('mongodb');
  34. }
  35. /**
  36. * Updates all documents in the collection using the provided attribute values and conditions.
  37. * For example, to change the status to be 1 for all customers whose status is 2:
  38. *
  39. * ```php
  40. * Customer::updateAll(['status' => 1], ['status' => 2]);
  41. * ```
  42. *
  43. * @param array $attributes attribute values (name-value pairs) to be saved into the collection
  44. * @param array $condition description of the objects to update.
  45. * Please refer to [[Query::where()]] on how to specify this parameter.
  46. * @param array $options list of options in format: optionName => optionValue.
  47. * @return int the number of documents updated.
  48. */
  49. public static function updateAll($attributes, $condition = [], $options = [])
  50. {
  51. return static::getCollection()->update($condition, $attributes, $options);
  52. }
  53. /**
  54. * Updates all documents in the collection using the provided counter changes and conditions.
  55. * For example, to increment all customers' age by 1,
  56. *
  57. * ```php
  58. * Customer::updateAllCounters(['age' => 1]);
  59. * ```
  60. *
  61. * @param array $counters the counters to be updated (attribute name => increment value).
  62. * Use negative values if you want to decrement the counters.
  63. * @param array $condition description of the objects to update.
  64. * Please refer to [[Query::where()]] on how to specify this parameter.
  65. * @param array $options list of options in format: optionName => optionValue.
  66. * @return int the number of documents updated.
  67. */
  68. public static function updateAllCounters($counters, $condition = [], $options = [])
  69. {
  70. return static::getCollection()->update($condition, ['$inc' => $counters], $options);
  71. }
  72. /**
  73. * Deletes documents in the collection using the provided conditions.
  74. * WARNING: If you do not specify any condition, this method will delete documents rows in the collection.
  75. *
  76. * For example, to delete all customers whose status is 3:
  77. *
  78. * ```php
  79. * Customer::deleteAll(['status' => 3]);
  80. * ```
  81. *
  82. * @param array $condition description of the objects to delete.
  83. * Please refer to [[Query::where()]] on how to specify this parameter.
  84. * @param array $options list of options in format: optionName => optionValue.
  85. * @return int the number of documents deleted.
  86. */
  87. public static function deleteAll($condition = [], $options = [])
  88. {
  89. return static::getCollection()->remove($condition, $options);
  90. }
  91. /**
  92. * {@inheritdoc}
  93. * @return ActiveQuery the newly created [[ActiveQuery]] instance.
  94. */
  95. public static function find()
  96. {
  97. return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
  98. }
  99. /**
  100. * Declares the name of the Mongo collection associated with this AR class.
  101. *
  102. * Collection name can be either a string or array:
  103. * - if string considered as the name of the collection inside the default database.
  104. * - if array - first element considered as the name of the database, second - as
  105. * name of collection inside that database
  106. *
  107. * By default this method returns the class name as the collection name by calling [[Inflector::camel2id()]].
  108. * For example, 'Customer' becomes 'customer', and 'OrderItem' becomes
  109. * 'order_item'. You may override this method if the collection is not named after this convention.
  110. * @return string|array the collection name
  111. */
  112. public static function collectionName()
  113. {
  114. return Inflector::camel2id(StringHelper::basename(get_called_class()), '_');
  115. }
  116. /**
  117. * Return the Mongo collection instance for this AR class.
  118. * @return Collection collection instance.
  119. */
  120. public static function getCollection()
  121. {
  122. return static::getDb()->getCollection(static::collectionName());
  123. }
  124. /**
  125. * Returns the primary key name(s) for this AR class.
  126. * The default implementation will return ['_id'].
  127. *
  128. * Note that an array should be returned even for a collection with single primary key.
  129. *
  130. * @return string[] the primary keys of the associated Mongo collection.
  131. */
  132. public static function primaryKey()
  133. {
  134. return ['_id'];
  135. }
  136. /**
  137. * Returns the list of all attribute names of the model.
  138. * This method must be overridden by child classes to define available attributes.
  139. * Note: primary key attribute "_id" should be always present in returned array.
  140. * For example:
  141. *
  142. * ```php
  143. * public function attributes()
  144. * {
  145. * return ['_id', 'name', 'address', 'status'];
  146. * }
  147. * ```
  148. *
  149. * @throws \yii\base\InvalidConfigException if not implemented
  150. * @return array list of attribute names.
  151. */
  152. public function attributes()
  153. {
  154. throw new InvalidConfigException('The attributes() method of mongodb ActiveRecord has to be implemented by child classes.');
  155. }
  156. /**
  157. * Inserts a row into the associated Mongo collection using the attribute values of this record.
  158. *
  159. * This method performs the following steps in order:
  160. *
  161. * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation
  162. * fails, it will skip the rest of the steps;
  163. * 2. call [[afterValidate()]] when `$runValidation` is true.
  164. * 3. call [[beforeSave()]]. If the method returns false, it will skip the
  165. * rest of the steps;
  166. * 4. insert the record into collection. If this fails, it will skip the rest of the steps;
  167. * 5. call [[afterSave()]];
  168. *
  169. * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
  170. * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
  171. * will be raised by the corresponding methods.
  172. *
  173. * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database.
  174. *
  175. * If the primary key is null during insertion, it will be populated with the actual
  176. * value after insertion.
  177. *
  178. * For example, to insert a customer record:
  179. *
  180. * ```php
  181. * $customer = new Customer();
  182. * $customer->name = $name;
  183. * $customer->email = $email;
  184. * $customer->insert();
  185. * ```
  186. *
  187. * @param bool $runValidation whether to perform validation before saving the record.
  188. * If the validation fails, the record will not be inserted into the collection.
  189. * @param array $attributes list of attributes that need to be saved. Defaults to null,
  190. * meaning all attributes that are loaded will be saved.
  191. * @return bool whether the attributes are valid and the record is inserted successfully.
  192. * @throws \Exception in case insert failed.
  193. */
  194. public function insert($runValidation = true, $attributes = null)
  195. {
  196. if ($runValidation && !$this->validate($attributes)) {
  197. return false;
  198. }
  199. $result = $this->insertInternal($attributes);
  200. return $result;
  201. }
  202. /**
  203. * @see ActiveRecord::insert()
  204. */
  205. protected function insertInternal($attributes = null)
  206. {
  207. if (!$this->beforeSave(true)) {
  208. return false;
  209. }
  210. $values = $this->getDirtyAttributes($attributes);
  211. if (empty($values)) {
  212. $currentAttributes = $this->getAttributes();
  213. foreach ($this->primaryKey() as $key) {
  214. if (isset($currentAttributes[$key])) {
  215. $values[$key] = $currentAttributes[$key];
  216. }
  217. }
  218. }
  219. $newId = static::getCollection()->insert($values);
  220. if ($newId !== null) {
  221. $this->setAttribute('_id', $newId);
  222. $values['_id'] = $newId;
  223. }
  224. $changedAttributes = array_fill_keys(array_keys($values), null);
  225. $this->setOldAttributes($values);
  226. $this->afterSave(true, $changedAttributes);
  227. return true;
  228. }
  229. /**
  230. * @see ActiveRecord::update()
  231. * @throws StaleObjectException
  232. */
  233. protected function updateInternal($attributes = null)
  234. {
  235. if (!$this->beforeSave(false)) {
  236. return false;
  237. }
  238. $values = $this->getDirtyAttributes($attributes);
  239. if (empty($values)) {
  240. $this->afterSave(false, $values);
  241. return 0;
  242. }
  243. $condition = $this->getOldPrimaryKey(true);
  244. $lock = $this->optimisticLock();
  245. if ($lock !== null) {
  246. if (!isset($values[$lock])) {
  247. $values[$lock] = $this->$lock + 1;
  248. }
  249. $condition[$lock] = $this->$lock;
  250. }
  251. // We do not check the return value of update() because it's possible
  252. // that it doesn't change anything and thus returns 0.
  253. $rows = static::getCollection()->update($condition, $values);
  254. if ($lock !== null && !$rows) {
  255. throw new StaleObjectException('The object being updated is outdated.');
  256. }
  257. if (isset($values[$lock])) {
  258. $this->$lock = $values[$lock];
  259. }
  260. $changedAttributes = [];
  261. foreach ($values as $name => $value) {
  262. $changedAttributes[$name] = $this->getOldAttribute($name);
  263. $this->setOldAttribute($name, $value);
  264. }
  265. $this->afterSave(false, $changedAttributes);
  266. return $rows;
  267. }
  268. /**
  269. * Deletes the document corresponding to this active record from the collection.
  270. *
  271. * This method performs the following steps in order:
  272. *
  273. * 1. call [[beforeDelete()]]. If the method returns false, it will skip the
  274. * rest of the steps;
  275. * 2. delete the document from the collection;
  276. * 3. call [[afterDelete()]].
  277. *
  278. * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
  279. * will be raised by the corresponding methods.
  280. *
  281. * @return int|bool the number of documents deleted, or false if the deletion is unsuccessful for some reason.
  282. * Note that it is possible the number of documents deleted is 0, even though the deletion execution is successful.
  283. * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
  284. * being deleted is outdated.
  285. * @throws \Exception in case delete failed.
  286. */
  287. public function delete()
  288. {
  289. $result = false;
  290. if ($this->beforeDelete()) {
  291. $result = $this->deleteInternal();
  292. $this->afterDelete();
  293. }
  294. return $result;
  295. }
  296. /**
  297. * @see ActiveRecord::delete()
  298. * @throws StaleObjectException
  299. */
  300. protected function deleteInternal()
  301. {
  302. // we do not check the return value of deleteAll() because it's possible
  303. // the record is already deleted in the database and thus the method will return 0
  304. $condition = $this->getOldPrimaryKey(true);
  305. $lock = $this->optimisticLock();
  306. if ($lock !== null) {
  307. $condition[$lock] = $this->$lock;
  308. }
  309. $result = static::getCollection()->remove($condition);
  310. if ($lock !== null && !$result) {
  311. throw new StaleObjectException('The object being deleted is outdated.');
  312. }
  313. $this->setOldAttributes(null);
  314. return $result;
  315. }
  316. /**
  317. * Returns a value indicating whether the given active record is the same as the current one.
  318. * The comparison is made by comparing the collection names and the primary key values of the two active records.
  319. * If one of the records [[isNewRecord|is new]] they are also considered not equal.
  320. * @param ActiveRecord $record record to compare to
  321. * @return bool whether the two active records refer to the same row in the same Mongo collection.
  322. */
  323. public function equals($record)
  324. {
  325. if ($this->isNewRecord || $record->isNewRecord) {
  326. return false;
  327. }
  328. return $this->collectionName() === $record->collectionName() && (string) $this->getPrimaryKey() === (string) $record->getPrimaryKey();
  329. }
  330. /**
  331. * {@inheritdoc}
  332. */
  333. public function toArray(array $fields = [], array $expand = [], $recursive = true)
  334. {
  335. $data = parent::toArray($fields, $expand, false);
  336. if (!$recursive) {
  337. return $data;
  338. }
  339. return $this->toArrayInternal($data);
  340. }
  341. /**
  342. * Converts data to array recursively, converting MongoDB BSON objects to readable values.
  343. * @param mixed $data the data to be converted into an array.
  344. * @return array the array representation of the data.
  345. * @since 2.1
  346. */
  347. private function toArrayInternal($data)
  348. {
  349. if (is_array($data)) {
  350. foreach ($data as $key => $value) {
  351. if (is_array($value)) {
  352. $data[$key] = $this->toArrayInternal($value);
  353. }
  354. if (is_object($value)) {
  355. if ($value instanceof Type) {
  356. $data[$key] = $this->dumpBsonObject($value);
  357. } else {
  358. $data[$key] = ArrayHelper::toArray($value);
  359. }
  360. }
  361. }
  362. return $data;
  363. } elseif (is_object($data)) {
  364. return ArrayHelper::toArray($data);
  365. }
  366. return [$data];
  367. }
  368. /**
  369. * Converts MongoDB BSON object to readable value.
  370. * @param Type $object MongoDB BSON object.
  371. * @return array|string object dump value.
  372. * @since 2.1
  373. */
  374. private function dumpBsonObject(Type $object)
  375. {
  376. if ($object instanceof Binary) {
  377. return $object->getData();
  378. }
  379. if (method_exists($object, '__toString')) {
  380. return $object->__toString();
  381. }
  382. return ArrayHelper::toArray($object);
  383. }
  384. }