Database.php 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, 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;
  9. use PDO;
  10. use PDOException;
  11. use lithium\util\Set;
  12. use lithium\util\String;
  13. use lithium\util\Inflector;
  14. use InvalidArgumentException;
  15. use lithium\core\ConfigException;
  16. use lithium\core\NetworkException;
  17. use lithium\data\model\QueryException;
  18. /**
  19. * The `Database` class provides the base-level abstraction for SQL-oriented relational databases.
  20. * It handles all aspects of abstraction, including formatting for basic query types and SQL
  21. * fragments (i.e. for joins), converting `Query` objects to SQL, and various other functionality
  22. * which is shared across multiple relational databases.
  23. *
  24. * @see lithium\data\model\Query
  25. */
  26. abstract class Database extends \lithium\data\Source {
  27. /**
  28. * @var PDO
  29. */
  30. public $connection;
  31. /**
  32. * The supported column types and their default values
  33. *
  34. * @var array
  35. */
  36. protected $_columns = array(
  37. 'string' => array('length' => 255)
  38. );
  39. /**
  40. * Strings used to render the given statement
  41. *
  42. * @see lithium\data\source\Database::renderCommand()
  43. * @var array
  44. */
  45. protected $_strings = array(
  46. 'create' => "INSERT INTO {:source} ({:fields}) VALUES ({:values});{:comment}",
  47. 'update' => "UPDATE {:source} SET {:fields} {:conditions};{:comment}",
  48. 'delete' => "DELETE {:flags} FROM {:source} {:conditions};{:comment}",
  49. 'schema' => "CREATE TABLE {:source} (\n{:columns}{:indexes});{:comment}",
  50. 'join' => "{:type} JOIN {:source} {:alias} {:constraints}"
  51. );
  52. /**
  53. * Classes used by `Database`.
  54. *
  55. * @var array
  56. */
  57. protected $_classes = array(
  58. 'entity' => 'lithium\data\entity\Record',
  59. 'set' => 'lithium\data\collection\RecordSet',
  60. 'relationship' => 'lithium\data\model\Relationship',
  61. 'result' => 'lithium\data\source\database\adapter\pdo\Result',
  62. 'schema' => 'lithium\data\Schema',
  63. 'query' => 'lithium\data\model\Query'
  64. );
  65. /**
  66. * List of SQL operators, paired with handling options.
  67. *
  68. * @var array
  69. */
  70. protected $_operators = array(
  71. '=' => array('multiple' => 'IN'),
  72. '<' => array(),
  73. '>' => array(),
  74. '<=' => array(),
  75. '>=' => array(),
  76. '!=' => array('multiple' => 'NOT IN'),
  77. '<>' => array('multiple' => 'NOT IN'),
  78. 'between' => array('format' => 'BETWEEN ? AND ?'),
  79. 'BETWEEN' => array('format' => 'BETWEEN ? AND ?'),
  80. 'like' => array(),
  81. 'LIKE' => array(),
  82. 'not like' => array(),
  83. 'NOT LIKE' => array()
  84. );
  85. protected $_constraintTypes = array(
  86. 'AND' => true,
  87. 'and' => true,
  88. 'OR' => true,
  89. 'or' => true
  90. );
  91. /**
  92. * A pair of opening/closing quote characters used for quoting identifiers in SQL queries.
  93. *
  94. * @var array
  95. */
  96. protected $_quotes = array();
  97. /**
  98. * Array of named callable objects representing different strategies for performing specific
  99. * types of queries.
  100. *
  101. * @var array
  102. */
  103. protected $_strategies = array();
  104. /**
  105. * Getter/Setter for the connection's encoding
  106. * Abstract. Must be defined by child class.
  107. *
  108. * @param mixed $encoding
  109. * @return mixed.
  110. */
  111. abstract public function encoding($encoding = null);
  112. /**
  113. * Return the last errors produced by a the execution of a query.
  114. * Abstract. Must be defined by child class.
  115. *
  116. */
  117. abstract public function error();
  118. /**
  119. * Execute a given query
  120. * Abstract. Must be defined by child class.
  121. *
  122. * @see lithium\data\source\Database::renderCommand()
  123. * @param string $sql The sql string to execute
  124. * @return resource
  125. */
  126. abstract protected function _execute($sql);
  127. /**
  128. * Get the last insert id from the database.
  129. * Abstract. Must be defined by child class.
  130. *
  131. * @param $query lithium\data\model\Query $context The given query.
  132. * @return void
  133. */
  134. abstract protected function _insertId($query);
  135. /**
  136. * Creates the database object and set default values for it.
  137. *
  138. * Options defined:
  139. * - 'database' _string_ Name of the database to use. Defaults to `null`.
  140. * - 'host' _string_ Name/address of server to connect to. Defaults to 'localhost'.
  141. * - 'login' _string_ Username to use when connecting to server. Defaults to 'root'.
  142. * - 'password' _string_ Password to use when connecting to server. Defaults to `''`.
  143. * - 'persistent' _boolean_ If true a persistent connection will be attempted, provided the
  144. * adapter supports it. Defaults to `true`.
  145. *
  146. * @param $config array Array of configuration options.
  147. * @return Database object.
  148. */
  149. public function __construct(array $config = array()) {
  150. $defaults = array(
  151. 'persistent' => true,
  152. 'host' => 'localhost',
  153. 'login' => 'root',
  154. 'password' => '',
  155. 'database' => null,
  156. 'encoding' => null,
  157. 'dsn' => null,
  158. 'options' => array()
  159. );
  160. $this->_strings += array(
  161. 'read' => 'SELECT {:fields} FROM {:source} {:alias} {:joins} {:conditions} {:group} ' .
  162. '{:having} {:order} {:limit};{:comment}'
  163. );
  164. $this->_strategies += array(
  165. 'joined' => function($self, $model, $context) {
  166. $with = $context->with();
  167. $strategy = function($me, $model, $tree, $path, $from, &$deps) use ($self, $context, $with) {
  168. foreach ($tree as $name => $childs) {
  169. if (!$rel = $model::relations($name)) {
  170. throw new QueryException("Model relationship `{$name}` not found.");
  171. }
  172. $constraints = array();
  173. $alias = $name;
  174. $relPath = $path ? $path . '.' . $name : $name;
  175. if (isset($with[$relPath])) {
  176. list($unallowed, $allowed) = Set::slice($with[$relPath], array(
  177. 'alias',
  178. 'constraints'
  179. ));
  180. if ($unallowed) {
  181. $message = "Only `'alias'` and `'constraints'` are allowed in ";
  182. $message .= "`'with'` using the `'joined'` strategy.";
  183. throw new QueryException($message);
  184. }
  185. extract($with[$relPath]);
  186. }
  187. $to = $context->alias($alias, $relPath);
  188. $deps[$to] = $deps[$from];
  189. $deps[$to][] = $from;
  190. if ($context->relationships($relPath) === null) {
  191. $context->relationships($relPath, array(
  192. 'type' => $rel->type(),
  193. 'model' => $rel->to(),
  194. 'fieldName' => $rel->fieldName(),
  195. 'alias' => $to
  196. ));
  197. $self->join($context, $rel, $from, $to, $constraints);
  198. }
  199. if (!empty($childs)) {
  200. $me($me, $rel->to(), $childs, $relPath, $to, $deps);
  201. }
  202. }
  203. };
  204. $tree = Set::expand(Set::normalize(array_keys($with)));
  205. $alias = $context->alias();
  206. $deps = array($alias => array());
  207. $strategy($strategy, $model, $tree, '', $alias, $deps);
  208. $models = $context->models();
  209. foreach ($context->fields() as $field) {
  210. if (!is_string($field)) {
  211. continue;
  212. }
  213. list($alias, $field) = $self->invokeMethod('_splitFieldname', array($field));
  214. $alias = $alias ?: $field;
  215. if ($alias && isset($models[$alias])) {
  216. foreach ($deps[$alias] as $depAlias) {
  217. $depModel = $models[$depAlias];
  218. $context->fields(array($depAlias => (array) $depModel::meta('key')));
  219. }
  220. }
  221. }
  222. },
  223. 'nested' => function($self, $model, $context) {
  224. throw new QueryException("This strategy is not yet implemented.");
  225. }
  226. );
  227. parent::__construct($config + $defaults);
  228. }
  229. public function connect() {
  230. $this->_isConnected = false;
  231. $config = $this->_config;
  232. if (!$config['database']) {
  233. throw new ConfigException('No Database configured');
  234. }
  235. if (!$config['dsn']) {
  236. throw new ConfigException('No DSN setup for DB Connection');
  237. }
  238. $dsn = $config['dsn'];
  239. $options = $config['options'] + array(
  240. PDO::ATTR_PERSISTENT => $config['persistent'],
  241. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
  242. );
  243. try {
  244. $this->connection = new PDO($dsn, $config['login'], $config['password'], $options);
  245. } catch (PDOException $e) {
  246. preg_match('/SQLSTATE\[(.+?)\]/', $e->getMessage(), $code);
  247. $code = $code[1] ?: 0;
  248. switch (true) {
  249. case $code === 'HY000' || substr($code, 0, 2) === '08':
  250. $msg = "Unable to connect to host `{$config['host']}`.";
  251. throw new NetworkException($msg, null, $e);
  252. case in_array($code, array('28000', '42000')):
  253. $msg = "Host connected, but could not access database `{$config['database']}`.";
  254. throw new ConfigException($msg, null, $e);
  255. }
  256. throw new ConfigException("An unknown configuration error has occured.", null, $e);
  257. }
  258. $this->_isConnected = true;
  259. if ($this->_config['encoding']) {
  260. $this->encoding($this->_config['encoding']);
  261. }
  262. return $this->_isConnected;
  263. }
  264. /**
  265. * Disconnects the adapter from the database.
  266. *
  267. * @return boolean Returns `true` on success, else `false`.
  268. */
  269. public function disconnect() {
  270. if ($this->_isConnected) {
  271. unset($this->connection);
  272. $this->_isConnected = false;
  273. }
  274. return true;
  275. }
  276. /**
  277. * Field name handler to ensure proper escaping.
  278. *
  279. * @param string $name Field or identifier name.
  280. * @return string Returns `$name` quoted according to the rules and quote characters of the
  281. * database adapter subclass.
  282. */
  283. public function name($name) {
  284. $open = reset($this->_quotes);
  285. $close = next($this->_quotes);
  286. list($first, $second) = $this->_splitFieldname($name);
  287. if ($first) {
  288. return "{$open}{$first}{$close}.{$open}{$second}{$close}";
  289. }
  290. return preg_match('/^[a-z0-9_-]+$/i', $name) ? "{$open}{$name}{$close}" : $name;
  291. }
  292. /**
  293. * Return the alias and the field name from an identifier name.
  294. *
  295. * @param string $field Field name or identifier name.
  296. * @return array Returns an array with the alias (or `null` if not applicable) as first value
  297. * and the field name as second value.
  298. */
  299. protected function _splitFieldname($field) {
  300. if (is_string($field)) {
  301. if (preg_match('/^[a-z0-9_-]+\.([a-z0-9_-]+|\*)$/i', $field)) {
  302. return explode('.', $field, 2);
  303. }
  304. }
  305. return array(null, $field);
  306. }
  307. /**
  308. * Return the field name from a conditions key.
  309. *
  310. * @param string $field Field or identifier name.
  311. * @return string Returns the field name without the table alias, if applicable.
  312. * @todo Eventually, this should be refactored and moved to the Query or Schema
  313. * class. Also, by handling field resolution in this way we are not handling
  314. * cases where query conditions use the same field name in multiple tables.
  315. * e.g. Foos.bar and Bars.bar will both return bar.
  316. */
  317. protected function _fieldName($field) {
  318. if (is_string($field)) {
  319. if (preg_match('/^[a-z0-9_-]+\.[a-z0-9_-]+$/i', $field)) {
  320. list($first, $second) = explode('.', $field, 2);
  321. return $second;
  322. }
  323. }
  324. return $field;
  325. }
  326. /**
  327. * Converts a given value into the proper type based on a given schema definition.
  328. *
  329. * @see lithium\data\source\Database::schema()
  330. * @param mixed $value The value to be converted. Arrays will be recursively converted.
  331. * @param array $schema Formatted array from `lithium\data\source\Database::schema()`
  332. * @return mixed value with converted type
  333. */
  334. public function value($value, array $schema = array()) {
  335. if (is_array($value)) {
  336. foreach ($value as $key => $val) {
  337. $value[$key] = $this->value($val, isset($schema[$key]) ? $schema[$key] : $schema);
  338. }
  339. return $value;
  340. }
  341. if (is_object($value) && isset($value->scalar)) {
  342. return $value->scalar;
  343. }
  344. if ($value === null) {
  345. return 'NULL';
  346. }
  347. switch ($type = isset($schema['type']) ? $schema['type'] : $this->_introspectType($value)) {
  348. case 'boolean':
  349. case 'float':
  350. case 'integer':
  351. return $this->_cast($type, $value);
  352. default:
  353. return $this->connection->quote($this->_cast($type, $value));
  354. }
  355. }
  356. /**
  357. * Inserts a new record into the database based on a the `Query`. The record is updated
  358. * with the id of the insert.
  359. *
  360. * @see lithium\util\String::insert()
  361. * @param object $query An SQL query string, or `lithium\data\model\Query` object instance.
  362. * @param array $options If $query is a string, $options contains an array of bind values to be
  363. * escaped, quoted, and inserted into `$query` using `String::insert()`.
  364. * @return boolean Returns `true` if the query succeeded, otherwise `false`.
  365. * @filter
  366. */
  367. public function create($query, array $options = array()) {
  368. return $this->_filter(__METHOD__, compact('query', 'options'), function($self, $params) {
  369. $query = $params['query'];
  370. $model = $entity = $object = $id = null;
  371. if (is_object($query)) {
  372. $object = $query;
  373. $model = $query->model();
  374. $params = $query->export($self);
  375. $entity =& $query->entity();
  376. $query = $self->renderCommand('create', $params, $query);
  377. } else {
  378. $query = String::insert($query, $self->value($params['options']));
  379. }
  380. if (!$self->invokeMethod('_execute', array($query))) {
  381. return false;
  382. }
  383. if ($entity) {
  384. if (($model) && !$model::key($entity)) {
  385. $id = $self->invokeMethod('_insertId', array($object));
  386. }
  387. $entity->sync($id);
  388. }
  389. return true;
  390. });
  391. }
  392. /**
  393. * Reads records from a database using a `lithium\data\model\Query` object or raw SQL string.
  394. *
  395. * @param string|object $query `lithium\data\model\Query` object or SQL string.
  396. * @param array $options If `$query` is a raw string, contains the values that will be escaped
  397. * and quoted. Other options:
  398. * - `'return'` _string_: switch return between `'array'`, `'item'`, or
  399. * `'resource'` _string_: Defaults to `'item'`.
  400. * @return mixed Determined by `$options['return']`.
  401. * @filter
  402. */
  403. public function read($query, array $options = array()) {
  404. $defaults = array(
  405. 'return' => is_string($query) ? 'array' : 'item',
  406. 'schema' => null,
  407. 'quotes' => $this->_quotes
  408. );
  409. $options += $defaults;
  410. return $this->_filter(__METHOD__, compact('query', 'options'), function($self, $params) {
  411. $query = $params['query'];
  412. $args = $params['options'];
  413. $return = $args['return'];
  414. unset($args['return']);
  415. $model = is_object($query) ? $query->model() : null;
  416. if (is_string($query)) {
  417. $sql = String::insert($query, $self->value($args));
  418. } else {
  419. if (!$data = $self->invokeMethod('_queryExport', array($query))) {
  420. return false;
  421. }
  422. $sql = $self->renderCommand($data['type'], $data);
  423. }
  424. $result = $self->invokeMethod('_execute', array($sql));
  425. switch ($return) {
  426. case 'resource':
  427. return $result;
  428. case 'array':
  429. $columns = $args['schema'] ?: $self->schema($query, $result);
  430. if (!is_array(reset($columns))) {
  431. $columns = array('' => $columns);
  432. }
  433. $i = 0;
  434. $records = array();
  435. foreach ($result as $data) {
  436. $offset = 0;
  437. $records[$i] = array();
  438. foreach ($columns as $path => $cols) {
  439. $len = count($cols);
  440. $values = array_combine($cols, array_slice($data, $offset, $len));
  441. if ($path) {
  442. $records[$i][$path] = $values;
  443. } else {
  444. $records[$i] += $values;
  445. }
  446. $offset += $len;
  447. }
  448. $i++;
  449. }
  450. return Set::expand($records);
  451. case 'item':
  452. return $self->item($query->model(), array(), compact('query', 'result') + array(
  453. 'class' => 'set'
  454. ));
  455. }
  456. });
  457. }
  458. /**
  459. * Helper which export the query export
  460. *
  461. * @param object $query The query object
  462. * @return array The export array
  463. */
  464. protected function &_queryExport($query) {
  465. $data = $query->export($this);
  466. if ($query->limit() && ($model = $query->model())) {
  467. foreach ($query->relationships() as $relation) {
  468. if ($relation['type'] === 'hasMany') {
  469. $name = $model::meta('name');
  470. $key = $model::key();
  471. $fields = $data['fields'];
  472. $fieldname = $this->name("{$name}.{$key}");
  473. $data['fields'] = "DISTINCT({$fieldname}) AS _ID_";
  474. $sql = $this->renderCommand('read', $data);
  475. $result = $this->_execute($sql);
  476. $ids = array();
  477. while ($row = $result->next()) {
  478. $ids[] = $row[0];
  479. }
  480. if (!$ids) {
  481. $return = null;
  482. return $return;
  483. }
  484. $data['fields'] = $fields;
  485. $data['limit'] = '';
  486. $data['conditions'] = $this->conditions(array("{$name}.{$key}" => $ids), $query);
  487. return $data;
  488. }
  489. }
  490. }
  491. return $data;
  492. }
  493. /**
  494. * Updates a record in the database based on the given `Query`.
  495. *
  496. * @param object $query A `lithium\data\model\Query` object
  497. * @param array $options none
  498. * @return boolean
  499. * @filter
  500. */
  501. public function update($query, array $options = array()) {
  502. return $this->_filter(__METHOD__, compact('query', 'options'), function($self, $params) {
  503. $query = $params['query'];
  504. $params = $query->export($self);
  505. $sql = $self->renderCommand('update', $params, $query);
  506. $result = (boolean) $self->invokeMethod('_execute', array($sql));
  507. if ($result && is_object($query) && $query->entity()) {
  508. $query->entity()->sync();
  509. }
  510. return $result;
  511. });
  512. }
  513. /**
  514. * Deletes a record in the database based on the given `Query`.
  515. *
  516. * @param object $query An SQL string, or `lithium\data\model\Query` object instance.
  517. * @param array $options If `$query` is a string, `$options` is the array of quoted/escaped
  518. * parameter values to be inserted into the query.
  519. * @return boolean Returns `true` on successful query execution (not necessarily if records are
  520. * deleted), otherwise `false`.
  521. * @filter
  522. */
  523. public function delete($query, array $options = array()) {
  524. return $this->_filter(__METHOD__, compact('query', 'options'), function($self, $params) {
  525. $query = $params['query'];
  526. $isObject = is_object($query);
  527. if ($isObject) {
  528. $sql = $self->renderCommand('delete', $query->export($self), $query);
  529. } else {
  530. $sql = String::insert($query, $self->value($params['options']));
  531. }
  532. $result = (boolean) $self->invokeMethod('_execute', array($sql));
  533. if ($result && $isObject && $query->entity()) {
  534. $query->entity()->sync(null, array(), array('dematerialize' => true));
  535. }
  536. return $result;
  537. });
  538. }
  539. /**
  540. * Executes calculation-related queries, such as those required for `count` and other
  541. * aggregates.
  542. *
  543. * @param string $type Only accepts `count`.
  544. * @param mixed $query The query to be executed.
  545. * @param array $options Optional arguments for the `read()` query that will be executed
  546. * to obtain the calculation result.
  547. * @return integer Result of the calculation.
  548. */
  549. public function calculation($type, $query, array $options = array()) {
  550. $query->calculate($type);
  551. switch ($type) {
  552. case 'count':
  553. if (strpos($fields = $this->fields($query->fields(), $query), ',') !== false) {
  554. $fields = "*";
  555. }
  556. $query->fields("COUNT({$fields}) as count", true);
  557. $query->map(array($query->alias() => array('count')));
  558. list($record) = $this->read($query, $options)->data();
  559. return isset($record['count']) ? intval($record['count']) : null;
  560. }
  561. }
  562. /**
  563. * Defines or modifies the default settings of a relationship between two models.
  564. *
  565. * @param string $class the primary model of the relationship
  566. * @param string $type the type of the relationship (hasMany, hasOne, belongsTo)
  567. * @param string $name the name of the relationship
  568. * @param array $config relationship options
  569. * @return array Returns an array containing the configuration for a model relationship.
  570. */
  571. public function relationship($class, $type, $name, array $config = array()) {
  572. $field = Inflector::underscore(Inflector::singularize($name));
  573. $key = "{$field}_id";
  574. $primary = $class::meta('key');
  575. if (is_array($primary)) {
  576. $key = array_combine($primary, $primary);
  577. } elseif ($type === 'hasMany' || $type === 'hasOne') {
  578. if ($type === 'hasMany') {
  579. $field = Inflector::pluralize($field);
  580. }
  581. $secondary = Inflector::underscore(Inflector::singularize($class::meta('name')));
  582. $key = array($primary => "{$secondary}_id");
  583. }
  584. $from = $class;
  585. $fieldName = $field;
  586. $config += compact('type', 'name', 'key', 'from', 'fieldName');
  587. return $this->_instance('relationship', $config);
  588. }
  589. /**
  590. * Determines the set of methods to be used when exporting query values.
  591. *
  592. * @return array
  593. */
  594. public function methods() {
  595. $result = parent::methods();
  596. $key = array_search('schema', $result);
  597. unset($result[$key]);
  598. return $result;
  599. }
  600. /**
  601. * Returns a given `type` statement for the given data, rendered from `Database::$_strings`.
  602. *
  603. * @param string $type One of `'create'`, `'read'`, `'update'`, `'delete'` or `'join'`.
  604. * @param string $data The data to replace in the string.
  605. * @param string $context
  606. * @return string
  607. */
  608. public function renderCommand($type, $data = null, $context = null) {
  609. if (is_object($type)) {
  610. $context = $type;
  611. $data = $context->export($this);
  612. $type = $context->type();
  613. }
  614. if (!isset($this->_strings[$type])) {
  615. throw new InvalidArgumentException("Invalid query type `{$type}`.");
  616. }
  617. $template = $this->_strings[$type];
  618. $data = array_filter($data);
  619. return trim(String::insert($template, $data, array('clean' => true)));
  620. }
  621. /**
  622. * Builds an array of keyed on the fully-namespaced `Model` with array of fields as values
  623. * for the given `Query`
  624. *
  625. * @param data\model\Query $query A Query instance.
  626. * @param object $resource
  627. * @param object $context
  628. */
  629. public function schema($query, $resource = null, $context = null) {
  630. if (is_object($query)) {
  631. $query->applyStrategy($this);
  632. return $this->_schema($query, $this->_fields($query->fields(), $query));
  633. }
  634. $result = array();
  635. $count = $resource->resource()->columnCount();
  636. for ($i = 0; $i < $count; $i++) {
  637. $meta = $resource->resource()->getColumnMeta($i);
  638. $result[] = $meta['name'];
  639. }
  640. return $result;
  641. }
  642. /**
  643. * Helper method for `data\model\Database::shema()`
  644. *
  645. * @param data\model\Query $query A Query instance.
  646. * @param array $fields Array of formatted fields.
  647. */
  648. protected function _schema($query, $fields = null) {
  649. $model = $query->model();
  650. $paths = $query->paths($this);
  651. $models = $query->models($this);
  652. $alias = $query->alias();
  653. $result = array();
  654. if (!$model) {
  655. foreach ($fields as $field => $value) {
  656. if (is_array($value)) {
  657. $result[$field] = array_keys($value);
  658. } else {
  659. $result[''][] = $field;
  660. }
  661. }
  662. return $result;
  663. }
  664. if (!$fields) {
  665. foreach ($paths as $alias => $relation) {
  666. $model = $models[$alias];
  667. $result[$relation] = $model::schema()->names();
  668. }
  669. return $result;
  670. }
  671. $unalias = function ($value) {
  672. if (is_object($value) && isset($value->scalar)) {
  673. $value = $value->scalar;
  674. }
  675. $aliasing = preg_split("/\s+as\s+/i", $value);
  676. return isset($aliasing[1]) ? $aliasing[1] : $value;
  677. };
  678. if (isset($fields[0])) {
  679. $raw = array_map($unalias, $fields[0]);
  680. unset($fields[0]);
  681. }
  682. $fields = isset($fields[$alias]) ? array($alias => $fields[$alias]) + $fields : $fields;
  683. foreach ($fields as $field => $value) {
  684. if (is_array($value)) {
  685. if (isset($value['*'])) {
  686. $relModel = $models[$field];
  687. $result[$paths[$field]] = $relModel::schema()->names();
  688. } else {
  689. $result[$paths[$field]] = array_map($unalias, array_keys($value));
  690. }
  691. }
  692. }
  693. if (isset($raw)) {
  694. $result[''] = isset($result['']) ? array_merge($raw, $result['']) : $raw;
  695. }
  696. return $result;
  697. }
  698. /**
  699. * Returns a string of formatted conditions to be inserted into the query statement. If the
  700. * query conditions are defined as an array, key pairs are converted to SQL strings.
  701. *
  702. * Conversion rules are as follows:
  703. *
  704. * - If `$key` is numeric and `$value` is a string, `$value` is treated as a literal SQL
  705. * fragment and returned.
  706. *
  707. * @param string|array $conditions The conditions for this query.
  708. * @param object $context The current `lithium\data\model\Query` instance.
  709. * @param array $options
  710. * - `prepend` _boolean_: Whether the return string should be prepended with the
  711. * `WHERE` keyword.
  712. * @return string Returns the `WHERE` clause of an SQL query.
  713. */
  714. public function conditions($conditions, $context, array $options = array()) {
  715. $defaults = array('prepend' => 'WHERE');
  716. $options += $defaults;
  717. return $this->_conditions($conditions, $context, $options);
  718. }
  719. /**
  720. * Returns a string of formatted havings to be inserted into the query statement. If the
  721. * query havings are defined as an array, key pairs are converted to SQL strings.
  722. *
  723. * Conversion rules are as follows:
  724. *
  725. * - If `$key` is numeric and `$value` is a string, `$value` is treated as a literal SQL
  726. * fragment and returned.
  727. *
  728. * @param string|array $conditions The havings for this query.
  729. * @param object $context The current `lithium\data\model\Query` instance.
  730. * @param array $options
  731. * - `prepend` _boolean_: Whether the return string should be prepended with the
  732. * `HAVING` keyword.
  733. * @return string Returns the `HAVING` clause of an SQL query.
  734. */
  735. public function having($conditions, $context, array $options = array()) {
  736. $defaults = array('prepend' => 'HAVING');
  737. $options += $defaults;
  738. return $this->_conditions($conditions, $context, $options);
  739. }
  740. /**
  741. * Returns a string of formatted conditions to be inserted into the query statement. If the
  742. * query conditions are defined as an array, key pairs are converted to SQL strings.
  743. *
  744. * Conversion rules are as follows:
  745. *
  746. * - If `$key` is numeric and `$value` is a string, `$value` is treated as a literal SQL
  747. * fragment and returned.
  748. *
  749. * @param string|array $conditions The conditions for this query.
  750. * @param object $context The current `lithium\data\model\Query` instance.
  751. * @param array $options
  752. * - `prepend` mixed: The string to prepend or false for no prepending
  753. * @return string Returns an SQL conditions clause.
  754. */
  755. protected function _conditions($conditions, $context, array $options = array()) {
  756. $defaults = array('prepend' => false);
  757. $ops = $this->_operators;
  758. $options += $defaults;
  759. switch (true) {
  760. case empty($conditions):
  761. return '';
  762. case is_string($conditions):
  763. return $options['prepend'] ? $options['prepend'] . " {$conditions}" : $conditions;
  764. case !is_array($conditions):
  765. return '';
  766. }
  767. $result = array();
  768. foreach ($conditions as $key => $value) {
  769. $return = $this->_processConditions($key, $value, $context);
  770. if ($return) {
  771. $result[] = $return;
  772. }
  773. }
  774. $result = join(" AND ", $result);
  775. return ($options['prepend'] && $result) ? $options['prepend'] . " {$result}" : $result;
  776. }
  777. protected function _processConditions($key, $value, $context, $schema = null, $glue = 'AND') {
  778. $constraintTypes =& $this->_constraintTypes;
  779. $model = $context->model();
  780. $models = $context->models();
  781. list($first, $second) = $this->_splitFieldname($key);
  782. if ($first && isset($models[$first]) && $class = $models[$first]) {
  783. $schema = $class::schema();
  784. } elseif ($model) {
  785. $schema = $model::schema();
  786. }
  787. $fieldMeta = $schema ? (array) $schema->fields($second) : array();
  788. switch (true) {
  789. case (is_numeric($key) && is_string($value)):
  790. return $value;
  791. case is_object($value) && isset($value->scalar):
  792. if (is_numeric($key)) {
  793. return $this->value($value);
  794. }
  795. case is_scalar($value) || is_null($value):
  796. if ($context && ($context->type() === 'read') && ($alias = $context->alias())) {
  797. $key = $this->_aliasing($key, $alias);
  798. }
  799. if (isset($value)) {
  800. return $this->name($key) . ' = ' . $this->value($value, $fieldMeta);
  801. }
  802. return $this->name($key) . " IS NULL";
  803. case is_numeric($key) && is_array($value):
  804. $result = array();
  805. foreach ($value as $cKey => $cValue) {
  806. $result[] = $this->_processConditions($cKey, $cValue, $context, $schema, $glue);
  807. }
  808. return '(' . implode(' ' . $glue . ' ', $result) . ')';
  809. case (is_string($key) && is_object($value)):
  810. $value = trim(rtrim($this->renderCommand($value), ';'));
  811. return "{$this->name($key)} IN ({$value})";
  812. case is_array($value) && isset($constraintTypes[strtoupper($key)]):
  813. $result = array();
  814. $glue = strtoupper($key);
  815. foreach ($value as $cKey => $cValue) {
  816. $result[] = $this->_processConditions($cKey, $cValue, $context, $schema, $glue);
  817. }
  818. return '(' . implode(' ' . $glue . ' ', $result) . ')';
  819. case (is_string($key) && is_array($value) && isset($this->_operators[key($value)])):
  820. foreach ($value as $op => $val) {
  821. $result[] = $this->_operator($key, array($op => $val), $fieldMeta);
  822. }
  823. return '(' . implode(' ' . $glue . ' ', $result) . ')';
  824. case is_array($value):
  825. if (!is_numeric($op = key($value))) {
  826. throw new QueryException("Unsupported operator `{$op}`.");
  827. }
  828. $value = join(', ', $this->value($value, $fieldMeta));
  829. return "{$this->name($key)} IN ({$value})";
  830. }
  831. }
  832. /**
  833. * Returns a string of formatted fields to be inserted into the query statement.
  834. *
  835. * @param array $fields Array of fields.
  836. * @param object $context Generally a `data\model\Query` instance.
  837. * @return string A SQL formatted string
  838. */
  839. public function fields($fields, $context) {
  840. $type = $context->type();
  841. $schema = $context->schema()->fields();
  842. $alias = $context->alias();
  843. if (!is_array($fields)) {
  844. return $this->_fieldsReturn($type, $context, $fields, $schema);
  845. }
  846. $context->applyStrategy($this);
  847. $fields = $this->_fields($fields ? : $context->fields(), $context);
  848. $context->map($this->_schema($context, $fields));
  849. $toMerge = array();
  850. if (isset($fields[0])) {
  851. foreach ($fields[0] as $val) {
  852. $toMerge[] = (is_object($val) && isset($val->scalar)) ? $val->scalar : $val;
  853. }
  854. unset($fields[0]);
  855. }
  856. $fields = isset($fields[$alias]) ? array($alias => $fields[$alias]) + $fields : $fields;
  857. foreach ($fields as $field => $value) {
  858. if (is_array($value)) {
  859. if (isset($value['*'])) {
  860. $toMerge[] = $this->name($field) . '.*';
  861. continue;
  862. }
  863. foreach ($value as $fieldname => $mode) {
  864. $toMerge[] = $this->_fieldsQuote($field, $fieldname);
  865. }
  866. }
  867. }
  868. return $this->_fieldsReturn($type, $context, $toMerge, $schema);
  869. }
  870. /**
  871. * Helper for `Database::fields()` && `Database::schema()`.
  872. * Reformat fields to be alias based.
  873. *
  874. * @param array $fields Array of fields.
  875. * @param object $context Generally a `data\model\Query` instance.
  876. * @return array Reformatted fields
  877. */
  878. protected function _fields($fields, $context) {
  879. $alias = $context->alias();
  880. $models = $context->models($this);
  881. $list = array();
  882. foreach ($fields as $key => $field) {
  883. if (!is_string($field)) {
  884. if (isset($models[$key])) {
  885. $field = array_fill_keys($field, true);
  886. $list[$key] = isset($list[$key]) ? array_merge($list[$key], $field) : $field;
  887. } else {
  888. $list[0][] = is_array($field) ? reset($field) : $field;
  889. }
  890. continue;
  891. }
  892. if (preg_match('/^([a-z0-9_-]+|\*)$/i', $field)) {
  893. isset($models[$field]) ? $list[$field]['*'] = true : $list[$alias][$field] = true;
  894. } elseif (preg_match('/^([a-z0-9_-]+)\.(.*)$/i', $field, $matches)) {
  895. $list[$matches[1]][$matches[2]] = true;
  896. } else {
  897. $list[0][] = $field;
  898. }
  899. }
  900. return $list;
  901. }
  902. protected function _fieldsQuote($alias, $field) {
  903. $open = $this->_quotes[0];
  904. $close = $this->_quotes[1];
  905. $aliasing = preg_split("/\s+as\s+/i", $field);
  906. if (isset($aliasing[1])) {
  907. list($aliasname, $fieldname) = $this->_splitFieldname($aliasing[0]);
  908. $alias = $aliasname ? : $alias;
  909. return "{$open}{$alias}{$close}.{$open}{$fieldname}{$close} as {$aliasing[1]}";
  910. } elseif ($alias) {
  911. return "{$open}{$alias}{$close}.{$open}{$field}{$close}";
  912. } else {
  913. return "{$open}{$field}{$close}";
  914. }
  915. }
  916. protected function _fieldsReturn($type, $context, $fields, $schema) {
  917. if ($type === 'create' || $type === 'update') {
  918. $data = $context->data();
  919. if (isset($data['data']) && is_array($data['data']) && count($data) === 1) {
  920. $data = $data['data'];
  921. }
  922. if ($fields && is_array($fields) && is_int(key($fields))) {
  923. $data = array_intersect_key($data, array_combine($fields, $fields));
  924. }
  925. $method = "_{$type}Fields";
  926. return $this->{$method}($data, $schema, $context);
  927. }
  928. return empty($fields) ? '*' : join(', ', $fields);
  929. }
  930. /**
  931. * Returns a LIMIT statement from the given limit and the offset of the context object.
  932. *
  933. * @param integer $limit An
  934. * @param object $context The `lithium\data\model\Query` object
  935. * @return string
  936. */
  937. public function limit($limit, $context) {
  938. if (!$limit) {
  939. return;
  940. }
  941. if ($offset = $context->offset() ?: '') {
  942. $offset = " OFFSET {$offset}";
  943. }
  944. return "LIMIT {$limit}{$offset}";
  945. }
  946. /**
  947. * Returns a join statement for given array of query objects
  948. *
  949. * @param object|array $joins A single or array of `lithium\data\model\Query` objects
  950. * @param object $context The parent `lithium\data\model\Query` object
  951. * @return string
  952. */
  953. public function joins(array $joins, $context) {
  954. $result = null;
  955. foreach ($joins as $key => $join) {
  956. if ($result) {
  957. $result .= ' ';
  958. }
  959. $join = is_array($join) ? $this->_instance('query', $join) : $join;
  960. $options['keys'] = array('source', 'alias', 'constraints');
  961. $result .= $this->renderCommand('join', $join->export($this, $options));
  962. }
  963. return $result;
  964. }
  965. /**
  966. * Returns a string of formatted constraints to be inserted into the query statement. If the
  967. * query constraints are defined as an array, key pairs are converted to SQL strings.
  968. *
  969. * Conversion rules are as follows:
  970. *
  971. * - If `$key` is numeric and `$value` is a string, `$value` is treated as a literal SQL
  972. * fragment and returned.
  973. *
  974. * @param string|array $constraints The constraints for a `ON` clause.
  975. * @param object $context The current `lithium\data\model\Query` instance.
  976. * @param array $options
  977. * - `prepend` _boolean_: Whether the return string should be prepended with the
  978. * `ON` keyword.
  979. * @return string Returns the `ON` clause of an SQL query.
  980. */
  981. public function constraints($constraints, $context, array $options = array()) {
  982. $defaults = array('prepend' => 'ON');
  983. $options += $defaults;
  984. if (is_array($constraints)) {
  985. $constraints = $this->_constraints($constraints);
  986. }
  987. return $this->_conditions($constraints, $context, $options);
  988. }
  989. /**
  990. * Auto escape string value to a field name value
  991. *
  992. * @param array $constraints The constraints array
  993. * @return array The escaped constraints array
  994. */
  995. protected function _constraints(array $constraints) {
  996. foreach ($constraints as &$value) {
  997. if (is_string($value)) {
  998. $value = (object) $this->name($value);
  999. } elseif (is_array($value)) {
  1000. $value = $this->_constraints($value);
  1001. }
  1002. }
  1003. return $constraints;
  1004. }
  1005. /**
  1006. * Return formatted clause for order.
  1007. *
  1008. * @param mixed $order The `order` clause to be formatted
  1009. * @param object $context
  1010. * @return mixed Formatted `order` clause.
  1011. */
  1012. public function order($order, $context) {
  1013. $direction = 'ASC';
  1014. $model = $context->model();
  1015. if (is_string($order)) {
  1016. if (!$model::schema($order)) {
  1017. $match = '/\s+(A|DE)SC/i';
  1018. return "ORDER BY {$order}" . (preg_match($match, $order) ? '' : " {$direction}");
  1019. }
  1020. $order = array($order => $direction);
  1021. }
  1022. if (!is_array($order)) {
  1023. return;
  1024. }
  1025. $result = array();
  1026. foreach ($order as $column => $dir) {
  1027. if (is_int($column)) {
  1028. $column = $dir;
  1029. $dir = $direction;
  1030. }
  1031. $dir = in_array($dir, array('ASC', 'asc', 'DESC', 'desc')) ? $dir : $direction;
  1032. if (!$model) {
  1033. $result[] = "{$column} {$dir}";
  1034. continue;
  1035. }
  1036. if ($field = $model::schema($column)) {
  1037. $name = $this->name($model::meta('name')) . '.' . $this->name($column);
  1038. $result[] = "{$name} {$dir}";
  1039. continue;
  1040. }
  1041. $result[] = "{$column} {$dir}";
  1042. }
  1043. $order = join(', ', $result);
  1044. return "ORDER BY {$order}";
  1045. }
  1046. public function group($group, $context = null) {
  1047. if (!$group) {
  1048. return null;
  1049. }
  1050. return 'GROUP BY ' . join(', ', (array) $group);
  1051. }
  1052. /**
  1053. * Adds formatting to SQL comments before they're embedded in queries.
  1054. *
  1055. * @param string $comment
  1056. * @return string
  1057. */
  1058. public function comment($comment) {
  1059. return $comment ? "/* {$comment} */" : null;
  1060. }
  1061. public function alias($alias, $context) {
  1062. if (!$alias && ($model = $context->model())) {
  1063. $alias = $model::meta('name');
  1064. }
  1065. return $alias ? "AS " . $this->name($alias) : null;
  1066. }
  1067. public function cast($entity, array $data, array $options = array()) {
  1068. return $data;
  1069. }
  1070. /**
  1071. * Cast a value according to a column type.
  1072. *
  1073. * @param string $type Name of the column type
  1074. * @param string $value Value to cast
  1075. *
  1076. * @return mixed Casted value
  1077. *
  1078. */
  1079. protected function _cast($type, $value) {
  1080. if (is_object($value) || $value === null) {
  1081. return $value;
  1082. }
  1083. if ($type === 'boolean') {
  1084. return $this->_toNativeBoolean($value);
  1085. }
  1086. if (!isset($this->_columns[$type]) || !isset($this->_columns[$type]['formatter'])) {
  1087. return $value;
  1088. }
  1089. $column = $this->_columns[$type];
  1090. switch ($column['formatter']) {
  1091. case 'date':
  1092. return $column['formatter']($column['format'], strtotime($value));
  1093. default:
  1094. return $column['formatter']($value);
  1095. }
  1096. }
  1097. protected function _createFields($data, $schema, $context) {
  1098. $fields = $values = array();
  1099. foreach ($data as $field => $value) {
  1100. $fields[] = $this->name($field);
  1101. $values[] = $this->value($value, isset($schema[$field]) ? $schema[$field] : array());
  1102. }
  1103. $fields = join(', ', $fields);
  1104. $values = join(', ', $values);
  1105. return compact('fields', 'values');
  1106. }
  1107. protected function _updateFields($data, $schema, $context) {
  1108. $fields = array();
  1109. foreach ($data as $field => $value) {
  1110. $schema += array($field => array('default' => null));
  1111. $fields[] = $this->name($field) . ' = ' . $this->value($value, $schema[$field]);
  1112. }
  1113. return join(', ', $fields);
  1114. }
  1115. /**
  1116. * Handles conversion of SQL operator keys to SQL statements.
  1117. *
  1118. * @param string $key Key in a conditions array. Usually a field name.
  1119. * @param mixed $value An SQL operator or comparison value.
  1120. * @param array $schema An array defining the schema of the field used in the criteria.
  1121. * @param array $options
  1122. * @return string Returns an SQL string representing part of a `WHERE` clause of a query.
  1123. */
  1124. protected function _operator($key, $value, array $schema = array(), array $options = array()) {
  1125. $defaults = array('boolean' => 'AND');
  1126. $options += $defaults;
  1127. list($op, $value) = each($value);
  1128. $config = $this->_operators[$op];
  1129. $key = $this->name($key);
  1130. $values = array();
  1131. if (!is_object($value)) {
  1132. foreach ((array) $value as $val) {
  1133. $values[] = $this->value($val, $schema);
  1134. }
  1135. } elseif (isset($value->scalar)) {
  1136. return "{$key} {$op} {$value->scalar}";
  1137. }
  1138. switch (true) {
  1139. case (isset($config['format'])):
  1140. return $key . ' ' . String::insert($config['format'], $values);
  1141. case (is_object($value) && isset($config['multiple'])):
  1142. $op = $config['multiple'];
  1143. $value = trim(rtrim($this->renderCommand($value), ';'));
  1144. return "{$key} {$op} ({$value})";
  1145. case (count($values) > 1 && isset($config['multiple'])):
  1146. $op = $config['multiple'];
  1147. $values = join(', ', $values);
  1148. return "{$key} {$op} ({$values})";
  1149. case (count($values) > 1):
  1150. return join(" {$options['boolean']} ", array_map(
  1151. function($v) use ($key, $op) { return "{$key} {$op} {$v}"; }, $values
  1152. ));
  1153. }
  1154. return "{$key} {$op} {$values[0]}";
  1155. }
  1156. /**
  1157. * Returns a fully-qualified table name (i.e. with prefix), quoted.
  1158. *
  1159. * @param string $entity A table name or fully-namespaced model class name.
  1160. * @param array $options Available options:
  1161. * - `'quoted'` _boolean_: Indicates whether the name should be quoted.
  1162. * @return string Returns a quoted table name.
  1163. */
  1164. protected function _entityName($entity, array $options = array()) {
  1165. $defaults = array('quoted' => false);
  1166. $options += $defaults;
  1167. if (class_exists($entity, false) && method_exists($entity, 'meta')) {
  1168. $entity = $entity::meta('source');
  1169. }
  1170. return $options['quoted'] ? $this->name($entity) : $entity;
  1171. }
  1172. /**
  1173. * Attempts to automatically determine the column type of a value. Used by the `value()` method
  1174. * of various database adapters to determine how to prepare a value if the schema is not
  1175. * specified.
  1176. *
  1177. * @param mixed $value The value to be prepared for an SQL query.
  1178. * @return string Returns the name of the column type which `$value` most likely belongs to.
  1179. */
  1180. protected function _introspectType($value) {
  1181. switch (true) {
  1182. case (is_bool($value)):
  1183. return 'boolean';
  1184. case (is_float($value) || preg_match('/^\d+\.\d+$/', $value)):
  1185. return 'float';
  1186. case (is_int($value) || preg_match('/^\d+$/', $value)):
  1187. return 'integer';
  1188. case (is_string($value) && strlen($value) <= $this->_columns['string']['length']):
  1189. return 'string';
  1190. default:
  1191. return 'text';
  1192. }
  1193. }
  1194. /**
  1195. * Casts a value which is being written or compared to a boolean-type database column.
  1196. *
  1197. * @param mixed $value A value of unknown type to be cast to boolean. Numeric values not equal
  1198. * to zero evaluate to `true`, otherwise `false`. String values equal to `'true'`,
  1199. * `'t'` or `'T'` evaluate to `true`, all others to `false`. In all other cases,
  1200. * uses PHP's default casting.
  1201. * @return boolean Returns a boolean representation of `$value`, based on the comparison rules
  1202. * specified above. Database adapters may override this method if boolean type coercion
  1203. * is required and falls outside the rules defined.
  1204. */
  1205. protected function _toBoolean($value) {
  1206. if (is_bool($value)) {
  1207. return $value;
  1208. }
  1209. if (is_int($value) || is_float($value)) {
  1210. return ($value !== 0);
  1211. }
  1212. if (is_string($value)) {
  1213. return ($value === 't' || $value === 'T' || $value === 'true');
  1214. }
  1215. return (boolean) $value;
  1216. }
  1217. protected function _toNativeBoolean($value) {
  1218. return $value ? 1 : 0;
  1219. }
  1220. /**
  1221. * Throw a `QueryException` error
  1222. *
  1223. * @param string $sql The offending SQL string
  1224. * @filter
  1225. */
  1226. protected function _error($sql){
  1227. $params = compact('sql');
  1228. return $this->_filter(__METHOD__, $params, function($self, $params) {
  1229. $sql = $params['sql'];
  1230. list($code, $error) = $self->error();
  1231. throw new QueryException("{$sql}: {$error}", $code);
  1232. });
  1233. }
  1234. /**
  1235. * Applying a strategy to a `lithium\data\model\Query` object
  1236. *
  1237. * @param array $options The option array
  1238. * @param object $context A query object to configure
  1239. */
  1240. public function applyStrategy($options, $context) {
  1241. $options += array('strategy' => 'joined');
  1242. if (!$model = $context->model()) {
  1243. throw new ConfigException('The `\'with\'` option need a valid `\'model\'` option.');
  1244. }
  1245. $strategy = $options['strategy'];
  1246. if (isset($this->_strategies[$strategy])) {
  1247. $strategy = $this->_strategies[$strategy];
  1248. $strategy($this, $model, $context);
  1249. } else {
  1250. throw new QueryException("Undefined query strategy `{$strategy}`.");
  1251. }
  1252. }
  1253. /**
  1254. * Set a query's join according a Relationship.
  1255. *
  1256. * @param object $context A Query instance
  1257. * @param object $rel A Relationship instance
  1258. * @param string $fromAlias Set a specific alias for the `'from'` `Model`.
  1259. * @param string $toAlias Set a specific alias for `'to'` `Model`.
  1260. * @param mixed $constraints If `$constraints` is an array, it will be merged to defaults
  1261. * constraints. If `$constraints` is an object, defaults won't be merged.
  1262. */
  1263. public function join($context, $rel, $fromAlias = null, $toAlias = null, $constraints = array()) {
  1264. $model = $rel->to();
  1265. if ($fromAlias === null) {
  1266. $from = $rel->from();
  1267. $fromAlias = $context->alias();
  1268. }
  1269. if ($toAlias === null) {
  1270. $toAlias = $context->alias(null, $rel->name());
  1271. }
  1272. if (!is_object($constraints)) {
  1273. $constraints = $this->on($rel, $fromAlias, $toAlias, $constraints);
  1274. } else {
  1275. $constraints = (array) $constraints;
  1276. }
  1277. $context->joins($toAlias, compact('constraints', 'model') + array(
  1278. 'type' => 'LEFT',
  1279. 'alias' => $toAlias
  1280. ));
  1281. }
  1282. /**
  1283. * Helper which add an alias basename to a field name if necessary
  1284. *
  1285. * @param string $name The field name.
  1286. * @param string $alias The alias name
  1287. * @param array $map An array of `'modelname' => 'aliasname'` mapping
  1288. * @return string
  1289. */
  1290. protected function _aliasing($name, $alias, $map = array()) {
  1291. list($first, $second) = $this->_splitFieldname($name);
  1292. if (!$first && preg_match('/^[a-z0-9_-]+$/i', $second)) {
  1293. return $alias . "." . $second;
  1294. } elseif (isset($map[$first])) {
  1295. return $map[$first] . "." . $second;
  1296. }
  1297. return $name;
  1298. }
  1299. /**
  1300. * Build the `ON` constraints from a `Relationship` instance
  1301. *
  1302. * @param object $rel A Relationship instance
  1303. * @param string $fromAlias Set a specific alias for the `'from'` `Model`.
  1304. * @param string $toAlias Set a specific alias for `'to'` `Model`.
  1305. * @param array $constraints Array of additionnal $constraints.
  1306. * @return array A constraints array.
  1307. */
  1308. public function on($rel, $aliasFrom = null, $aliasTo = null, $constraints = array()) {
  1309. $model = $rel->from();
  1310. $aliasFrom = $aliasFrom ?: $model::meta('name');
  1311. $aliasTo = $aliasTo ?: $rel->name();
  1312. $keyConstraints = array();
  1313. foreach ($rel->key() as $from => $to) {
  1314. $keyConstraints["{$aliasFrom}.{$from}"] = "{$aliasTo}.{$to}";
  1315. }
  1316. $mapAlias = array($model::meta('name') => $aliasFrom, $rel->name() => $aliasTo);
  1317. $relConstraints = $this->_on((array) $rel->constraints(), $aliasFrom, $aliasTo, $mapAlias);
  1318. $constraints = $this->_on($constraints, $aliasFrom, $aliasTo, array());
  1319. return $constraints + $relConstraints + $keyConstraints;
  1320. }
  1321. protected function _on(array $constraints, $aliasFrom, $aliasTo, $mapAlias = array()) {
  1322. $result = array();
  1323. foreach ($constraints as $key => $value) {
  1324. if (
  1325. !is_numeric($key) &&
  1326. !isset($this->_constraintTypes[$key]) &&
  1327. !isset($this->_operators[$key])
  1328. ) {
  1329. $key = $this->_aliasing($key, $aliasFrom, $mapAlias);
  1330. }
  1331. if (is_string($value)) {
  1332. $result[$key] = $this->_aliasing($value, $aliasTo, $mapAlias);
  1333. } elseif (is_array($value)) {
  1334. $result[$key] = $this->_on($value, $aliasFrom, $aliasTo, $mapAlias);
  1335. } else {
  1336. $result[$key] = $value;
  1337. }
  1338. }
  1339. return $result;
  1340. }
  1341. }
  1342. ?>