PageRenderTime 60ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Sublime/Data/ORM.php

https://github.com/chrsm/S1
PHP | 1187 lines | 565 code | 152 blank | 470 comment | 44 complexity | e191f30bb4b7ac581e3a72688a594328 MD5 | raw file
  1. <?php
  2. /**
  3. * Sublime\Data\ORM
  4. *
  5. * ORM is a namespaced port of Idiorm. Some modifications made.
  6. *
  7. * @see http://github.com/j4mie/idiorm/
  8. * @author chrsm <chrstphrmrtnz@gmail.com>
  9. * @copyright 2011 - 2012
  10. */
  11. /**
  12. *
  13. * Idiorm
  14. *
  15. * http://github.com/j4mie/idiorm/
  16. *
  17. * A single-class super-simple database abstraction layer for PHP.
  18. * Provides (nearly) zero-configuration object-relational mapping
  19. * and a fluent interface for building basic, commonly-used queries.
  20. *
  21. * BSD Licensed.
  22. *
  23. * Copyright (c) 2010, Jamie Matthews
  24. * All rights reserved.
  25. *
  26. * Redistribution and use in source and binary forms, with or without
  27. * modification, are permitted provided that the following conditions are met:
  28. *
  29. * * Redistributions of source code must retain the above copyright notice, this
  30. * list of conditions and the following disclaimer.
  31. *
  32. * * Redistributions in binary form must reproduce the above copyright notice,
  33. * this list of conditions and the following disclaimer in the documentation
  34. * and/or other materials provided with the distribution.
  35. *
  36. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  37. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  38. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  39. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
  40. * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  41. * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  42. * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  43. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  44. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  45. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  46. *
  47. */
  48. namespace Sublime\Data;
  49. use \PDO;
  50. class ORM
  51. {
  52. // ----------------------- //
  53. // --- CLASS CONSTANTS --- //
  54. // ----------------------- //
  55. // Where condition array keys
  56. const WHERE_FRAGMENT = 0;
  57. const WHERE_VALUES = 1;
  58. // ------------------------ //
  59. // --- CLASS PROPERTIES --- //
  60. // ------------------------ //
  61. // Class configuration
  62. protected static $_config = array(
  63. 'connection_string' => 'sqlite::memory:',
  64. 'id_column' => 'id',
  65. 'id_column_overrides' => array(),
  66. 'error_mode' => PDO::ERRMODE_EXCEPTION,
  67. 'username' => null,
  68. 'password' => null,
  69. 'driver_options' => null,
  70. 'identifier_quote_character' => null, // if this is null, will be autodetected
  71. 'logging' => false,
  72. 'caching' => false,
  73. );
  74. // Database connection, instance of the PDO class
  75. protected static $_db;
  76. // Last query run, only populated if logging is enabled
  77. protected static $_last_query;
  78. // Log of all queries run, only populated if logging is enabled
  79. protected static $_query_log = array();
  80. // Query cache, only used if query caching is enabled
  81. protected static $_query_cache = array();
  82. // --------------------------- //
  83. // --- INSTANCE PROPERTIES --- //
  84. // --------------------------- //
  85. // The name of the table the current ORM instance is associated with
  86. protected $_table_name;
  87. // Alias for the table to be used in SELECT queries
  88. protected $_table_alias = null;
  89. // Values to be bound to the query
  90. protected $_values = array();
  91. // Columns to select in the result
  92. protected $_result_columns = array('*');
  93. // Are we using the default result column or have these been manually changed?
  94. protected $_using_default_result_columns = true;
  95. // Join sources
  96. protected $_join_sources = array();
  97. // Should the query include a DISTINCT keyword?
  98. protected $_distinct = false;
  99. // Is this a raw query?
  100. protected $_is_raw_query = false;
  101. // The raw query
  102. protected $_raw_query = '';
  103. // The raw query parameters
  104. protected $_raw_parameters = array();
  105. // Array of WHERE clauses
  106. protected $_where_conditions = array();
  107. // LIMIT
  108. protected $_limit = null;
  109. // OFFSET
  110. protected $_offset = null;
  111. // ORDER BY
  112. protected $_order_by = array();
  113. // GROUP BY
  114. protected $_group_by = array();
  115. // The data for a hydrated instance of the class
  116. protected $_data = array();
  117. // Fields that have been modified during the
  118. // lifetime of the object
  119. protected $_dirty_fields = array();
  120. // Is this a new object (has create() been called)?
  121. protected $_is_new = false;
  122. // Name of the column to use as the primary key for
  123. // this instance only. Overrides the config settings.
  124. protected $_instance_id_column = null;
  125. /**
  126. * @patch 2012-03-06 chrsm
  127. * Allows a model to be reset.
  128. */
  129. protected $_resets = array('_values'=> array(), '_result_columns' => array('*'), '_using_default_result_columns' => true, '_join_sources' => array(),
  130. '_distinct' => false, '_is_raw_query' => false, '_raw_query' => '', '_raw_parameters' => array(),
  131. '_where_conditions' => array(), '_limit' => null, '_offset' => null, '_order_by' => array(), '_group_by' => array(),
  132. '_data' => array(), '_dirty_fields' => array(), '_is_new' => false
  133. );
  134. // ---------------------- //
  135. // --- STATIC METHODS --- //
  136. // ---------------------- //
  137. /**
  138. * Pass configuration settings to the class in the form of
  139. * key/value pairs. As a shortcut, if the second argument
  140. * is omitted, the setting is assumed to be the DSN string
  141. * used by PDO to connect to the database. Often, this
  142. * will be the only configuration required to use Idiorm.
  143. */
  144. public static function configure($key, $value=null) {
  145. // Shortcut: If only one argument is passed,
  146. // assume it's a connection string
  147. if (is_null($value)) {
  148. $value = $key;
  149. $key = 'connection_string';
  150. }
  151. self::$_config[$key] = $value;
  152. }
  153. /**
  154. * Despite its slightly odd name, this is actually the factory
  155. * method used to acquire instances of the class. It is named
  156. * this way for the sake of a readable interface, ie
  157. * ORM::for_table('table_name')->find_one()-> etc. As such,
  158. * this will normally be the first method called in a chain.
  159. */
  160. public static function for_table($table_name) {
  161. self::_setup_db();
  162. return new self($table_name);
  163. }
  164. /**
  165. * Set up the database connection used by the class.
  166. */
  167. protected static function _setup_db() {
  168. if (!is_object(self::$_db)) {
  169. $connection_string = self::$_config['connection_string'];
  170. $username = self::$_config['username'];
  171. $password = self::$_config['password'];
  172. $driver_options = self::$_config['driver_options'];
  173. $db = new PDO($connection_string, $username, $password, $driver_options);
  174. $db->setAttribute(PDO::ATTR_ERRMODE, self::$_config['error_mode']);
  175. self::set_db($db);
  176. }
  177. }
  178. /**
  179. * Set the PDO object used by Idiorm to communicate with the database.
  180. * This is public in case the ORM should use a ready-instantiated
  181. * PDO object as its database connection.
  182. */
  183. public static function set_db($db) {
  184. self::$_db = $db;
  185. self::_setup_identifier_quote_character();
  186. }
  187. /**
  188. * Detect and initialise the character used to quote identifiers
  189. * (table names, column names etc). If this has been specified
  190. * manually using ORM::configure('identifier_quote_character', 'some-char'),
  191. * this will do nothing.
  192. */
  193. public static function _setup_identifier_quote_character() {
  194. if (is_null(self::$_config['identifier_quote_character'])) {
  195. self::$_config['identifier_quote_character'] = self::_detect_identifier_quote_character();
  196. }
  197. }
  198. /**
  199. * Return the correct character used to quote identifiers (table
  200. * names, column names etc) by looking at the driver being used by PDO.
  201. */
  202. protected static function _detect_identifier_quote_character() {
  203. switch(self::$_db->getAttribute(PDO::ATTR_DRIVER_NAME)) {
  204. case 'pgsql':
  205. case 'sqlsrv':
  206. case 'dblib':
  207. case 'mssql':
  208. case 'sybase':
  209. return '"';
  210. case 'mysql':
  211. case 'sqlite':
  212. case 'sqlite2':
  213. default:
  214. return '`';
  215. }
  216. }
  217. /**
  218. * Returns the PDO instance used by the the ORM to communicate with
  219. * the database. This can be called if any low-level DB access is
  220. * required outside the class.
  221. */
  222. public static function get_db() {
  223. self::_setup_db(); // required in case this is called before Idiorm is instantiated
  224. return self::$_db;
  225. }
  226. /**
  227. * Add a query to the internal query log. Only works if the
  228. * 'logging' config option is set to true.
  229. *
  230. * This works by manually binding the parameters to the query - the
  231. * query isn't executed like this (PDO normally passes the query and
  232. * parameters to the database which takes care of the binding) but
  233. * doing it this way makes the logged queries more readable.
  234. */
  235. protected static function _log_query($query, $parameters) {
  236. // If logging is not enabled, do nothing
  237. if (!self::$_config['logging']) {
  238. return false;
  239. }
  240. if (count($parameters) > 0) {
  241. // Escape the parameters
  242. $parameters = array_map(array(self::$_db, 'quote'), $parameters);
  243. // Replace placeholders in the query for vsprintf
  244. $query = str_replace("?", "%s", $query);
  245. // Replace the question marks in the query with the parameters
  246. $bound_query = vsprintf($query, $parameters);
  247. } else {
  248. $bound_query = $query;
  249. }
  250. self::$_last_query = $bound_query;
  251. self::$_query_log[] = $bound_query;
  252. return true;
  253. }
  254. /**
  255. * Get the last query executed. Only works if the
  256. * 'logging' config option is set to true. Otherwise
  257. * this will return null.
  258. */
  259. public static function get_last_query() {
  260. return self::$_last_query;
  261. }
  262. /**
  263. * Get an array containing all the queries run up to
  264. * now. Only works if the 'logging' config option is
  265. * set to true. Otherwise returned array will be empty.
  266. */
  267. public static function get_query_log() {
  268. return self::$_query_log;
  269. }
  270. // ------------------------ //
  271. // --- INSTANCE METHODS --- //
  272. // ------------------------ //
  273. /**
  274. * "Private" constructor; shouldn't be called directly.
  275. * Use the ORM::for_table factory method instead.
  276. */
  277. protected function __construct($table_name, $data=array()) {
  278. $this->_table_name = $table_name;
  279. $this->_data = $data;
  280. }
  281. /**
  282. * Create a new, empty instance of the class. Used
  283. * to add a new row to your database. May optionally
  284. * be passed an associative array of data to populate
  285. * the instance. If so, all fields will be flagged as
  286. * dirty so all will be saved to the database when
  287. * save() is called.
  288. */
  289. public function create($data=null) {
  290. $this->_is_new = true;
  291. if (!is_null($data)) {
  292. return $this->hydrate($data)->force_all_dirty();
  293. }
  294. return $this;
  295. }
  296. /**
  297. * Specify the ID column to use for this instance or array of instances only.
  298. * This overrides the id_column and id_column_overrides settings.
  299. *
  300. * This is mostly useful for libraries built on top of Idiorm, and will
  301. * not normally be used in manually built queries. If you don't know why
  302. * you would want to use this, you should probably just ignore it.
  303. */
  304. public function use_id_column($id_column) {
  305. $this->_instance_id_column = $id_column;
  306. return $this;
  307. }
  308. /**
  309. * Create an ORM instance from the given row (an associative
  310. * array of data fetched from the database)
  311. */
  312. protected function _create_instance_from_row($row) {
  313. $instance = self::for_table($this->_table_name);
  314. $instance->use_id_column($this->_instance_id_column);
  315. $instance->hydrate($row);
  316. return $instance;
  317. }
  318. /**
  319. * Tell the ORM that you are expecting a single result
  320. * back from your query, and execute it. Will return
  321. * a single instance of the ORM class, or false if no
  322. * rows were returned.
  323. * As a shortcut, you may supply an ID as a parameter
  324. * to this method. This will perform a primary key
  325. * lookup on the table.
  326. */
  327. public function find_one($id=null) {
  328. if (!is_null($id)) {
  329. $this->where_id_is($id);
  330. }
  331. $this->limit(1);
  332. $rows = $this->_run();
  333. if (empty($rows)) {
  334. return false;
  335. }
  336. return $this->_create_instance_from_row($rows[0]);
  337. }
  338. /**
  339. * Tell the ORM that you are expecting multiple results
  340. * from your query, and execute it. Will return an array
  341. * of instances of the ORM class, or an empty array if
  342. * no rows were returned.
  343. */
  344. public function find_many() {
  345. $rows = $this->_run();
  346. return array_map(array($this, '_create_instance_from_row'), $rows);
  347. }
  348. /**
  349. * Tell the ORM that you wish to execute a COUNT query.
  350. * Will return an integer representing the number of
  351. * rows returned.
  352. */
  353. public function count() {
  354. $this->select_expr('COUNT(*)', 'count');
  355. $result = $this->find_one();
  356. return ($result !== false && isset($result->count)) ? (int) $result->count : 0;
  357. }
  358. /**
  359. * This method can be called to hydrate (populate) this
  360. * instance of the class from an associative array of data.
  361. * This will usually be called only from inside the class,
  362. * but it's public in case you need to call it directly.
  363. */
  364. public function hydrate($data=array()) {
  365. $this->_data = $data;
  366. return $this;
  367. }
  368. /**
  369. * Force the ORM to flag all the fields in the $data array
  370. * as "dirty" and therefore update them when save() is called.
  371. */
  372. public function force_all_dirty() {
  373. $this->_dirty_fields = $this->_data;
  374. return $this;
  375. }
  376. /**
  377. * Perform a raw query. The query should contain placeholders,
  378. * in either named or question mark style, and the parameters
  379. * should be an array of values which will be bound to the
  380. * placeholders in the query. If this method is called, all
  381. * other query building methods will be ignored.
  382. */
  383. public function raw_query($query, $parameters) {
  384. $this->_is_raw_query = true;
  385. $this->_raw_query = $query;
  386. $this->_raw_parameters = $parameters;
  387. return $this;
  388. }
  389. /**
  390. * Add an alias for the main table to be used in SELECT queries
  391. */
  392. public function table_alias($alias) {
  393. $this->_table_alias = $alias;
  394. return $this;
  395. }
  396. /**
  397. * Internal method to add an unquoted expression to the set
  398. * of columns returned by the SELECT query. The second optional
  399. * argument is the alias to return the expression as.
  400. */
  401. protected function _add_result_column($expr, $alias=null) {
  402. if (!is_null($alias)) {
  403. $expr .= " AS " . $this->_quote_identifier($alias);
  404. }
  405. if ($this->_using_default_result_columns) {
  406. $this->_result_columns = array($expr);
  407. $this->_using_default_result_columns = false;
  408. } else {
  409. $this->_result_columns[] = $expr;
  410. }
  411. return $this;
  412. }
  413. /**
  414. * Add a column to the list of columns returned by the SELECT
  415. * query. This defaults to '*'. The second optional argument is
  416. * the alias to return the column as.
  417. */
  418. public function select($column, $alias=null) {
  419. $column = $this->_quote_identifier($column);
  420. return $this->_add_result_column($column, $alias);
  421. }
  422. /**
  423. * Add an unquoted expression to the list of columns returned
  424. * by the SELECT query. The second optional argument is
  425. * the alias to return the column as.
  426. */
  427. public function select_expr($expr, $alias=null) {
  428. return $this->_add_result_column($expr, $alias);
  429. }
  430. /**
  431. * Add a DISTINCT keyword before the list of columns in the SELECT query
  432. */
  433. public function distinct() {
  434. $this->_distinct = true;
  435. return $this;
  436. }
  437. /**
  438. * Internal method to add a JOIN source to the query.
  439. *
  440. * The join_operator should be one of INNER, LEFT OUTER, CROSS etc - this
  441. * will be prepended to JOIN.
  442. *
  443. * The table should be the name of the table to join to.
  444. *
  445. * The constraint may be either a string or an array with three elements. If it
  446. * is a string, it will be compiled into the query as-is, with no escaping. The
  447. * recommended way to supply the constraint is as an array with three elements:
  448. *
  449. * first_column, operator, second_column
  450. *
  451. * Example: array('user.id', '=', 'profile.user_id')
  452. *
  453. * will compile to
  454. *
  455. * ON `user`.`id` = `profile`.`user_id`
  456. *
  457. * The final (optional) argument specifies an alias for the joined table.
  458. */
  459. protected function _add_join_source($join_operator, $table, $constraint, $table_alias=null) {
  460. $join_operator = trim("{$join_operator} JOIN");
  461. $table = $this->_quote_identifier($table);
  462. // Add table alias if present
  463. if (!is_null($table_alias)) {
  464. $table_alias = $this->_quote_identifier($table_alias);
  465. $table .= " {$table_alias}";
  466. }
  467. // Build the constraint
  468. if (is_array($constraint)) {
  469. list($first_column, $operator, $second_column) = $constraint;
  470. $first_column = $this->_quote_identifier($first_column);
  471. $second_column = $this->_quote_identifier($second_column);
  472. $constraint = "{$first_column} {$operator} {$second_column}";
  473. }
  474. $this->_join_sources[] = "{$join_operator} {$table} ON {$constraint}";
  475. return $this;
  476. }
  477. /**
  478. * Add a simple JOIN source to the query
  479. */
  480. public function join($table, $constraint, $table_alias=null) {
  481. return $this->_add_join_source("", $table, $constraint, $table_alias);
  482. }
  483. /**
  484. * Add an INNER JOIN souce to the query
  485. */
  486. public function inner_join($table, $constraint, $table_alias=null) {
  487. return $this->_add_join_source("INNER", $table, $constraint, $table_alias);
  488. }
  489. /**
  490. * Add a LEFT OUTER JOIN souce to the query
  491. */
  492. public function left_outer_join($table, $constraint, $table_alias=null) {
  493. return $this->_add_join_source("LEFT OUTER", $table, $constraint, $table_alias);
  494. }
  495. /**
  496. * Add an RIGHT OUTER JOIN souce to the query
  497. */
  498. public function right_outer_join($table, $constraint, $table_alias=null) {
  499. return $this->_add_join_source("RIGHT OUTER", $table, $constraint, $table_alias);
  500. }
  501. /**
  502. * Add an FULL OUTER JOIN souce to the query
  503. */
  504. public function full_outer_join($table, $constraint, $table_alias=null) {
  505. return $this->_add_join_source("FULL OUTER", $table, $constraint, $table_alias);
  506. }
  507. /**
  508. * Internal method to add a WHERE condition to the query
  509. */
  510. public function _add_where($fragment, $values=array()) {
  511. if (!is_array($values)) {
  512. $values = array($values);
  513. }
  514. $this->_where_conditions[] = array(
  515. self::WHERE_FRAGMENT => $fragment,
  516. self::WHERE_VALUES => $values,
  517. );
  518. return $this;
  519. }
  520. /**
  521. * Helper method to compile a simple COLUMN SEPARATOR VALUE
  522. * style WHERE condition into a string and value ready to
  523. * be passed to the _add_where method. Avoids duplication
  524. * of the call to _quote_identifier
  525. */
  526. protected function _add_simple_where($column_name, $separator, $value) {
  527. $column_name = $this->_quote_identifier($column_name);
  528. return $this->_add_where("{$column_name} {$separator} ?", $value);
  529. }
  530. /**
  531. * Return a string containing the given number of question marks,
  532. * separated by commas. Eg "?, ?, ?"
  533. */
  534. protected function _create_placeholders($number_of_placeholders) {
  535. return join(", ", array_fill(0, $number_of_placeholders, "?"));
  536. }
  537. /**
  538. * Add a WHERE column = value clause to your query. Each time
  539. * this is called in the chain, an additional WHERE will be
  540. * added, and these will be ANDed together when the final query
  541. * is built.
  542. */
  543. public function where($column_name, $value) {
  544. return $this->where_equal($column_name, $value);
  545. }
  546. /**
  547. * More explicitly named version of for the where() method.
  548. * Can be used if preferred.
  549. */
  550. public function where_equal($column_name, $value) {
  551. return $this->_add_simple_where($column_name, '=', $value);
  552. }
  553. /**
  554. * Add a WHERE column != value clause to your query.
  555. */
  556. public function where_not_equal($column_name, $value) {
  557. return $this->_add_simple_where($column_name, '!=', $value);
  558. }
  559. /**
  560. * Special method to query the table by its primary key
  561. */
  562. public function where_id_is($id) {
  563. return $this->where($this->_get_id_column_name(), $id);
  564. }
  565. /**
  566. * Add a WHERE ... LIKE clause to your query.
  567. */
  568. public function where_like($column_name, $value) {
  569. return $this->_add_simple_where($column_name, 'LIKE', $value);
  570. }
  571. /**
  572. * Add where WHERE ... NOT LIKE clause to your query.
  573. */
  574. public function where_not_like($column_name, $value) {
  575. return $this->_add_simple_where($column_name, 'NOT LIKE', $value);
  576. }
  577. /**
  578. * Add a WHERE ... > clause to your query
  579. */
  580. public function where_gt($column_name, $value) {
  581. return $this->_add_simple_where($column_name, '>', $value);
  582. }
  583. /**
  584. * Add a WHERE ... < clause to your query
  585. */
  586. public function where_lt($column_name, $value) {
  587. return $this->_add_simple_where($column_name, '<', $value);
  588. }
  589. /**
  590. * Add a WHERE ... >= clause to your query
  591. */
  592. public function where_gte($column_name, $value) {
  593. return $this->_add_simple_where($column_name, '>=', $value);
  594. }
  595. /**
  596. * Add a WHERE ... <= clause to your query
  597. */
  598. public function where_lte($column_name, $value) {
  599. return $this->_add_simple_where($column_name, '<=', $value);
  600. }
  601. /**
  602. * Add a WHERE ... IN clause to your query
  603. */
  604. public function where_in($column_name, $values) {
  605. $column_name = $this->_quote_identifier($column_name);
  606. $placeholders = $this->_create_placeholders(count($values));
  607. return $this->_add_where("{$column_name} IN ({$placeholders})", $values);
  608. }
  609. /**
  610. * Add a WHERE ... NOT IN clause to your query
  611. */
  612. public function where_not_in($column_name, $values) {
  613. $column_name = $this->_quote_identifier($column_name);
  614. $placeholders = $this->_create_placeholders(count($values));
  615. return $this->_add_where("{$column_name} NOT IN ({$placeholders})", $values);
  616. }
  617. /**
  618. * Add a WHERE column IS null clause to your query
  619. */
  620. public function where_null($column_name) {
  621. $column_name = $this->_quote_identifier($column_name);
  622. return $this->_add_where("{$column_name} IS null");
  623. }
  624. /**
  625. * Add a WHERE column IS NOT null clause to your query
  626. */
  627. public function where_not_null($column_name) {
  628. $column_name = $this->_quote_identifier($column_name);
  629. return $this->_add_where("{$column_name} IS NOT null");
  630. }
  631. /**
  632. * Add a raw WHERE clause to the query. The clause should
  633. * contain question mark placeholders, which will be bound
  634. * to the parameters supplied in the second argument.
  635. */
  636. public function where_raw($clause, $parameters=array()) {
  637. return $this->_add_where($clause, $parameters);
  638. }
  639. /**
  640. * Add a LIMIT to the query
  641. */
  642. public function limit($limit) {
  643. $this->_limit = $limit;
  644. return $this;
  645. }
  646. /**
  647. * Add an OFFSET to the query
  648. */
  649. public function offset($offset) {
  650. $this->_offset = $offset;
  651. return $this;
  652. }
  653. /**
  654. * Add an ORDER BY clause to the query
  655. */
  656. protected function _add_order_by($column_name, $ordering) {
  657. $column_name = $this->_quote_identifier($column_name);
  658. $this->_order_by[] = "{$column_name} {$ordering}";
  659. return $this;
  660. }
  661. /**
  662. * Add an ORDER BY column DESC clause
  663. */
  664. public function order_by_desc($column_name) {
  665. return $this->_add_order_by($column_name, 'DESC');
  666. }
  667. /**
  668. * Add an ORDER BY column ASC clause
  669. */
  670. public function order_by_asc($column_name) {
  671. return $this->_add_order_by($column_name, 'ASC');
  672. }
  673. /**
  674. * Add a column to the list of columns to GROUP BY
  675. */
  676. public function group_by($column_name) {
  677. $column_name = $this->_quote_identifier($column_name);
  678. $this->_group_by[] = $column_name;
  679. return $this;
  680. }
  681. /**
  682. * Build a SELECT statement based on the clauses that have
  683. * been passed to this instance by chaining method calls.
  684. */
  685. protected function _build_select() {
  686. // If the query is raw, just set the $this->_values to be
  687. // the raw query parameters and return the raw query
  688. if ($this->_is_raw_query) {
  689. $this->_values = $this->_raw_parameters;
  690. return $this->_raw_query;
  691. }
  692. // Build and return the full SELECT statement by concatenating
  693. // the results of calling each separate builder method.
  694. return $this->_join_if_not_empty(" ", array(
  695. $this->_build_select_start(),
  696. $this->_build_join(),
  697. $this->_build_where(),
  698. $this->_build_group_by(),
  699. $this->_build_order_by(),
  700. $this->_build_limit(),
  701. $this->_build_offset(),
  702. ));
  703. }
  704. /**
  705. * Build the start of the SELECT statement
  706. */
  707. protected function _build_select_start() {
  708. $result_columns = join(', ', $this->_result_columns);
  709. if ($this->_distinct) {
  710. $result_columns = 'DISTINCT ' . $result_columns;
  711. }
  712. $fragment = "SELECT {$result_columns} FROM " . $this->_quote_identifier($this->_table_name);
  713. if (!is_null($this->_table_alias)) {
  714. $fragment .= " " . $this->_quote_identifier($this->_table_alias);
  715. }
  716. return $fragment;
  717. }
  718. /**
  719. * Build the JOIN sources
  720. */
  721. protected function _build_join() {
  722. if (count($this->_join_sources) === 0) {
  723. return '';
  724. }
  725. return join(" ", $this->_join_sources);
  726. }
  727. /**
  728. * Build the WHERE clause(s)
  729. */
  730. protected function _build_where() {
  731. // If there are no WHERE clauses, return empty string
  732. if (count($this->_where_conditions) === 0) {
  733. return '';
  734. }
  735. $where_conditions = array();
  736. foreach ($this->_where_conditions as $condition) {
  737. $where_conditions[] = $condition[self::WHERE_FRAGMENT];
  738. $this->_values = array_merge($this->_values, $condition[self::WHERE_VALUES]);
  739. }
  740. return "WHERE " . join(" AND ", $where_conditions);
  741. }
  742. /**
  743. * Build GROUP BY
  744. */
  745. protected function _build_group_by() {
  746. if (count($this->_group_by) === 0) {
  747. return '';
  748. }
  749. return "GROUP BY " . join(", ", $this->_group_by);
  750. }
  751. /**
  752. * Build ORDER BY
  753. */
  754. protected function _build_order_by() {
  755. if (count($this->_order_by) === 0) {
  756. return '';
  757. }
  758. return "ORDER BY " . join(", ", $this->_order_by);
  759. }
  760. /**
  761. * Build LIMIT
  762. */
  763. protected function _build_limit() {
  764. if (!is_null($this->_limit)) {
  765. return "LIMIT " . $this->_limit;
  766. }
  767. return '';
  768. }
  769. /**
  770. * Build OFFSET
  771. */
  772. protected function _build_offset() {
  773. if (!is_null($this->_offset)) {
  774. return "OFFSET " . $this->_offset;
  775. }
  776. return '';
  777. }
  778. /**
  779. * Wrapper around PHP's join function which
  780. * only adds the pieces if they are not empty.
  781. */
  782. protected function _join_if_not_empty($glue, $pieces) {
  783. $filtered_pieces = array();
  784. foreach ($pieces as $piece) {
  785. if (is_string($piece)) {
  786. $piece = trim($piece);
  787. }
  788. if (!empty($piece)) {
  789. $filtered_pieces[] = $piece;
  790. }
  791. }
  792. return join($glue, $filtered_pieces);
  793. }
  794. /**
  795. * Quote a string that is used as an identifier
  796. * (table names, column names etc). This method can
  797. * also deal with dot-separated identifiers eg table.column
  798. */
  799. protected function _quote_identifier($identifier) {
  800. $parts = explode('.', $identifier);
  801. $parts = array_map(array($this, '_quote_identifier_part'), $parts);
  802. return join('.', $parts);
  803. }
  804. /**
  805. * This method performs the actual quoting of a single
  806. * part of an identifier, using the identifier quote
  807. * character specified in the config (or autodetected).
  808. */
  809. protected function _quote_identifier_part($part) {
  810. if ($part === '*') {
  811. return $part;
  812. }
  813. $quote_character = self::$_config['identifier_quote_character'];
  814. return $quote_character . $part . $quote_character;
  815. }
  816. /**
  817. * Create a cache key for the given query and parameters.
  818. */
  819. protected static function _create_cache_key($query, $parameters) {
  820. $parameter_string = join(',', $parameters);
  821. $key = $query . ':' . $parameter_string;
  822. return sha1($key);
  823. }
  824. /**
  825. * Check the query cache for the given cache key. If a value
  826. * is cached for the key, return the value. Otherwise, return false.
  827. */
  828. protected static function _check_query_cache($cache_key) {
  829. if (isset(self::$_query_cache[$cache_key])) {
  830. return self::$_query_cache[$cache_key];
  831. }
  832. return false;
  833. }
  834. /**
  835. * Clear the query cache
  836. */
  837. public static function clear_cache() {
  838. self::$_query_cache = array();
  839. }
  840. /**
  841. * Add the given value to the query cache.
  842. */
  843. protected static function _cache_query_result($cache_key, $value) {
  844. self::$_query_cache[$cache_key] = $value;
  845. }
  846. /**
  847. * Execute the SELECT query that has been built up by chaining methods
  848. * on this class. Return an array of rows as associative arrays.
  849. */
  850. protected function _run() {
  851. $query = $this->_build_select();
  852. $caching_enabled = self::$_config['caching'];
  853. if ($caching_enabled) {
  854. $cache_key = self::_create_cache_key($query, $this->_values);
  855. $cached_result = self::_check_query_cache($cache_key);
  856. if ($cached_result !== false) {
  857. return $cached_result;
  858. }
  859. }
  860. self::_log_query($query, $this->_values);
  861. $statement = self::$_db->prepare($query);
  862. $statement->execute($this->_values);
  863. $rows = array();
  864. while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
  865. $rows[] = $row;
  866. }
  867. if ($caching_enabled) {
  868. self::_cache_query_result($cache_key, $rows);
  869. }
  870. return $rows;
  871. }
  872. /**
  873. * Return the raw data wrapped by this ORM
  874. * instance as an associative array. Column
  875. * names may optionally be supplied as arguments,
  876. * if so, only those keys will be returned.
  877. */
  878. public function as_array() {
  879. if (func_num_args() === 0) {
  880. return $this->_data;
  881. }
  882. $args = func_get_args();
  883. return array_intersect_key($this->_data, array_flip($args));
  884. }
  885. /**
  886. * Return the raw data wrapped by this ORM
  887. * instance as an object instanceof ParameterGroup.
  888. */
  889. public function asGroup() {
  890. if (func_num_args() === 0) {
  891. return new \Sublime\Util\ParameterGroup($this->_data);
  892. }
  893. $args = func_get_args();
  894. return new \Sublime\Util\ParameterGroup(array_intersect_key($this->_data, array_flip($args)));
  895. }
  896. /**
  897. * Return the value of a property of this object (database row)
  898. * or null if not present.
  899. */
  900. public function get($key) {
  901. return isset($this->_data[$key]) ? $this->_data[$key] : null;
  902. }
  903. /**
  904. * Return the name of the column in the database table which contains
  905. * the primary key ID of the row.
  906. */
  907. protected function _get_id_column_name() {
  908. if (!is_null($this->_instance_id_column)) {
  909. return $this->_instance_id_column;
  910. }
  911. if (isset(self::$_config['id_column_overrides'][$this->_table_name])) {
  912. return self::$_config['id_column_overrides'][$this->_table_name];
  913. } else {
  914. return self::$_config['id_column'];
  915. }
  916. }
  917. /**
  918. * Get the primary key ID of this object.
  919. */
  920. public function id() {
  921. return $this->get($this->_get_id_column_name());
  922. }
  923. /**
  924. * Set a property to a particular value on this object.
  925. * Flags that property as 'dirty' so it will be saved to the
  926. * database when save() is called.
  927. */
  928. public function set($key, $value) {
  929. $this->_data[$key] = $value;
  930. $this->_dirty_fields[$key] = $value;
  931. }
  932. /**
  933. * Check whether the given field has been changed since this
  934. * object was saved.
  935. */
  936. public function is_dirty($key) {
  937. return isset($this->_dirty_fields[$key]);
  938. }
  939. /**
  940. * Save any fields which have been modified on this object
  941. * to the database.
  942. */
  943. public function save() {
  944. $query = array();
  945. $values = array_values($this->_dirty_fields);
  946. if (!$this->_is_new) { // UPDATE
  947. // If there are no dirty values, do nothing
  948. if (count($values) == 0) {
  949. return true;
  950. }
  951. $query = $this->_build_update();
  952. $values[] = $this->id();
  953. } else { // INSERT
  954. $query = $this->_build_insert();
  955. }
  956. self::_log_query($query, $values);
  957. $statement = self::$_db->prepare($query);
  958. $success = $statement->execute($values);
  959. // If we've just inserted a new record, set the ID of this object
  960. if ($this->_is_new) {
  961. $this->_is_new = false;
  962. if (is_null($this->id())) {
  963. $this->_data[$this->_get_id_column_name()] = self::$_db->lastInsertId();
  964. }
  965. }
  966. $this->_dirty_fields = array();
  967. return $success;
  968. }
  969. /**
  970. * Build an UPDATE query
  971. */
  972. protected function _build_update() {
  973. $query = array();
  974. $query[] = "UPDATE {$this->_quote_identifier($this->_table_name)} SET";
  975. $field_list = array();
  976. foreach ($this->_dirty_fields as $key => $value) {
  977. $field_list[] = "{$this->_quote_identifier($key)} = ?";
  978. }
  979. $query[] = join(", ", $field_list);
  980. $query[] = "WHERE";
  981. $query[] = $this->_quote_identifier($this->_get_id_column_name());
  982. $query[] = "= ?";
  983. return join(" ", $query);
  984. }
  985. /**
  986. * Build an INSERT query
  987. */
  988. protected function _build_insert() {
  989. $query[] = "INSERT INTO";
  990. $query[] = $this->_quote_identifier($this->_table_name);
  991. $field_list = array_map(array($this, '_quote_identifier'), array_keys($this->_dirty_fields));
  992. $query[] = "(" . join(", ", $field_list) . ")";
  993. $query[] = "VALUES";
  994. $placeholders = $this->_create_placeholders(count($this->_dirty_fields));
  995. $query[] = "({$placeholders})";
  996. return join(" ", $query);
  997. }
  998. /**
  999. * Delete this record from the database
  1000. */
  1001. public function delete() {
  1002. $query = join(" ", array(
  1003. "DELETE FROM",
  1004. $this->_quote_identifier($this->_table_name),
  1005. "WHERE",
  1006. $this->_quote_identifier($this->_get_id_column_name()),
  1007. "= ?",
  1008. ));
  1009. $params = array($this->id());
  1010. self::_log_query($query, $params);
  1011. $statement = self::$_db->prepare($query);
  1012. return $statement->execute($params);
  1013. }
  1014. /**
  1015. * @patch 2012-03-06 chrsm
  1016. * Resets the model/instance.
  1017. */
  1018. protected function reset() {
  1019. foreach($this->_resets as $k => $resetVal) {
  1020. $this->$k = $resetVal;
  1021. }
  1022. }
  1023. // --------------------- //
  1024. // --- MAGIC METHODS --- //
  1025. // --------------------- //
  1026. public function __get($key) {
  1027. return $this->get($key);
  1028. }
  1029. public function __set($key, $value) {
  1030. $this->set($key, $value);
  1031. }
  1032. public function __isset($key) {
  1033. return isset($this->_data[$key]);
  1034. }
  1035. }