CouchDb.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\data\source\http\adapter;
  9. use lithium\core\ConfigException;
  10. /**
  11. * A data source adapter which allows you to connect to Apache CouchDB.
  12. *
  13. * By default, it will attempt to connect to the CouchDB running on `localhost` on port
  14. * 5984 using HTTP version 1.0.
  15. *
  16. * @link http://couchdb.apache.org
  17. */
  18. class CouchDb extends \lithium\data\source\Http {
  19. /**
  20. * Increment value of current result set loop
  21. * used by `result` to handle rows of json responses.
  22. *
  23. * @var string
  24. */
  25. protected $_iterator = 0;
  26. /**
  27. * True if Database exists.
  28. *
  29. * @var boolean
  30. */
  31. protected $_db = false;
  32. /**
  33. * Classes used by `CouchDb`.
  34. *
  35. * @var array
  36. */
  37. protected $_classes = array(
  38. 'service' => 'lithium\net\http\Service',
  39. 'entity' => 'lithium\data\entity\Document',
  40. 'set' => 'lithium\data\collection\DocumentSet',
  41. 'schema' => 'lithium\data\DocumentSchema'
  42. );
  43. protected $_handlers = array();
  44. /**
  45. * Constructor.
  46. * @param array $config
  47. */
  48. public function __construct(array $config = array()) {
  49. $defaults = array('port' => 5984, 'version' => 1, 'database' => null);
  50. parent::__construct($config + $defaults);
  51. }
  52. protected function _init() {
  53. parent::_init();
  54. $this->_handlers += array(
  55. 'integer' => function($v) { return (integer) $v; },
  56. 'float' => function($v) { return (float) $v; },
  57. 'boolean' => function($v) { return (boolean) $v; }
  58. );
  59. }
  60. /**
  61. * Ensures that the server connection is closed and resources are freed when the adapter
  62. * instance is destroyed.
  63. *
  64. * @return void
  65. */
  66. public function __destruct() {
  67. if (!$this->_isConnected) {
  68. return;
  69. }
  70. $this->disconnect();
  71. $this->_db = false;
  72. unset($this->connection);
  73. }
  74. /**
  75. * Configures a model class by setting the primary key to `'id'`, in keeping with CouchDb
  76. * conventions.
  77. *
  78. * @see lithium\data\Model::$_meta
  79. * @see lithium\data\Model::$_classes
  80. * @param string $class The fully-namespaced model class name to be configured.
  81. * @return Returns an array containing keys `'classes'` and `'meta'`, which will be merged with
  82. * their respective properties in `Model`.
  83. */
  84. public function configureClass($class) {
  85. return array(
  86. 'meta' => array('key' => 'id', 'locked' => false),
  87. 'schema' => array(
  88. 'id' => array('type' => 'string'),
  89. 'rev' => array('type' => 'string')
  90. )
  91. );
  92. }
  93. /**
  94. * Magic for passing methods to http service.
  95. *
  96. * @param string $method
  97. * @param array $params
  98. * @return void
  99. */
  100. public function __call($method, $params = array()) {
  101. list($path, $data, $options) = ($params + array('/', array(), array()));
  102. return json_decode($this->connection->{$method}($path, $data, $options));
  103. }
  104. /**
  105. * Custom check to determine if our given magic methods can be responded to.
  106. *
  107. * @param string $method Method name.
  108. * @param bool $internal Interal call or not.
  109. * @return bool
  110. */
  111. public function respondsTo($method, $internal = false) {
  112. $parentRespondsTo = parent::respondsTo($method, $internal);
  113. return $parentRespondsTo || is_callable(array($this->connection, $method));
  114. }
  115. /**
  116. * Returns an array of object types accessible through this database.
  117. *
  118. * @param object $class
  119. * @return void
  120. */
  121. public function sources($class = null) {
  122. }
  123. /**
  124. * Describe database, create if it does not exist.
  125. *
  126. * @throws ConfigException
  127. * @param string $entity
  128. * @param array $schema Any schema data pre-defined by the model.
  129. * @param array $meta
  130. * @return void
  131. */
  132. public function describe($entity, $schema = array(), array $meta = array()) {
  133. $database = $this->_config['database'];
  134. if (!$this->_db) {
  135. $result = $this->get($database);
  136. if (isset($result->db_name)) {
  137. $this->_db = true;
  138. }
  139. if (!$this->_db) {
  140. if (isset($result->error)) {
  141. if ($result->error === 'not_found') {
  142. $result = $this->put($database);
  143. }
  144. }
  145. if (isset($result->ok) || isset($result->db_name)) {
  146. $this->_db = true;
  147. }
  148. }
  149. }
  150. if (!$this->_db) {
  151. throw new ConfigException("Database `{$entity}` is not available.");
  152. }
  153. return $this->_instance('schema', array(array('fields' => $schema)));
  154. }
  155. /**
  156. * Quotes identifiers.
  157. *
  158. * CouchDb does not need identifiers quoted, so this method simply returns the identifier.
  159. *
  160. * @param string $name The identifier to quote.
  161. * @return string The quoted identifier.
  162. */
  163. public function name($name) {
  164. return $name;
  165. }
  166. /**
  167. * Create new document.
  168. *
  169. * @param string $query
  170. * @param array $options
  171. * @return boolean
  172. * @filter
  173. */
  174. public function create($query, array $options = array()) {
  175. $defaults = array('model' => $query->model());
  176. $options += $defaults;
  177. $params = compact('query', 'options');
  178. $conn =& $this->connection;
  179. $config = $this->_config;
  180. return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, $config) {
  181. $request = array('type' => 'json');
  182. $query = $params['query'];
  183. $options = $params['options'];
  184. $data = $query->data();
  185. $data += array('type' => $options['model']::meta('source'));
  186. if (isset($data['id'])) {
  187. return $self->update($query, $options);
  188. }
  189. $result = $conn->post($config['database'], $data, $request);
  190. $result = is_string($result) ? json_decode($result, true) : $result;
  191. if (isset($result['_id']) || (isset($result['ok']) && $result['ok'] === true)) {
  192. $result = $self->invokeMethod('_format', array($result, $options));
  193. $query->entity()->sync($result['id'], $result);
  194. return true;
  195. }
  196. return false;
  197. });
  198. }
  199. /**
  200. * Read from document.
  201. *
  202. * @param string $query
  203. * @param array $options
  204. * @return object
  205. * @filter
  206. */
  207. public function read($query, array $options = array()) {
  208. $defaults = array('return' => 'resource', 'model' => $query->model());
  209. $options += $defaults;
  210. $params = compact('query', 'options');
  211. $conn =& $this->connection;
  212. $config = $this->_config;
  213. return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, $config) {
  214. $query = $params['query'];
  215. $options = $params['options'];
  216. $params = $query->export($self);
  217. extract($params, EXTR_OVERWRITE);
  218. list($_path, $conditions) = (array) $conditions;
  219. $model = $query->model();
  220. if (empty($_path)) {
  221. $_path = '_all_docs';
  222. $conditions['include_docs'] = 'true';
  223. }
  224. $path = "{$config['database']}/{$_path}";
  225. $args = (array) $conditions + (array) $limit + (array) $order;
  226. $result = $conn->get($path, $args);
  227. $result = is_string($result) ? json_decode($result, true) : $result;
  228. $data = $stats = array();
  229. if (isset($result['_id'])) {
  230. $data = array($result);
  231. } elseif (isset($result['rows'])) {
  232. $data = $result['rows'];
  233. unset($result['rows']);
  234. $stats = $result;
  235. }
  236. foreach ($data as $key => $val) {
  237. $data[$key] = $self->item($model, $val, array('exists' => true));
  238. }
  239. $stats += array('total_rows' => null, 'offset' => null);
  240. $opts = compact('stats') + array('class' => 'set', 'exists' => true);
  241. return $self->item($query->model(), $data, $opts);
  242. });
  243. }
  244. /**
  245. * Update document.
  246. *
  247. * @param string $query
  248. * @param array $options
  249. * @return boolean
  250. * @filter
  251. */
  252. public function update($query, array $options = array()) {
  253. $params = compact('query', 'options');
  254. $conn =& $this->connection;
  255. $config = $this->_config;
  256. return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, $config) {
  257. $query = $params['query'];
  258. $options = $params['options'];
  259. $params = $query->export($self);
  260. extract($params, EXTR_OVERWRITE);
  261. list($_path, $conditions) = (array) $conditions;
  262. $data = $query->data();
  263. foreach (array('id', 'rev') as $key) {
  264. $data["_{$key}"] = isset($data[$key]) ? (string) $data[$key] : null;
  265. unset($data[$key]);
  266. }
  267. $data = (array) $conditions + array_filter((array) $data);
  268. $result = $conn->put("{$config['database']}/{$_path}", $data, array('type' => 'json'));
  269. $result = is_string($result) ? json_decode($result, true) : $result;
  270. if (isset($result['_id']) || (isset($result['ok']) && $result['ok'] === true)) {
  271. $result = $self->invokeMethod('_format', array($result, $options));
  272. $query->entity()->sync($result['id'], array('rev' => $result['rev']));
  273. return true;
  274. }
  275. if (isset($result['error'])) {
  276. $query->entity()->errors(array($result['error']));
  277. }
  278. return false;
  279. });
  280. }
  281. /**
  282. * Delete document.
  283. *
  284. * @param string $query
  285. * @param array $options
  286. * @return boolean
  287. * @filter
  288. */
  289. public function delete($query, array $options = array()) {
  290. $params = compact('query', 'options');
  291. $conn =& $this->connection;
  292. $config = $this->_config;
  293. return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, $config) {
  294. $query = $params['query'];
  295. $params = $query->export($self);
  296. list($_path, $conditions) = $params['conditions'];
  297. $data = $query->data();
  298. if (!empty($data['rev'])) {
  299. $conditions['rev'] = $data['rev'];
  300. }
  301. $result = json_decode($conn->delete("{$config['database']}/{$_path}", $conditions));
  302. $result = (isset($result->ok) && $result->ok === true);
  303. if ($query->entity()) {
  304. $query->entity()->sync(null, array(), array('dematerialize' => true));
  305. }
  306. return $result;
  307. });
  308. }
  309. /**
  310. * Executes calculation-related queries, such as those required for `count`.
  311. *
  312. * @param string $type Only accepts `count`.
  313. * @param mixed $query The query to be executed.
  314. * @param array $options Optional arguments for the `read()` query that will be executed
  315. * to obtain the calculation result.
  316. * @return integer Result of the calculation.
  317. */
  318. public function calculation($type, $query, array $options = array()) {
  319. switch ($type) {
  320. case 'count':
  321. return $this->read($query, $options)->stats('total_rows');
  322. default:
  323. return null;
  324. }
  325. }
  326. /**
  327. * Returns a newly-created `Document` object, bound to a model and populated with default data
  328. * and options.
  329. *
  330. * @param string $model A fully-namespaced class name representing the model class to which the
  331. * `Document` object will be bound.
  332. * @param array $data The default data with which the new `Document` should be populated.
  333. * @param array $options Any additional options to pass to the `Document`'s constructor
  334. * @return object Returns a new, un-saved `Document` object bound to the model class specified
  335. * in `$model`.
  336. */
  337. public function item($model, array $data = array(), array $options = array()) {
  338. if (isset($data['doc'])) {
  339. return parent::item($model, $this->_format($data['doc']), $options);
  340. }
  341. if (isset($data['value'])) {
  342. $data = $data['value'];
  343. }
  344. return parent::item($model, $this->_format($data), $options);
  345. }
  346. /**
  347. * Casts data into proper format when added to a collection or entity object.
  348. *
  349. * @param mixed $entity The entity or collection for which data is being cast, or the name of
  350. * the model class to which the entity/collection is bound.
  351. * @param array $data An array of data being assigned.
  352. * @param array $options Any associated options with, for example, instantiating new objects in
  353. * which to wrap the data. Options implemented by `cast()` itself:
  354. * - `first` _boolean_: Used when only one value is passed to `cast()`. Even though
  355. * that value must be wrapped in an array, setting the `'first'` option to `true`
  356. * causes only that one value to be returned.
  357. * @return mixed Returns the value of `$data`, cast to the proper format according to the schema
  358. * definition of the model class specified by `$model`.
  359. */
  360. public function cast($entity, array $data, array $options = array()) {
  361. $defaults = array('pathKey' => null, 'model' => null);
  362. $options += $defaults;
  363. $model = $options['model'] ?: $entity->model();
  364. foreach ($data as $key => $val) {
  365. if (!is_array($val)) {
  366. continue;
  367. }
  368. $pathKey = $options['pathKey'] ? "{$options['pathKey']}.{$key}" : $key;
  369. $class = (range(0, count($val) - 1) === array_keys($val)) ? 'set' : 'entity';
  370. $data[$key] = $this->item($model, $val, compact('class', 'pathKey') + $options);
  371. }
  372. return parent::cast($entity, $data, $options);
  373. }
  374. /**
  375. * Handle conditions.
  376. *
  377. * @param string $conditions
  378. * @param string $context
  379. * @return array
  380. */
  381. public function conditions($conditions, $context) {
  382. $path = null;
  383. if (isset($conditions['design'])) {
  384. $paths = array('design', 'view');
  385. foreach ($paths as $element) {
  386. if (isset($conditions[$element])) {
  387. $path .= "_{$element}/{$conditions[$element]}/";
  388. unset($conditions[$element]);
  389. }
  390. }
  391. }
  392. if (isset($conditions['id'])) {
  393. $path = "{$conditions['id']}";
  394. unset($conditions['id']);
  395. }
  396. if (isset($conditions['path'])) {
  397. $path = "{$conditions['path']}";
  398. unset($conditions['path']);
  399. }
  400. return array($path, $conditions);
  401. }
  402. /**
  403. * Fields for query.
  404. *
  405. * @param string $fields
  406. * @param string $context
  407. * @return array
  408. */
  409. public function fields($fields, $context) {
  410. return $fields ?: array();
  411. }
  412. /**
  413. * Limit for query.
  414. *
  415. * @param string $limit
  416. * @param string $context
  417. * @return array
  418. */
  419. public function limit($limit, $context) {
  420. return compact('limit') ?: array();
  421. }
  422. /**
  423. * Order for query.
  424. *
  425. * @param string $order
  426. * @param string $context
  427. * @return array
  428. */
  429. public function order($order, $context) {
  430. return (array) $order ?: array();
  431. }
  432. /**
  433. * With no parameter, always returns `true`, since CouchDB only depends on HTTP. With a
  434. * parameter, queries for a specific supported feature.
  435. *
  436. * @param string $feature Test for support for a specific feature, i.e. `"transactions"` or
  437. * `"arrays"`.
  438. * @return boolean Returns `true` if the particular feature support is enabled, otherwise
  439. * `false`.
  440. */
  441. public static function enabled($feature = null) {
  442. if (!$feature) {
  443. return true;
  444. }
  445. $features = array(
  446. 'arrays' => true,
  447. 'transactions' => false,
  448. 'booleans' => true,
  449. 'relationships' => false
  450. );
  451. return isset($features[$feature]) ? $features[$feature] : null;
  452. }
  453. /**
  454. * Formats a CouchDb result set into a standard result to be passed to item.
  455. *
  456. * @param array $data data returned from query
  457. * @return array
  458. */
  459. protected function _format(array $data) {
  460. foreach (array("id", "rev") as $key) {
  461. $data[$key] = isset($data["_{$key}"]) ? $data["_{$key}"] : null;
  462. unset($data["_{$key}"]);
  463. }
  464. return $data;
  465. }
  466. }
  467. ?>