EMongoClient.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. <?php
  2. /**
  3. * EMongoClient
  4. *
  5. * The MongoDB and MongoClient class combined.
  6. *
  7. * Quite deceptively the magic functions of this class actually represent the DATABASE not the connection.
  8. * This is in contrast to MongoClient whos' own represent the SERVER.
  9. *
  10. * Normally this would represent the MongoClient or Mongo and it is even named after them and implements
  11. * some of their functions but it is not due to the way Yii works.
  12. */
  13. class EMongoClient extends CApplicationComponent
  14. {
  15. /**
  16. * The server string (connection string pre-1.3)
  17. * @var string
  18. */
  19. public $server;
  20. /**
  21. * Additional options for the connection constructor
  22. * @var array
  23. */
  24. public $options = array();
  25. /**
  26. * The name of the database
  27. * @var string
  28. */
  29. public $db;
  30. /**
  31. * Write Concern, will default to acked writes
  32. * @see http://php.net/manual/en/mongo.writeconcerns.php
  33. * @var int string
  34. */
  35. public $w = 1;
  36. /**
  37. * Are we using journaled writes here? Beware this makes all writes wait for the journal, it does not
  38. * state whether MongoDB is using journaling.
  39. * Note: this is NOT straight to disk,
  40. * it infact makes the journal to disk time a third of its normal time (anywhere between 2-30ms).
  41. * Only works 1.3+ driver
  42. * @var boolean
  43. */
  44. public $j = false;
  45. /**
  46. * The read preference
  47. * The first param is the textual version of the constant name in the MongoClient for the type of read
  48. * i.e.
  49. * RP_PRIMARY and the second emulates the setReadPreference prototypes second parameter
  50. * @see http://php.net/manual/en/mongo.readpreferences.php
  51. * @var array()
  52. */
  53. public $RP = array('RP_PRIMARY', array());
  54. /**
  55. * The Legacy read preference.
  56. * DO NOT USE IF YOU ARE ON VERSION 1.3+
  57. * @var boolean
  58. */
  59. public $setSlaveOkay = false;
  60. /**
  61. * Allows one to connect when they want to when turned to false
  62. * Note that if you try and access MongoDB before it is connected it
  63. * will attempt to connect
  64. * @var boolean
  65. */
  66. public $autoConnect = true;
  67. /**
  68. * Enables logging to the profiler
  69. * @var boolean
  70. */
  71. public $enableProfiling = false;
  72. /**
  73. * @var integer number of seconds that query results can remain valid in cache.
  74. * Use 0 or negative value to indicate not caching query results (the default behavior).
  75. *
  76. * In order to enable query caching, this property must be a positive
  77. * integer and {@link queryCacheID} must point to a valid cache component ID.
  78. *
  79. * The method {@link cache()} is provided as a convenient way of setting this property
  80. * and {@link queryCachingDependency} on the fly.
  81. *
  82. * @see cache
  83. * @see queryCachingDependency
  84. * @see queryCacheID
  85. */
  86. public $queryCachingDuration = 0;
  87. /**
  88. * @var CCacheDependency|ICacheDependency the dependency that will be used when saving query results into cache.
  89. * @see queryCachingDuration
  90. */
  91. public $queryCachingDependency;
  92. /**
  93. * @var integer the number of SQL statements that need to be cached next.
  94. * If this is 0, then even if query caching is enabled, no query will be cached.
  95. * Note that each time after executing a SQL statement (whether executed on DB server or fetched from
  96. * query cache), this property will be reduced by 1 until 0.
  97. */
  98. public $queryCachingCount = 0;
  99. /**
  100. * @var string the ID of the cache application component that is used for query caching.
  101. * Defaults to 'cache' which refers to the primary cache application component.
  102. * Set this property to false if you want to disable query caching.
  103. */
  104. public $queryCacheID = 'cache';
  105. /**
  106. * The Mongo Connection instance
  107. * @var Mongo MongoClient
  108. */
  109. private $_mongo;
  110. /**
  111. * The database instance
  112. * @var MongoDB
  113. */
  114. private $_db;
  115. /**
  116. * Caches reflection properties for our objects so we don't have
  117. * to keep getting them
  118. * @var array
  119. */
  120. private $_meta = array();
  121. /**
  122. * The default action is to find a getX whereby X is the $k param
  123. * you input.
  124. * The secondary function, if not getter found, is to get a collection
  125. */
  126. public function __get($k)
  127. {
  128. $getter = 'get' . $k;
  129. if(method_exists($this, $getter)){
  130. return $this->$getter();
  131. }
  132. return $this->selectCollection($k);
  133. }
  134. /**
  135. * Will call a function on the database or error out stating that the function does not exist
  136. * @param string $name
  137. * @param array $parameters
  138. * @return mixed
  139. */
  140. public function __call($name, $parameters = array())
  141. {
  142. if(!method_exists($this->getDB(), $name)){
  143. return parent::__call($name, $parameters);
  144. }
  145. return call_user_func_array(array($this->getDB(), $name), $parameters);
  146. }
  147. public function __construct()
  148. {
  149. // We copy this function to add the subdocument validator as a built in validator
  150. CValidator::$builtInValidators['subdocument'] = 'ESubdocumentValidator';
  151. }
  152. /**
  153. * The init function
  154. * We also connect here
  155. * @see yii/framework/base/CApplicationComponent::init()
  156. */
  157. public function init()
  158. {
  159. parent::init();
  160. if($this->db){
  161. $this->options['db'] = $this->db;
  162. }
  163. if($this->autoConnect){
  164. $this->connect();
  165. }
  166. }
  167. /**
  168. * Connects to our database
  169. */
  170. public function connect()
  171. {
  172. if(!extension_loaded('mongo')){
  173. throw new EMongoException(
  174. yii::t(
  175. 'yii',
  176. 'We could not find the MongoDB extension ( http://php.net/manual/en/mongo.installation.php ), please install it'
  177. )
  178. );
  179. }
  180. // We don't need to throw useless exceptions here, the MongoDB PHP Driver has its own checks and error reporting
  181. // Yii will easily and effortlessly display the errors from the PHP driver, we should only catch its exceptions if
  182. // we wanna add our own custom messages on top which we don't, the errors are quite self explanatory
  183. if(version_compare(phpversion('mongo'), '1.3.0', '<')){
  184. $this->_mongo = new Mongo($this->server, $this->options);
  185. $this->_mongo->connect();
  186. if($this->setSlaveOkay){
  187. $this->_mongo->setSlaveOkay($this->setSlaveOkay);
  188. }
  189. }else{
  190. $this->_mongo = new MongoClient($this->server, $this->options);
  191. if(is_array($this->RP)){
  192. $const = $this->RP[0];
  193. $opts = $this->RP[1];
  194. if(!empty($opts)){
  195. // I do this due to a bug that exists in some PHP driver versions
  196. $this->_mongo->setReadPreference(constant('MongoClient::' . $const), $opts);
  197. }else{
  198. $this->_mongo->setReadPreference(constant('MongoClient::' . $const));
  199. }
  200. }
  201. }
  202. }
  203. /**
  204. * Gets the connection object
  205. * Use this to access the Mongo/MongoClient instance within the extension
  206. * @return Mongo MongoClient
  207. */
  208. public function getConnection()
  209. {
  210. if(empty($this->_mongo)){
  211. $this->connect();
  212. }
  213. return $this->_mongo;
  214. }
  215. /**
  216. * Sets the raw database adhoc
  217. * @param string $name
  218. */
  219. public function setDB($name)
  220. {
  221. $this->_db = $this->getConnection ()->selectDb ( $name );
  222. }
  223. /**
  224. * Selects a different database
  225. * @param $name
  226. * @return MongoDB
  227. */
  228. public function selectDB($name)
  229. {
  230. $this->setDB($name);
  231. return $this->getDB();
  232. }
  233. /**
  234. * Gets the raw Database
  235. * @return MongoDB
  236. */
  237. public function getDB()
  238. {
  239. if(empty($this->_db)){
  240. $this->setDB ( $this->db );
  241. }
  242. return $this->_db;
  243. }
  244. /**
  245. * You should never call this function.
  246. * The PHP driver will handle connections automatically, and will
  247. * keep this performant for you.
  248. */
  249. public function close()
  250. {
  251. if(!empty($this->_mongo)){
  252. $this->_mongo->close ();
  253. return true;
  254. }
  255. return false;
  256. }
  257. /**
  258. * This function is designed to be a helper to make calling the aggregate command
  259. * more standard across all drivers.
  260. *
  261. * @param string $collection
  262. * @param
  263. * $pipelines
  264. * @return array
  265. */
  266. public function aggregate($collection, $pipelines)
  267. {
  268. if(version_compare(phpversion('mongo'), '1.3.0', '<')){
  269. return $this->getDB()->command(array('aggregate' => $collection, 'pipeline' => $pipelines));
  270. }
  271. return $this->getDB()->$collection->aggregate($pipelines);
  272. }
  273. /**
  274. * Command helper
  275. * @param array|string $command
  276. * @return array
  277. */
  278. public function command($command = array())
  279. {
  280. return $this->getDB()->command($command);
  281. }
  282. /**
  283. * A wrapper for the original processing
  284. * @param string $name
  285. * @return MongoCollection
  286. */
  287. public function selectCollection($name)
  288. {
  289. return $this->getDB()->selectCollection($name);
  290. }
  291. /**
  292. * Sets the document cache for any particular document (EMongoDocument/EMongoModel)
  293. * sent in as the first parameter of this function.
  294. * Will not cache actual EMongoDocument/EMongoModel instances
  295. * only active classes that inherit these
  296. * @param $o
  297. */
  298. public function setDocumentCache($o)
  299. {
  300. if(
  301. $this->getDocumentCache(get_class($o)) === array() && // Run reflection and cache it if not already there
  302. (get_class($o) != 'EMongoDocument' && get_class($o) != 'EMongoModel') /* We can't cache the model */
  303. ){
  304. $_meta = array();
  305. $reflect = new ReflectionClass(get_class($o));
  306. $class_vars = $reflect->getProperties(ReflectionProperty::IS_PUBLIC); // Pre-defined doc attributes
  307. foreach($class_vars as $prop){
  308. if($prop->isStatic()){
  309. continue;
  310. }
  311. $docBlock = $prop->getDocComment();
  312. $field_meta = array(
  313. 'name' => $prop->getName(),
  314. 'virtual' => $prop->isProtected() || preg_match('/@virtual/i', $docBlock) <= 0 ? false : true
  315. );
  316. // Lets fetch the data type for this field
  317. // Since we always fetch the data type for this field we make a regex that will only pick out the first
  318. if(preg_match('/@var ([a-zA-Z]+)/', $docBlock, $matches) > 0){
  319. $field_meta['type'] = $matches[1];
  320. }
  321. $_meta[$prop->getName()] = $field_meta;
  322. }
  323. $this->_meta[get_class($o)] = $_meta;
  324. }
  325. }
  326. /**
  327. * Get a list of the fields (attributes) for a document from cache
  328. * @param string $name
  329. * @param boolean $include_virtual
  330. * @return array
  331. */
  332. public function getFieldCache($name, $include_virtual = false)
  333. {
  334. $doc = isset($this->_meta[$name]) ? $this->_meta[$name] : array();
  335. $fields = array();
  336. foreach($doc as $name => $opts){
  337. if($include_virtual || !$opts['virtual']){
  338. $fields[] = $name;
  339. }
  340. }
  341. return $fields;
  342. }
  343. /**
  344. * Just gets the document cache for a model
  345. * @param string $name
  346. * @return array
  347. */
  348. public function getDocumentCache($name)
  349. {
  350. return isset($this->_meta[$name]) ? $this->_meta[$name] : array();
  351. }
  352. /**
  353. * Gets the default write concern options for all queries through active record
  354. * @return array
  355. */
  356. public function getDefaultWriteConcern()
  357. {
  358. if(!version_compare(phpversion('mongo'), '1.3.0', '<')){
  359. return array('w' => $this->w, 'j' => $this->j);
  360. }elseif($this->w == 1){
  361. return array('safe' => true);
  362. }elseif($this->w > 0){
  363. return array('safe' => $this->w);
  364. }
  365. return array ();
  366. }
  367. /**
  368. * Create ObjectId from timestamp.
  369. * This function is not actively used it is
  370. * here as a helper for anyone who needs it
  371. * @param int $yourTimestamp
  372. * @return MongoID
  373. */
  374. public function createMongoIdFromTimestamp($yourTimestamp)
  375. {
  376. static $inc = 0;
  377. $ts = pack('N', $yourTimestamp);
  378. $m = substr(md5(gethostname ()), 0, 3);
  379. $pid = pack('n', getmypid());
  380. $trail = substr(pack('N', $inc ++), 1, 3);
  381. $bin = sprintf("%s%s%s%s", $ts, $m, $pid, $trail);
  382. $id = '';
  383. for($i = 0; $i < 12; $i ++){
  384. $id .= sprintf("%02X", ord($bin[$i]));
  385. }
  386. return new MongoID($id);
  387. }
  388. /**
  389. * Set read preference on MongoClient
  390. * @param string $pref
  391. * @param array $options
  392. * @return bool
  393. */
  394. public function setReadPreference($pref, $options = array())
  395. {
  396. return $this->getConnection()->setReadPreference($pref, $options);
  397. }
  398. /**
  399. * setSlaveOkay on Mongo
  400. * @param bool $bool
  401. * @return bool
  402. */
  403. public function setSlaveOkay($bool)
  404. {
  405. return $this->getConnection()->setSlaveOkay($bool);
  406. }
  407. /**
  408. * Sets the parameters for query caching.
  409. * This method can be used to enable or disable query caching.
  410. * By setting the $duration parameter to be 0, the query caching will be disabled.
  411. * Otherwise, query results of the new SQL statements executed next will be saved in cache
  412. * and remain valid for the specified duration.
  413. * If the same query is executed again, the result may be fetched from cache directly
  414. * without actually executing the SQL statement.
  415. *
  416. * @param integer $duration
  417. * the number of seconds that query results may remain valid in cache.
  418. * If this is 0, the caching will be disabled.
  419. * @param CCacheDependency|ICacheDependency $dependency
  420. * the dependency that will be used when saving
  421. * the query results into cache.
  422. * @param integer $queryCount
  423. * number of SQL queries that need to be cached after calling this method. Defaults to 1,
  424. * meaning that the next SQL query will be cached.
  425. * @return static the connection instance itself.
  426. * @since 1.1.7
  427. */
  428. public function cache($duration, $dependency = null, $queryCount = 1)
  429. {
  430. $this->queryCachingDuration = $duration;
  431. $this->queryCachingDependency = $dependency;
  432. $this->queryCachingCount = $queryCount;
  433. return $this;
  434. }
  435. public function getSerialisedQuery($criteria = array(), $fields = array(), $sort = array(), $skip = 0, $limit = null)
  436. {
  437. $query = array(
  438. '$query' => $criteria,
  439. '$fields' => $fields,
  440. '$sort' => $sort,
  441. '$skip' => $skip,
  442. '$limit' => $limit
  443. );
  444. return json_encode($query);
  445. }
  446. /**
  447. *
  448. * @return array the first element indicates the number of query statements executed,
  449. * and the second element the total time spent in query execution.
  450. */
  451. public function getStats()
  452. {
  453. $logger = Yii::getLogger();
  454. $timings = $logger->getProfilingResults(null, 'extensions.MongoYii.EMongoDocument.findOne');
  455. $count = count($timings);
  456. $time = array_sum($timings);
  457. $timings = $logger->getProfilingResults(null, 'extensions.MongoYii.EMongoDocument.insert');
  458. $count += count($timings);
  459. $time += array_sum($timings);
  460. $timings = $logger->getProfilingResults(null, 'extensions.MongoYii.EMongoDocument.find');
  461. $count += count($timings);
  462. $time += array_sum($timings);
  463. $timings = $logger->getProfilingResults(null, 'extensions.MongoYii.EMongoDocument.deleteByPk');
  464. $count += count($timings);
  465. $time += array_sum($timings);
  466. $timings = $logger->getProfilingResults(null, 'extensions.MongoYii.EMongoDocument.updateByPk');
  467. $count += count($timings);
  468. $time += array_sum($timings);
  469. $timings = $logger->getProfilingResults(null, 'extensions.MongoYii.EMongoDocument.updateAll');
  470. $count += count($timings);
  471. $time += array_sum($timings);
  472. $timings = $logger->getProfilingResults(null, 'extensions.MongoYii.EMongoDocument.deleteAll');
  473. $count += count($timings);
  474. $time += array_sum($timings);
  475. return array($count, $time);
  476. }
  477. }