ArraySource.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. <?php
  2. /**
  3. * Array Datasource
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * For full copyright and license information, please see the LICENSE.txt
  12. * Redistributions of files must retain the above copyright notice.
  13. *
  14. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  15. * @link http://cakephp.org CakePHP(tm) Project
  16. * @since CakePHP Datasources v 0.3
  17. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  18. */
  19. App::uses('Hash', 'Utility');
  20. App::uses('ConnectionManager', 'Model');
  21. /**
  22. * Array Datasource
  23. *
  24. * Datasource for array based models
  25. */
  26. class ArraySource extends DataSource {
  27. /**
  28. * Description string for this Data Source.
  29. *
  30. * @var string
  31. */
  32. public $description = 'Array Datasource';
  33. /**
  34. * List of requests ("queries")
  35. *
  36. * @var array
  37. */
  38. protected $_requestsLog = array();
  39. /**
  40. * Base Config
  41. *
  42. * @var array
  43. */
  44. protected $_baseConfig = array(
  45. 'driver' => '' // Just to avoid DebugKit warning
  46. );
  47. /**
  48. * Start quote
  49. *
  50. * @var string
  51. */
  52. public $startQuote = null;
  53. /**
  54. * End quote
  55. *
  56. * @var string
  57. */
  58. public $endQuote = null;
  59. /**
  60. * Imitation of DboSource method.
  61. *
  62. * @param mixed $data Either a string with a column to quote. An array of columns
  63. * to quote.
  64. * @return string SQL field
  65. */
  66. public function name($data) {
  67. if (is_object($data) && isset($data->type)) {
  68. return $data->value;
  69. }
  70. if ($data === '*') {
  71. return '*';
  72. }
  73. if (is_array($data)) {
  74. foreach ($data as $i => $dataItem) {
  75. $data[$i] = $this->name($dataItem);
  76. }
  77. return $data;
  78. }
  79. return (string)$data;
  80. }
  81. /**
  82. * Returns a Model description (metadata) or null if none found.
  83. *
  84. * @param Model $model
  85. * @return array Show only id
  86. */
  87. public function describe($model) {
  88. return array('id' => array());
  89. }
  90. /**
  91. * List sources
  92. *
  93. * @param mixed $data
  94. * @return boolean Always false. It's not supported
  95. */
  96. public function listSources($data = null) {
  97. return false;
  98. }
  99. /**
  100. * Used to read records from the Datasource. The "R" in CRUD
  101. *
  102. * @param Model $model The model being read.
  103. * @param array $queryData An array of query data used to find the data you want
  104. * @param null $recursive
  105. * @return mixed
  106. */
  107. public function read(Model $model, $queryData = array(), $recursive = null) {
  108. if (!isset($model->records) || !is_array($model->records) || empty($model->records)) {
  109. $this->_requestsLog[] = array(
  110. 'query' => 'Model ' . $model->alias,
  111. 'error' => __('No records found in model.'),
  112. 'affected' => 0,
  113. 'numRows' => 0,
  114. 'took' => 0
  115. );
  116. return array($model->alias => array());
  117. }
  118. $startTime = microtime(true);
  119. $data = array();
  120. $i = 0;
  121. $limit = false;
  122. if ($recursive === null && isset($queryData['recursive'])) {
  123. $recursive = $queryData['recursive'];
  124. }
  125. if ($recursive !== null) {
  126. $_recursive = $model->recursive;
  127. $model->recursive = $recursive;
  128. }
  129. if (is_int($queryData['limit']) && $queryData['limit'] > 0) {
  130. $limit = $queryData['page'] * $queryData['limit'];
  131. }
  132. foreach ($model->records as $pos => $record) {
  133. // Tests whether the record will be chosen
  134. if (!empty($queryData['conditions'])) {
  135. $queryData['conditions'] = (array)$queryData['conditions'];
  136. if (!$this->conditionsFilter($model, $record, $queryData['conditions'])) {
  137. continue;
  138. }
  139. }
  140. $data[$i][$model->alias] = $record;
  141. $i++;
  142. // Test limit
  143. if ($limit !== false && $i == $limit && empty($queryData['order'])) {
  144. break;
  145. }
  146. }
  147. if ($queryData['fields'] === 'COUNT') {
  148. $this->_registerLog($model, $queryData, microtime(true) - $startTime, 1);
  149. if ($limit !== false) {
  150. $data = array_slice($data, ($queryData['page'] - 1) * $queryData['limit'], $queryData['limit'], false);
  151. }
  152. return array(array(array('count' => count($data))));
  153. }
  154. // Order
  155. if (!empty($queryData['order'])) {
  156. if (is_string($queryData['order'][0])) {
  157. $field = $queryData['order'][0];
  158. $alias = $model->alias;
  159. if (strpos($field, '.') !== false) {
  160. list($alias, $field) = explode('.', $field, 2);
  161. }
  162. if ($alias === $model->alias) {
  163. $sort = 'ASC';
  164. if (strpos($field, ' ') !== false) {
  165. list($field, $sort) = explode(' ', $field, 2);
  166. }
  167. if ($data) {
  168. $data = Hash::sort($data, '{n}.' . $model->alias . '.' . $field, $sort);
  169. }
  170. }
  171. }
  172. }
  173. // Limit
  174. if ($limit !== false) {
  175. $data = array_slice($data, ($queryData['page'] - 1) * $queryData['limit'], $queryData['limit'], false);
  176. }
  177. // Filter fields
  178. if (!empty($queryData['fields'])) {
  179. $listOfFields = array();
  180. foreach ((array)$queryData['fields'] as $field) {
  181. if (strpos($field, '.') !== false) {
  182. list($alias, $field) = explode('.', $field, 2);
  183. if ($alias !== $model->alias) {
  184. continue;
  185. }
  186. }
  187. $listOfFields[] = $field;
  188. }
  189. foreach ($data as $id => $record) {
  190. foreach ($record[$model->alias] as $field => $value) {
  191. if (!in_array($field, $listOfFields)) {
  192. unset($data[$id][$model->alias][$field]);
  193. }
  194. }
  195. }
  196. }
  197. $this->_registerLog($model, $queryData, microtime(true) - $startTime, count($data));
  198. $associations = $model->_associations;
  199. if ($model->recursive > -1) {
  200. foreach ($associations as $type) {
  201. foreach ($model->{$type} as $assoc => $assocData) {
  202. $linkModel = $model->{$assoc};
  203. if ($model->useDbConfig == $linkModel->useDbConfig) {
  204. $db = $this;
  205. } else {
  206. $db = ConnectionManager::getDataSource($linkModel->useDbConfig);
  207. }
  208. if (isset($db)) {
  209. if (method_exists($db, 'queryAssociation')) {
  210. $stack = array($assoc);
  211. $db->queryAssociation($model, $linkModel, $type, $assoc, $assocData, $queryData, true, $data, $model->recursive - 1, $stack);
  212. }
  213. unset($db);
  214. }
  215. }
  216. }
  217. }
  218. if ($recursive !== null) {
  219. $model->recursive = $_recursive;
  220. }
  221. return $data;
  222. }
  223. /**
  224. * Conditions Filter
  225. *
  226. * @param Model $model
  227. * @param string $record
  228. * @param array $conditions
  229. * @param boolean $or
  230. * @return boolean
  231. */
  232. public function conditionsFilter(Model $model, $record, $conditions, $or = false) {
  233. foreach ($conditions as $field => $value) {
  234. $return = null;
  235. if ($value === '') {
  236. continue;
  237. }
  238. if (is_array($value) && in_array(strtoupper($field), array('AND', 'NOT', 'OR'))) {
  239. switch (strtoupper($field)) {
  240. case 'AND':
  241. $return = $this->conditionsFilter($model, $record, $value);
  242. break;
  243. case 'NOT':
  244. $return = !$this->conditionsFilter($model, $record, $value);
  245. break;
  246. case 'OR':
  247. $return = $this->conditionsFilter($model, $record, $value, true);
  248. break;
  249. }
  250. } else {
  251. if (is_array($value)) {
  252. $type = 'IN';
  253. } elseif (preg_match('/^(\w+\.?\w+)\s+(=|!=|LIKE|IN|<|<=|>|>=)\s*$/i', $field, $matches)) {
  254. $field = $matches[1];
  255. $type = strtoupper($matches[2]);
  256. } elseif (preg_match('/^(\w+\.?\w+)\s+(=|!=|LIKE|IN|<|<=|>|>=)\s+(.*)$/i', $value, $matches)) {
  257. $field = $matches[1];
  258. $type = strtoupper($matches[2]);
  259. $value = $matches[3];
  260. } else {
  261. $type = '=';
  262. }
  263. if (strpos($field, '.') !== false) {
  264. list($alias, $field) = explode('.', $field, 2);
  265. if ($alias != $model->alias) {
  266. continue;
  267. }
  268. }
  269. switch ($type) {
  270. case '<':
  271. $return = (array_key_exists($field, $record) && $record[$field] < $value);
  272. break;
  273. case '<=':
  274. $return = (array_key_exists($field, $record) && $record[$field] <= $value);
  275. break;
  276. case '=':
  277. $return = (array_key_exists($field, $record) && $record[$field] == $value);
  278. break;
  279. case '>':
  280. $return = (array_key_exists($field, $record) && $record[$field] > $value);
  281. break;
  282. case '>=':
  283. $return = (array_key_exists($field, $record) && $record[$field] >= $value);
  284. break;
  285. case '!=':
  286. $return = (!array_key_exists($field, $record) || $record[$field] != $value);
  287. break;
  288. case 'LIKE':
  289. $value = preg_replace(array('#(^|[^\\\\])_#', '#(^|[^\\\\])%#'), array('$1.', '$1.*'), $value);
  290. $return = (isset($record[$field]) && preg_match('#^' . $value . '$#i', $record[$field]));
  291. break;
  292. case 'IN':
  293. $items = array();
  294. if (is_array($value)) {
  295. $items = $value;
  296. } elseif (preg_match('/^\(\w+(,\s*\w+)*\)$/', $value)) {
  297. $items = explode(',', trim($value, '()'));
  298. $items = array_map('trim', $items);
  299. }
  300. $return = (array_key_exists($field, $record) && in_array($record[$field], (array)$items));
  301. break;
  302. }
  303. }
  304. if ($return === $or) {
  305. return $or;
  306. }
  307. }
  308. return !$or;
  309. }
  310. /**
  311. * Returns an calculation
  312. *
  313. * @param model $model
  314. * @param string $type Lowercase name type, i.e. 'count' or 'max'
  315. * @param array $params Function parameters (any values must be quoted manually)
  316. * @return string Calculation method
  317. */
  318. public function calculate(Model $model, $type, $params = array()) {
  319. return 'COUNT';
  320. }
  321. /**
  322. * Implemented to make the datasource work with Model::find('count').
  323. *
  324. * @return boolean Always false;
  325. */
  326. public function expression() {
  327. return false;
  328. }
  329. /**
  330. * Queries associations. Used to fetch results on recursive models.
  331. *
  332. * @param Model $model Primary Model object
  333. * @param Model $linkModel Linked model that
  334. * @param string $type Association type, one of the model association types ie. hasMany
  335. * @param string $association The name of the association
  336. * @param array $assocData The data about the association
  337. * @param array $queryData
  338. * @param boolean $external Whether or not the association query is on an external datasource.
  339. * @param array $resultSet Existing results
  340. * @param integer $recursive Number of levels of association
  341. * @param array $stack
  342. */
  343. public function queryAssociation(Model $model, Model $linkModel, $type, $association, $assocData, &$queryData, $external, &$resultSet, $recursive, $stack) {
  344. $assocData = array_merge(array('conditions' => null, 'fields' => null, 'order' => null), $assocData);
  345. if (isset($queryData['fields'])) {
  346. $assocData['fields'] = array_filter(array_merge((array)$queryData['fields'], (array)$assocData['fields']));
  347. }
  348. if (isset($queryData['conditions'])) {
  349. $assocData['conditions'] = array_filter(array_merge((array)$queryData['conditions'], (array)$assocData['conditions']));
  350. }
  351. $query = array(
  352. 'fields' => array_filter((array)$assocData['fields']),
  353. 'conditions' => array_filter((array)$assocData['conditions']),
  354. 'group' => null,
  355. 'order' => $assocData['order'],
  356. 'limit' => isset($assocData['limit']) ? $assocData['limit'] : null,
  357. 'page' => 1,
  358. 'offset' => null,
  359. 'callbacks' => true,
  360. 'recursive' => $recursive === 0 ? -1 : $recursive
  361. );
  362. foreach ($resultSet as &$record) {
  363. $data = array();
  364. if ($type === 'belongsTo') {
  365. if (isset($record[$model->alias][$assocData['foreignKey']])) {
  366. $conditions = array_merge($query['conditions'], array($linkModel->alias . '.' . $linkModel->primaryKey => $record[$model->alias][$assocData['foreignKey']]));
  367. $limit = 1;
  368. $data = $this->read($linkModel, compact('conditions', 'limit') + $query);
  369. }
  370. } elseif (($type === 'hasMany' || $type === 'hasOne') && $model->recursive > 0) {
  371. $conditions = array_merge($query['conditions'], array($linkModel->alias . '.' . $assocData['foreignKey'] => $record[$model->alias][$model->primaryKey]));
  372. $limit = $type === 'hasOne' ? 1 : $query['limit'];
  373. $data = $this->read($linkModel, compact('conditions', 'limit') + $query);
  374. } elseif ($type === 'hasAndBelongsToMany' && $model->recursive > 0) {
  375. $joinModel = ClassRegistry::init($assocData['with']);
  376. $fields = array($joinModel->alias . '.' . $assocData['associationForeignKey']);
  377. $conditions = array($joinModel->alias . '.' . $assocData['foreignKey'] => $record[$model->alias][$model->primaryKey]);
  378. $recursive = -1;
  379. $ids = $joinModel->getDataSource()->read($joinModel, compact('fields', 'conditions', 'recursive') + $query);
  380. if ($ids) {
  381. $ids = Hash::extract($ids, "{n}.{$joinModel->alias}.{$assocData['associationForeignKey']}");
  382. $conditions = array_merge($query['conditions'], array($linkModel->alias . '.' . $linkModel->primaryKey => $ids));
  383. $data = $this->read($linkModel, compact('conditions') + $query);
  384. }
  385. } else {
  386. continue;
  387. }
  388. if (!$data) {
  389. $record += array($linkModel->alias => array());
  390. continue;
  391. }
  392. $formatted = array();
  393. foreach ($data as $associated) {
  394. foreach ($associated as $modelName => $associatedData) {
  395. if ($modelName === $linkModel->alias) {
  396. continue;
  397. }
  398. $associated[$linkModel->alias][$modelName] = $associatedData;
  399. unset($associated[$modelName]);
  400. }
  401. $formatted[] = $associated;
  402. }
  403. if ($type === 'hasOne' || $type === 'belongsTo') {
  404. $record += array($linkModel->alias => $formatted[0][$linkModel->alias]);
  405. continue;
  406. }
  407. $record += array($linkModel->alias => Hash::extract($formatted, "{n}.{$linkModel->alias}"));
  408. }
  409. }
  410. /**
  411. * Get the query log as an array.
  412. *
  413. * @param boolean $sorted Get the queries sorted by time taken, defaults to false.
  414. * @param boolean $clear Clear after return logs
  415. * @return array Array of queries run as an array
  416. */
  417. public function getLog($sorted = false, $clear = true) {
  418. if ($sorted) {
  419. $log = sortByKey($this->_requestsLog, 'took', 'desc', SORT_NUMERIC);
  420. } else {
  421. $log = $this->_requestsLog;
  422. }
  423. if ($clear) {
  424. $this->_requestsLog = array();
  425. }
  426. return array('log' => $log, 'count' => count($log), 'time' => array_sum(Hash::extract($log, '{n}.took')));
  427. }
  428. /**
  429. * Generate a log registry
  430. *
  431. * @param Model $model
  432. * @param array $queryData
  433. * @param float $took
  434. * @param integer $numRows
  435. * @return void
  436. */
  437. protected function _registerLog(Model $model, &$queryData, $took, $numRows) {
  438. if (!Configure::read('debug')) {
  439. return;
  440. }
  441. $this->_requestsLog[] = array(
  442. 'query' => $this->_pseudoSelect($model, $queryData),
  443. 'error' => '',
  444. 'affected' => 0,
  445. 'numRows' => $numRows,
  446. 'took' => round($took, 3)
  447. );
  448. }
  449. /**
  450. * Generate a pseudo select to log
  451. *
  452. * @param Model $model Model
  453. * @param array $queryData Query data sent by find
  454. * @return string Pseudo query
  455. */
  456. protected function _pseudoSelect(Model $model, &$queryData) {
  457. $out = '(symbolic) SELECT ';
  458. if (empty($queryData['fields'])) {
  459. $out .= '*';
  460. } elseif ($queryData['fields']) {
  461. $out .= 'COUNT(*)';
  462. } else {
  463. $out .= implode(', ', $queryData['fields']);
  464. }
  465. $out .= ' FROM ' . $model->alias;
  466. if (!empty($queryData['conditions'])) {
  467. $out .= ' WHERE';
  468. foreach ($queryData['conditions'] as $id => $condition) {
  469. if (empty($condition)) {
  470. continue;
  471. }
  472. if (is_array($condition)) {
  473. $condition = '(' . implode(', ', $condition) . ')';
  474. if (strpos($id, ' ') === false) {
  475. $id .= ' IN';
  476. }
  477. }
  478. if (is_string($id)) {
  479. if (strpos($id, ' ') !== false) {
  480. $condition = $id . ' ' . $condition;
  481. } else {
  482. $condition = $id . ' = ' . $condition;
  483. }
  484. }
  485. if (preg_match('/^(\w+\.)?\w+ /', $condition, $matches)) {
  486. if (!empty($matches[1]) && substr($matches[1], 0, -1) !== $model->alias) {
  487. continue;
  488. }
  489. }
  490. $out .= ' (' . $condition . ') &&';
  491. }
  492. $out = substr($out, 0, -3);
  493. }
  494. if (!empty($queryData['order'][0])) {
  495. $order = $queryData['order'];
  496. if (is_array($order[0])) {
  497. $new = array();
  498. foreach ($order[0] as $field => $direction) {
  499. $new[] = "$field $direction";
  500. }
  501. $order = $new;
  502. }
  503. $out .= ' ORDER BY ' . implode(', ', $order);
  504. }
  505. if (!empty($queryData['limit'])) {
  506. $out .= ' LIMIT ' . (($queryData['page'] - 1) * $queryData['limit']) . ', ' . $queryData['limit'];
  507. }
  508. return $out;
  509. }
  510. }