PageRenderTime 81ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/firefly/active_record.php

https://github.com/yuweijun/blog
PHP | 2045 lines | 1422 code | 180 blank | 443 comment | 247 complexity | 13dc95f5ed824928a95587efa4975fa0 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. abstract class ActiveRecord {
  3. /**
  4. * database connection.
  5. */
  6. private static $connection = null;
  7. /**
  8. * database connection configurations.
  9. */
  10. private static $configurations = array();
  11. /**
  12. * cache active record objects.
  13. * first level cache.
  14. */
  15. private static $cached_active_records = array();
  16. /**
  17. * cache all table names for all models.
  18. */
  19. private static $cached_table_names = array();
  20. /**
  21. * cache all table columns info.
  22. */
  23. private static $cached_table_columns = array();
  24. /**
  25. * for caching static properties of models.
  26. */
  27. private static $cached_static_properties_of_models = array();
  28. /**
  29. * cache ancestor classes of models.
  30. */
  31. private static $cached_model_ancestors = array();
  32. /**
  33. * cache associations by model name.
  34. */
  35. private static $cached_associations = array();
  36. /**
  37. * cache associations keys by model name.
  38. */
  39. private static $cached_association_keys = array();
  40. /**
  41. * for nested transactions.
  42. */
  43. private static $transaction_started = false;
  44. /**
  45. * default table name is pluralized version of class name.
  46. */
  47. protected static $table_name = null;
  48. /**
  49. * table name prefix, can be overrided by subclasses.
  50. */
  51. protected static $table_name_prefix = '';
  52. /**
  53. * table name suffix, can be overrided by subclasses.
  54. */
  55. protected static $table_name_suffix = '';
  56. /**
  57. * defines the primary key field -- can be overridden in subclasses.
  58. * using logical primary key, don't using compsite id.
  59. */
  60. protected static $primary_key = 'id';
  61. /**
  62. * Sets the name of the sequence class to use when generating ids to the given value.
  63. * This is required for Oracle and is useful for any database which relies on sequences for primary key generation.
  64. */
  65. protected static $sequence_class_name = null;
  66. /**
  67. * has one association configure.
  68. */
  69. protected static $has_one = array();
  70. /**
  71. * has many assocition configure.
  72. */
  73. protected static $has_many = array();
  74. /**
  75. * belongs to association configure.
  76. */
  77. protected static $belongs_to = array();
  78. /**
  79. * attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards.
  80. */
  81. public static $readonly_attributes = array();
  82. public static function establish_connection($config) {
  83. self::validate_connection_params($config);
  84. $adapter = strtolower($config['adapter']);
  85. if ($adapter == 'mysqli') {
  86. $adapter = 'mysqlimprovement';
  87. }
  88. require_once('database_adapters' . DS . $adapter . '.php');
  89. return call_user_func(array($adapter, 'establish_connection'), $config['host'], $config['username'], $config['password'], $config['database'], $config['port'], $config['encoding']);
  90. }
  91. public static function get_connection() {
  92. if(self::$connection == null) {
  93. if(self::$configurations == null) {
  94. self::load_configurations();
  95. }
  96. self::$connection = self::establish_connection(self::$configurations);
  97. }
  98. return self::$connection;
  99. }
  100. /**
  101. * Find method first parameter may be:
  102. * 1. id.
  103. * 2. ids array.
  104. * 3. 'all'.
  105. * 4. 'first'.
  106. * 5. 'last'.
  107. *
  108. * This method second parameter options keys list
  109. * 1. select: sql select fields list.
  110. * 2. joins: join tables.
  111. * 3. confitions(where): sql conditions setting.
  112. * 4. group:
  113. * 5. having:
  114. * 6. order:
  115. * 7. limit:
  116. * 8. offset:
  117. * 9. lock: string such as 'LOCK IN SHARE MODE' or true ('for update').
  118. * 10. include: for preload configed association models.
  119. * include option avoid 1 + N sql problem using LEFT OUTER JOIN the included models.
  120. * include option can not be used to include self-join association.
  121. * include option will ignore 'select' and 'joins' in $options.
  122. */
  123. public static function find($ids, $options = array()) {
  124. self::validate_find_options($ids, $options);
  125. $model_name = self::get_model_name();
  126. switch($ids) {
  127. case 'first' :
  128. return self::find_first($model_name, $options);
  129. case 'last' :
  130. return self::find_last($model_name, $options);
  131. case 'all' :
  132. return self::find_all($model_name, $options);
  133. default :
  134. return self::find_from_ids($model_name, $ids, $options);
  135. }
  136. }
  137. /**
  138. * A convenience wrapper for find('first', $options).
  139. * If no record found will return false, not return null for sake association lazy load.
  140. */
  141. public static function first($options = array()) {
  142. return self::find('first', $options);
  143. }
  144. /**
  145. * A convenience wrapper for find('last', $options).
  146. * If no record found will return false, not return null for sake association lazy load.
  147. */
  148. public static function last($options = array()) {
  149. return self::find('last', $options);
  150. }
  151. /**
  152. * A convenience wrapper for find('all', $options).
  153. * If no record found will return false, not return null for sake association lazy load.
  154. */
  155. public static function all($options = array()) {
  156. return self::find('all', $options);
  157. }
  158. /**
  159. * This is an alias for find('all', $options).
  160. */
  161. public static function select($options = array()) {
  162. return self::all($options);
  163. }
  164. /**
  165. * Find results using complicated sql, Usally for select database operation.
  166. * Don't use this method to update or delete operation.
  167. * If $direct is true, will return query resultset by sql directly.
  168. */
  169. public static function find_by_sql($sql, $direct = false) {
  170. $records = self::fetch_rows($sql);
  171. if($direct) {
  172. return $records;
  173. }
  174. $model_name = self::get_model_name();
  175. return self::get_model_objects($model_name, $records);
  176. }
  177. /**
  178. * Create a new active record object using giving $attributes.
  179. * return false object if create failure.
  180. */
  181. public static function create($attributes) {
  182. $model_name = self::get_model_name();
  183. $object = new $model_name($attributes);
  184. if($object->save()) {
  185. return $object;
  186. } else {
  187. return false;
  188. }
  189. }
  190. /**
  191. * delete rows by id or array of ids.
  192. * callbacks will run such as before_destroy and after_destroy.
  193. */
  194. public static function delete($ids) {
  195. $objects = self::find($ids);
  196. if(is_array($objects)) {
  197. $results = array();
  198. foreach($objects as $object) {
  199. $results[] = $object->destroy();
  200. }
  201. return $results;
  202. } else {
  203. $result = $objects->destroy();
  204. return $result;
  205. }
  206. }
  207. public static function update($id, $attributes) {
  208. $object = self::find($id);
  209. return $object->update_attributes($attributes);
  210. }
  211. /**
  212. * $options is hash array and with keys:
  213. * 1. conditions
  214. * 2. order
  215. * 3. limit
  216. */
  217. public static function delete_all($options = array()) {
  218. $model_name = self::get_model_name();
  219. $sql = self::construct_delete_sql($model_name, $options);
  220. self::transaction_start();
  221. $result = self::get_connection()->delete($sql);
  222. self::transaction_commit();
  223. self::remove_objects_from_caches($model_name, $options);
  224. return $result;
  225. }
  226. public static function update_all($updates, $options = array()) {
  227. $model_name = self::get_model_name();
  228. $sql = self::construct_update_sql($model_name, $updates, $options);
  229. self::transaction_start();
  230. $result = self::get_connection()->update($sql);
  231. self::transaction_commit();
  232. return $result;
  233. }
  234. /**
  235. * The third approach, count using options, accepts an option hash as the only parameter.
  236. */
  237. public static function count($options = array()) {
  238. $c = self::first(array_merge($options, array('select' => 'count(*) rows')));
  239. return $c->rows;
  240. }
  241. /**
  242. * auto increament counter, default auto add 1 to $column_name.
  243. * if $step is set, then add $step to $column_name.
  244. */
  245. public static function counter($id, $column_name, $step = 1) {
  246. $model_name = self::get_model_name();
  247. if(isset(self::$cached_active_records[$model_name]) && isset(self::$cached_active_records[$model_name][$id])) {
  248. $object = self::$cached_active_records[$model_name][$id];
  249. $object->attributes[$column_name] = $object->attributes[$column_name] + $step;
  250. $object->save();
  251. return true;
  252. } else {
  253. $quoted_table_name = self::quoted_table_name($model_name);
  254. $quoted_primary_key = self::quoted_primary_key($model_name);
  255. $quoted_column_name = self::quote_column_name($column_name);
  256. $sql = "UPDATE " . $quoted_table_name . " SET " . $quoted_column_name . " = " . $quoted_column_name . " + " . $step . " WHERE " . $quoted_primary_key . " = " . $id;
  257. self::transaction_start();
  258. $result = self::get_connection()->update($sql);
  259. self::transaction_commit();
  260. return $result;
  261. }
  262. }
  263. public static function avg($column_name, $options = array()) {
  264. $quoted_column_name = self::quote_column_name($column_name);
  265. $avg = self::first(array_merge($options, array("select" => "AVG($quoted_column_name) AS average")));
  266. return $avg->average;
  267. }
  268. public static function min($column_name, $options = array()) {
  269. $quoted_column_name = self::quote_column_name($column_name);
  270. $min = self::first(array_merge($options, array("select" => "MIN($quoted_column_name) AS minimum")));
  271. return $min->minimum;
  272. }
  273. public static function max($column_name, $options = array()) {
  274. $quoted_column_name = self::quote_column_name($column_name);
  275. $max = self::first(array_merge($options, array("select" => "MAX($quoted_column_name) AS maximum")));
  276. return $max->maximum;
  277. }
  278. public static function sum($column_name, $options = array()) {
  279. $quoted_column_name = self::quote_column_name($column_name);
  280. $sum = self::first(array_merge($options, array("select" => "SUM($quoted_column_name) AS summary")));
  281. return $sum->summary;
  282. }
  283. /**
  284. * default $exclusive is false, remove object which does not match $conditions hash from $objects.
  285. * if $exclusive is true, then remove object which match $conditions hash from $objects.
  286. * return remain objects.
  287. */
  288. public static function filter($objects, $conditions, $exclusive = false) {
  289. $results = array();
  290. foreach($objects as $object) {
  291. $matched = true;
  292. foreach($conditions as $key => $value) {
  293. if($object->attributes[$key] !== $value) {
  294. $matched = false;
  295. break;
  296. }
  297. }
  298. if($matched) {
  299. $results[] = $object;
  300. }
  301. }
  302. if($exclusive) {
  303. return array_diff($objects, $results);
  304. } else {
  305. return $results;
  306. }
  307. }
  308. /**
  309. * if $all_models is true, will clear cache all, otherwise, only clear caches of target model.
  310. */
  311. public static function clear_cache() {
  312. self::$cached_active_records = array();
  313. }
  314. public static function clear_model_cache() {
  315. $model_name = self::get_model_name();
  316. self::$cached_active_records[$model_name] = array();
  317. }
  318. public static function get_table_name() {
  319. $model_name = self::get_model_name();
  320. return self::get_table_name_by_model($model_name);
  321. }
  322. public static function get_primary_key() {
  323. $model_name = self::get_model_name();
  324. return self::get_primary_key_by_model($model_name);
  325. }
  326. /**
  327. * Returns an array of columns for the table associated with this class.
  328. */
  329. public static function get_columns() {
  330. $model_name = self::get_model_name();
  331. return self::columns($model_name);
  332. }
  333. /**
  334. * Returns a string like 'ID:bigint(20) unsigned, post_author:bigint(20), post_date:datetime'.
  335. */
  336. public static function inspect() {
  337. $columns = self::get_columns();
  338. $desc = array();
  339. foreach($columns as $column) {
  340. $desc[] = $column[0] . ':' . $column[1];
  341. }
  342. return implode(', ', $desc);
  343. }
  344. /**
  345. * Transaction support for database operations.
  346. * If revoke this static method manually, should revoke static method
  347. * transaction_commit() or transation_rollback() manually to commit or rollback transactions.
  348. */
  349. public static function transaction_start() {
  350. if(self::$transaction_started === false) {
  351. self::$transaction_started = true;
  352. self::get_connection()->transaction_start();
  353. self::do_action('transaction_start');
  354. }
  355. }
  356. public static function transaction_commit() {
  357. if(self::$transaction_started) {
  358. self::$transaction_started = false;
  359. self::get_connection()->transaction_commit();
  360. self::do_action('transaction_commit');
  361. }
  362. }
  363. public static function transaction_rollback() {
  364. if(self::$transaction_started) {
  365. self::$transaction_started = false;
  366. self::get_connection()->transaction_rollback();
  367. self::do_action('transaction_rollback');
  368. }
  369. }
  370. // ======= ActiveRecord private static methods =======
  371. private static function quote_column_name($column_name) {
  372. return self::get_connection()->quote_column_name($column_name);
  373. }
  374. private static function quote_table_name($table_name) {
  375. return self::get_connection()->quote_table_name($table_name);
  376. }
  377. private static function quoted_primary_key($model_name) {
  378. return self::quote_column_name(self::get_primary_key_by_model($model_name));
  379. }
  380. private static function quoted_table_name($model_name) {
  381. return self::quote_table_name(self::get_table_name_by_model($model_name));
  382. }
  383. private static function quote_column_value($value) {
  384. if(is_numeric($value)) {
  385. return $value;
  386. }
  387. elseif(is_null($value)) {
  388. return 'NULL';
  389. } else {
  390. return "'" . self::escape_string($value) . "'";
  391. }
  392. }
  393. /**
  394. * prevent from sql injection.
  395. */
  396. private static function escape_string($value) {
  397. if(!is_string($value)) {
  398. return $value;
  399. } else {
  400. // php get_magic_quotes_gpc() function default return true.
  401. if(get_magic_quotes_gpc()) {
  402. $value = stripslashes($value);
  403. }
  404. // for mysqli
  405. if(function_exists('mysqli_real_escape_string')) {
  406. return self::get_connection()->real_escape_string($value);
  407. }
  408. // for mysql
  409. if(function_exists('mysql_real_escape_string')) {
  410. return self::get_connection()->real_escape_string($value);
  411. }
  412. if(function_exists('pg_escape_string')) {
  413. return pg_escape_string($value);
  414. }
  415. return addslashes($value);
  416. }
  417. }
  418. /**
  419. * Used to sanitize objects before they're used in an SELECT SQL statement.
  420. */
  421. private static function quote_value_for_conditions($column, $value) {
  422. $column = self::quote_column_name($column);
  423. if(is_null($value)) {
  424. return $column . " IS NULL";
  425. }
  426. elseif(is_array($value)) {
  427. $value = array_unique($value);
  428. $size = count($value);
  429. if($size == 0) {
  430. throw new FireflyException("The second parameter can not be an empty array!");
  431. }
  432. elseif($size == 1) {
  433. return $column . " = " . self::quote_column_value($value[0]);
  434. } else {
  435. return $column . " IN ('" . implode("', '", $value) . "')";
  436. }
  437. } else {
  438. // is_string || is_numeric
  439. return $column . " = " . self::quote_column_value($value);
  440. }
  441. }
  442. /**
  443. * Accepts an array, hash or string of SQL conditions and sanitizes them into a valid SQL fragment for a WHERE clause.
  444. * array("name=%s and group_id=%s", "foo'bar", 4) returns "name='foo''bar' and group_id='4'"
  445. * array("name"=>"foo'bar", "group_id"=>4) returns "name='foo\'bar' and group_id='4'"
  446. * "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
  447. */
  448. private static function sanitize($object) {
  449. if(is_array($object)) {
  450. if(isset($object[0])) {
  451. // for array("name=%s and group_id=%d", "foo'bar", 4) and array("name=? and group_id=?", "foo'bar", 4)
  452. $object[0] = str_replace('?', '%s', $object[0]);
  453. $sets = array($object[0]);
  454. $len = count($object);
  455. for($i = 1; $i < $len; $i++) {
  456. $sets[] = self::quote_column_value($object[$i]);
  457. }
  458. return call_user_func_array('sprintf', $sets);
  459. } else {
  460. // for array("name"=>"foo'bar", "group_id"=>4)
  461. $where = array();
  462. foreach($object as $key => $value) {
  463. $where[] = self::quote_value_for_conditions($key, $value);
  464. }
  465. return implode(' AND ', $where);
  466. }
  467. } else {
  468. return $object;
  469. }
  470. }
  471. private static function sanitize_conditions($options) {
  472. return self::sanitize($options);
  473. }
  474. /**
  475. * quote value for UPDATE SQL.
  476. */
  477. private static function sanitize_assignment($updates) {
  478. $sets = array();
  479. foreach($updates as $key => $value) {
  480. $column = self::quote_column_name($key);
  481. $sets[] = $column . '=' . self::quote_column_value($value);
  482. }
  483. return implode(', ', $sets);
  484. }
  485. /**
  486. * Validate find method options.
  487. */
  488. private static function validate_find_options($ids, $options) {
  489. if(!is_string($ids) && !is_array($ids) && !is_numeric($ids)) {
  490. throw new FireflyException("First parameter of find method can not be parsed: " . var_export($ids, true));
  491. }
  492. if(!is_array($options)) {
  493. throw new FireflyException("Second parameter of find method expected be Array but " . var_export($options, true));
  494. }
  495. }
  496. /**
  497. * Validate database connection parameters.
  498. */
  499. private static function validate_connection_params($config) {
  500. if(empty($config['adapter'])) {
  501. throw new FireflyException("Unkown database adapter.");
  502. }
  503. if(empty($config['host'])) {
  504. throw new FireflyException("Unkown database host.");
  505. }
  506. if(empty($config['database'])) {
  507. throw new FireflyException("Unkown database name.");
  508. }
  509. if(!isset($config['username'])) {
  510. throw new FireflyException("Unkown database username.");
  511. }
  512. if(!isset($config['password'])) {
  513. throw new FireflyException("Unkown database password.");
  514. }
  515. }
  516. private static function cache_active_record_object($model_name, $id, $object) {
  517. self::$cached_active_records[$model_name][$id] = $object;
  518. }
  519. private static function construct_finder_sql($model_name, $options) {
  520. $table_name = self::quoted_table_name($model_name);
  521. $sql = "SELECT ";
  522. $sql .= self::add_select_fields($table_name, $options);
  523. $sql .= self::add_from_tables($table_name, $options);
  524. $sql .= self::add_joins($options);
  525. $sql .= self::add_conditions($options);
  526. $sql .= self::append_sql_options($options);
  527. return $sql;
  528. }
  529. private static function construct_finder_sql_with_associations($model_name, $options, $include_associations) {
  530. $quoted_table_name = self::quoted_table_name($model_name);
  531. $sql = self::add_select_and_joins_with_associations($model_name, $quoted_table_name, $options, $include_associations);
  532. // NOTICE: columns in select/where clause maybe ambiguous, should explicit those columns in options.
  533. $sql .= self::add_conditions_with_eager_load($model_name, $quoted_table_name, $options, $include_associations);
  534. $sql .= self::append_sql_options($options);
  535. return $sql;
  536. }
  537. private static function construct_update_sql($model_name, $updates, $options) {
  538. $table_name = self::quoted_table_name($model_name);
  539. $sets = self::sanitize_assignment($updates);
  540. $sql = "UPDATE " . $table_name . " SET " . $sets;
  541. $sql .= self::add_conditions($options);
  542. $sql .= self::add_order($options);
  543. $sql .= self::add_limit_without_offset($options);
  544. return $sql;
  545. }
  546. private static function construct_delete_sql($model_name, $options = array()) {
  547. $table_name = self::quoted_table_name($model_name);
  548. $sql = "DELETE FROM " . $table_name;
  549. $sql .= self::add_conditions($options);
  550. $sql .= self::add_order($options);
  551. $sql .= self::add_limit_without_offset($options);
  552. return $sql;
  553. }
  554. private static function find_first($model_name, $options = array()) {
  555. $options = array_merge($options, array('limit' => 1));
  556. $records = self::find_every($model_name, $options);
  557. if(!empty($records)) {
  558. return $records[0];
  559. } else {
  560. return false;
  561. }
  562. }
  563. private static function find_last($model_name, $options = array()) {
  564. if(isset($options['order'])) {
  565. $options['order'] = self::reverse_sql_order($options['order']);
  566. } else {
  567. $quoted_primary_key = self::quoted_primary_key($model_name);
  568. $options['order'] = "$quoted_primary_key DESC";
  569. }
  570. return self::find_first($model_name, $options);
  571. }
  572. private static function find_all($model_name, $options = array()) {
  573. return self::find_every($model_name, $options);
  574. }
  575. private static function find_from_ids($model_name, $ids, $options = array()) {
  576. if(is_array($ids)) {
  577. $ids = array_unique($ids);
  578. $size = count($ids);
  579. if($size == 0) {
  580. throw new FireflyException("Couldn't find Record without an id.");
  581. }
  582. elseif($size == 1) {
  583. $object = self::find_one($model_name, $ids[0], $options);
  584. if($object) {
  585. return array($object);
  586. } else {
  587. return array();
  588. }
  589. } else {
  590. return self::find_some($model_name, $ids, $options);
  591. }
  592. } else {
  593. return self::find_one($model_name, $ids, $options);
  594. }
  595. }
  596. private static function find_some($model_name, $ids, $options = array()) {
  597. $ids_str = "'" . implode("', '", $ids) . "'";
  598. $quoted_primary_key = self::quoted_primary_key($model_name);
  599. if(isset($options['joins']) || isset($options['include'])) {
  600. // prevent from ambiguous columns
  601. $quoted_table_name = self::quoted_table_name($model_name);
  602. $quoted_primary_key = $quoted_table_name . "." . $quoted_primary_key;
  603. }
  604. $ids_list = $quoted_primary_key . " IN (" . $ids_str . ")";
  605. if(isset($options['conditions'])) {
  606. $options['conditions'] = self::sanitize_conditions($options['conditions']) . " AND $ids_list";
  607. } else {
  608. $options['conditions'] = $ids_list;
  609. }
  610. return self::find_every($model_name, $options);
  611. }
  612. private static function find_one($model_name, $id, $options = array()) {
  613. if(is_null($id)) {
  614. return false;
  615. }
  616. $cached_object = self::find_object_from_caches($model_name, $id);
  617. if(!is_null($cached_object)) {
  618. return $cached_object;
  619. }
  620. $id = self::quote_column_value($id);
  621. $quoted_primary_key = self::quoted_primary_key($model_name);
  622. if(isset($options['joins']) || isset($options['include'])) {
  623. // prevent from ambiguous columns
  624. $quoted_table_name = self::quoted_table_name($model_name);
  625. $conditions = "$quoted_table_name.$quoted_primary_key = " . $id;
  626. } else {
  627. $conditions = "$quoted_primary_key = " . $id;
  628. }
  629. $options['conditions'] = $conditions;
  630. $objects = self::find_every($model_name, $options);
  631. if(!empty($objects)) {
  632. return $objects[0];
  633. } else {
  634. throw new FireflyException("Couldn't find record with id=" . $id);
  635. }
  636. }
  637. private static function find_every($model_name, $options) {
  638. $include_associations = self::with_eager_load_associations($model_name, $options);
  639. if($include_associations) {
  640. return self::find_with_eager_load_associations($model_name, $options, $include_associations);
  641. }
  642. $cache = self::get_cache_from_options($model_name, $options);
  643. $sql = self::construct_finder_sql($model_name, $options);
  644. $records = self::fetch_rows($sql);
  645. $active_record_objects = array();
  646. $primary_key = self::get_primary_key_by_model($model_name);
  647. foreach($records as $record) {
  648. $object = self::get_model_object($model_name, $primary_key, $record, $cache);
  649. if($cache) {
  650. // cache active record object with all properties found without select option.
  651. $id = $object->id();
  652. self::cache_active_record_object($model_name, $id, $object);
  653. }
  654. $active_record_objects[] = $object;
  655. }
  656. return $active_record_objects;
  657. }
  658. private static function find_object_from_caches($model_name, $id) {
  659. // first level cache, optimize performance of has_many and belongs_to query.
  660. if(!isset(self::$cached_active_records[$model_name])) {
  661. self::$cached_active_records[$model_name] = array();
  662. }
  663. if(isset(self::$cached_active_records[$model_name][$id])) {
  664. return self::$cached_active_records[$model_name][$id];
  665. }
  666. return null;
  667. }
  668. /**
  669. * delete model object from active record object caches.
  670. */
  671. private static function remove_objects_from_caches($model_name, $options) {
  672. $conditions = isset($options['conditions']) ? $options['conditions'] : array();
  673. if(isset(self::$cached_active_records[$model_name])) {
  674. self::$cached_active_records[$model_name] = self::filter(self::$cached_active_records[$model_name], $conditions, true);
  675. }
  676. }
  677. private static function find_with_eager_load_associations($model_name, $options, $include_associations) {
  678. $sql = self::construct_finder_sql_with_associations($model_name, $options, $include_associations);
  679. $records = self::fetch_rows($sql);
  680. return self::get_eager_load_objects($model_name, $records, $include_associations);
  681. }
  682. private static function get_readonly_attributes($model_name) {
  683. $props = self::get_model_static_properties($model_name);
  684. return array_merge($props['readonly_attributes'], array($props['primary_key']));
  685. }
  686. private static function get_configuration_file() {
  687. defined('DATABASE_CONFIG_FILE') ? null : define('DATABASE_CONFIG_FILE', FIREFLY_BASE_DIR . DS . 'config' . DS . 'database.php');
  688. return DATABASE_CONFIG_FILE;
  689. }
  690. /**
  691. * Hack for php, get ActiveRecord subclass name.
  692. */
  693. private static function get_model_name() {
  694. // php >= 5.3.0
  695. if (function_exists('get_called_class')) {
  696. return strtolower(get_called_class());
  697. }
  698. // foreach $trace to get static method revoking model name.
  699. $traces = debug_backtrace();
  700. foreach($traces as $key => $trace) {
  701. if($trace['file'] != __FILE__) {
  702. $lines = file($trace['file']);
  703. $line = $lines[$trace['line'] - 1];
  704. preg_match('/\w+(?=\s*::\s*\w+)/i', $line, $matches);
  705. if(empty($matches)) {
  706. // for hacking php and get subclass name of active record.
  707. throw new FireflyException("Please write code in one line style nearby line " . $trace['line']);
  708. } else {
  709. // all $model_name using lower name in this program.
  710. return strtolower($matches[0]);
  711. }
  712. }
  713. }
  714. }
  715. private static function get_model_static_properties($model_name) {
  716. if(!isset(self::$cached_static_properties_of_models[$model_name])) {
  717. $class = new ReflectionClass($model_name);
  718. self::$cached_static_properties_of_models[$model_name] = $class->getStaticProperties();
  719. }
  720. return self::$cached_static_properties_of_models[$model_name];
  721. }
  722. private static function get_table_name_by_model($model_name) {
  723. if(!isset(self::$cached_table_names[$model_name])) {
  724. $props = self::get_model_static_properties($model_name);
  725. if(is_null($props['table_name']) || $props['table_name'] == '') {
  726. $table_name = $model_name;
  727. } else {
  728. $table_name = $props['table_name'];
  729. }
  730. self::$cached_table_names[$model_name] = $props['table_name_prefix'] . $table_name . $props['table_name_suffix'];
  731. }
  732. return self::$cached_table_names[$model_name];
  733. }
  734. private static function get_primary_key_by_model($model_name) {
  735. $props = self::get_model_static_properties($model_name);
  736. return $props['primary_key'];
  737. }
  738. private static function get_ancestor_classes($class) {
  739. if(empty(self::$cached_model_ancestors[$class])) {
  740. $classes = array($class);
  741. while($class = get_parent_class($class)) {
  742. $classes[] = $class;
  743. }
  744. self::$cached_model_ancestors[$class] = $classes;
  745. }
  746. return self::$cached_model_ancestors[$class];
  747. }
  748. /**
  749. * only cache full properties of model object.
  750. */
  751. private static function get_cache_from_options($model_name, $options) {
  752. if(empty($options['select'])) {
  753. return true;
  754. }
  755. $table_name = self::get_table_name_by_model($model_name);
  756. $quoted_table_name = self::quote_table_name($table_name);
  757. if($options['select'] == $table_name . '.*' || $options['select'] == $quoted_table_name . '.*') {
  758. return true;
  759. }
  760. return false;
  761. }
  762. /**
  763. * map eager load resultset to associated model objects.
  764. */
  765. private static function get_eager_load_objects($model_name, $records, $include_associations) {
  766. $objects = array();
  767. $null_assoc_records = array();
  768. foreach($records as $record) {
  769. $table_index = 0;
  770. $object = self::get_model_object_by_record($model_name, $record, $table_index);
  771. $id = $object->id();
  772. $null_assoc_records[$id] = array('has_many' => array(), 'has_one' => array(), 'belongs_to' => array());
  773. foreach($include_associations as $assoc_type => $associations) {
  774. foreach ($associations as $association_id => $association) {
  775. if(!isset($null_assoc_records[$id][$assoc_type][$association_id])) {
  776. $null_assoc_records[$id][$assoc_type][$association_id] = false;
  777. }
  778. if($null_assoc_records[$id][$assoc_type][$association_id]) {
  779. // for performance, preventing from creating null object.
  780. continue;
  781. }
  782. $table_index++;
  783. $assoc_model_name = self::get_association_model_name($assoc_type, $association_id, $association);
  784. $assoc_quoted_primary_key = self::quoted_primary_key($assoc_model_name);
  785. if($assoc_type == 'has_many') {
  786. // has_many and many_to_many association object initialize.
  787. if(!isset($object->attributes[$association_id])) {
  788. $object->attributes[$association_id] = array();
  789. }
  790. $assoc_object = self::get_model_object_by_record($assoc_model_name, $record, $table_index);
  791. if($assoc_object) {
  792. // avoid push array $object->attributes[$association_id] the same object repeatly using unique primary id.
  793. $assoc_object_id = $assoc_object->id();
  794. $object->attributes[$association_id][$assoc_object_id] = $assoc_object;
  795. self::cache_active_record_object($assoc_model_name, $assoc_object_id, $assoc_object);
  796. } else {
  797. // begin match null record and ignore other rows for current $object.
  798. $null_assoc_records[$id][$assoc_type][$association_id] = true;
  799. }
  800. } else {
  801. // has_one and belongs_to associations.
  802. if(!isset($object->attributes[$association_id])) {
  803. $assoc_object = self::get_model_object_by_record($assoc_model_name, $record, $table_index);
  804. if($assoc_object) {
  805. $assoc_object_id = $assoc_object->id();
  806. // $object primary key should not be null.
  807. $object->attributes[$association_id] = $assoc_object;
  808. self::cache_active_record_object($assoc_model_name, $assoc_object_id, $assoc_object);
  809. } else {
  810. // begin match null record and ignore other rows for current $object.
  811. $null_assoc_records[$id][$assoc_type][$association_id] = true;
  812. }
  813. }
  814. }
  815. }
  816. }
  817. // update $object cache according to $object primary key.
  818. $objects[$id] = $object;
  819. self::cache_active_record_object($model_name, $id, $object);
  820. }
  821. foreach($include_associations['has_many'] as $association_id => $association) {
  822. foreach($objects as $object) {
  823. $object->attributes[$association_id] = array_values($object->attributes[$association_id]);
  824. }
  825. }
  826. return array_values($objects);
  827. }
  828. private static function get_model_objects($model_name, $records) {
  829. $active_record_objects = array();
  830. $primary_key = self::get_primary_key_by_model($model_name);
  831. foreach($records as $record) {
  832. $active_record_objects[] = self::get_model_object($model_name, $primary_key, $record);
  833. }
  834. return $active_record_objects;
  835. }
  836. private static function get_model_object($model_name, $primary_key, $attributes, $cache = false) {
  837. $object = null;
  838. if($cache) {
  839. $id = $attributes[$primary_key];
  840. $object = self::find_object_from_caches($model_name, $id);
  841. }
  842. if(is_null($object)) {
  843. // create this object if there is no such object in caches.
  844. $object = new $model_name($attributes);
  845. $object->new_active_record = false;
  846. }
  847. return $object;
  848. }
  849. private static function get_model_object_by_record($model_name, $record, $table_index) {
  850. $attributes = array();
  851. $columns = self::columns($model_name);
  852. foreach($columns as $index => $column) {
  853. $column_name = $column;
  854. $alias = 't' . $table_index . '_c' . $index;
  855. $attributes[$column_name] = $record[$alias];
  856. }
  857. $primary_key = self::get_primary_key_by_model($model_name);
  858. if($attributes[$primary_key]) {
  859. return self::get_model_object($model_name, $primary_key, $attributes, true);
  860. } else {
  861. return null;
  862. }
  863. }
  864. // ============================= Associations ==============================
  865. private static function get_associations($model_name) {
  866. if(isset(self::$cached_associations[$model_name])) {
  867. return self::$cached_associations[$model_name];
  868. }
  869. $associations = array('has_one' => array(), 'has_many' => array(), 'belongs_to' => array());
  870. $classes = self::get_ancestor_classes($model_name);
  871. // inherit associations from ancestor classes.
  872. for($index = sizeof($classes) - 1; $index >= 0; $index--) {
  873. $props = self::get_model_static_properties($classes[$index]);
  874. // convert associations array to hash.
  875. $props['has_one'] = is_string($props['has_one']) ? array($props['has_one'] => array()) : self::hashed_associations($props['has_one']);
  876. $props['has_many'] = is_string($props['has_many']) ? array($props['has_many'] => array()) : self::hashed_associations($props['has_many']);
  877. $props['belongs_to'] = is_string($props['belongs_to']) ? array($props['belongs_to'] => array()) : self::hashed_associations($props['belongs_to']);
  878. $associations['has_one'] = array_merge($associations['has_one'], $props['has_one']);
  879. $associations['has_many'] = array_merge($associations['has_many'], $props['has_many']);
  880. $associations['belongs_to'] = array_merge($associations['belongs_to'], $props['belongs_to']);
  881. }
  882. // cache and return hashed associations.
  883. self::$cached_associations[$model_name] = $associations;
  884. return $associations;
  885. }
  886. private static function get_association_keys($model_name) {
  887. if(isset(self::$cached_association_keys[$model_name])) {
  888. return self::$cached_association_keys[$model_name];
  889. }
  890. $keys = array();
  891. $associations = self::get_associations($model_name);
  892. foreach($associations as $association) {
  893. $keys = array_merge($keys, array_keys($association));
  894. }
  895. self::$cached_association_keys[$model_name] = $keys;
  896. return $keys;
  897. }
  898. private static function get_association_objects($object, $options) {
  899. // $options['include'] is string and value is association_id.
  900. $association_id = $options['include'];
  901. $model_name = $object->get_object_model_name();
  902. $associations = self::get_associations($model_name);
  903. if(in_array($association_id, array_keys($associations['has_one']))) {
  904. self::get_has_one_association_objects($object, $model_name, $association_id, $associations['has_one'][$association_id]);
  905. }
  906. elseif(in_array($association_id, array_keys($associations['has_many']))) {
  907. self::get_has_many_association_objects($object, $model_name, $association_id, $associations['has_many'][$association_id]);
  908. }
  909. elseif(in_array($association_id, array_keys($associations['belongs_to']))) {
  910. self::get_belongs_to_association_objects($object, $model_name, $association_id, $associations['belongs_to'][$association_id]);
  911. } else {
  912. throw new FireflyException("Unknown association: " . $association_id);
  913. }
  914. }
  915. private static function get_foreign_key($association_type, $model_name, $assoc_model_name, $associations) {
  916. if(isset($associations['foreign_key'])) {
  917. return $associations['foreign_key'];
  918. } else {
  919. $foreign_key = $model_name . '_id'; // for has_one or has_many.
  920. if($association_type == 'belongs_to') {
  921. $foreign_key = $assoc_model_name . '_id';
  922. }
  923. return strtolower($foreign_key);
  924. }
  925. }
  926. private static function get_association_model_name($association_type, $association_id, $options) {
  927. $class_name = $association_id; // has_one or belongs_to
  928. if(isset($options['class_name'])) {
  929. $class_name = $options['class_name'];
  930. } else {
  931. // other association must specify class_name, especially for belongs_to association.
  932. $class_name = $association_id;
  933. }
  934. // all $model_name using lower name in this ActiveRecord class.
  935. return strtolower($class_name);
  936. }
  937. /**
  938. * processing many_to_many table relationships, 'relation_table' key must be available. Example:
  939. * $has_many = array('search_engines' => array('through' => array('relation_table' => 'cleints_search_engines', 'column1' => 'client_id', 'column2' => 'search_engine_id'), 'class_name' => 'SearchEngine'));
  940. */
  941. private static function get_many_to_many_joins($association, $assoc_model_name) {
  942. if(isset($association['through']) && $through = $association['through']) {
  943. if(isset($through['relation_table']) && $relation_table = $through['relation_table']) {
  944. $quoted_relation_table = self::quote_table_name($relation_table);
  945. $quoted_table_name = self::quoted_table_name($assoc_model_name);
  946. $quoted_primary_key = self::quoted_primary_key($assoc_model_name);
  947. if(isset($through['column2']) && $foreign_key = $through['column2']) {
  948. $quoted_foreign_key = self::quote_column_name($foreign_key);
  949. } else {
  950. $foreign_key = $assoc_model_name . '_id';
  951. $quoted_foreign_key = self::quote_column_name($foreign_key);
  952. }
  953. $joins = "LEFT OUTER JOIN $quoted_relation_table ON $quoted_relation_table.$quoted_foreign_key = $quoted_table_name.$quoted_primary_key";
  954. return $joins;
  955. } else {
  956. throw new FireflyException("Please set value to key 'relation_table'.");
  957. }
  958. } else {
  959. return '';
  960. }
  961. }
  962. /**
  963. * get association conditions from association options.
  964. */
  965. private static function get_association_conditions($association) {
  966. if(array_key_exists('conditions', $association)) {
  967. return $association['conditions'];
  968. } else {
  969. return array();
  970. }
  971. }
  972. /**
  973. * Specifies a one-to-one association with another class. This method should only be used
  974. * if this class contains the foreign key. If the other class contains the foreign key,
  975. * then you should use +has_one+ instead.
  976. *
  977. * === Options
  978. *
  979. * [class_name]
  980. * Specify the class name of the association. Use it only if that name can't be inferred
  981. * from the association name. So <tt>has_one = array('author')</tt> will by default be linked to the Author class, but
  982. * if the real class name is Person, you'll have to specify it with this option.
  983. * [foreign_key]
  984. * Specify the foreign key used for the association. By default this is guessed to be the name
  985. * of the associated class with an "_id" suffix.
  986. */
  987. private static function get_belongs_to_association_objects($object, $model_name, $association_id, $belongs_to_assoc) {
  988. $assoc_model_name = self::get_association_model_name('belongs_to', $association_id, $belongs_to_assoc);
  989. $foreign_key = self::get_foreign_key('belongs_to', $model_name, $assoc_model_name, $belongs_to_assoc);
  990. $belongs_to_object = self::find_one($assoc_model_name, $object->attributes[$foreign_key]);
  991. $object->attributes[$association_id] = $belongs_to_object;
  992. }
  993. /**
  994. * Specifies a one-to-one association with another class. This method should only be used
  995. * if the other class contains the foreign key. If the current class contains the foreign key,
  996. * then you should use +belongs_to+ instead.
  997. *
  998. * The declaration can also include an options hash to specialize the behavior of the association.
  999. * === Options
  1000. *
  1001. * [class_name]
  1002. * Specify the class name of the association. Use it only if that name can't be inferred
  1003. * from the association name.
  1004. * [conditions]
  1005. * Specify the conditions that the associated object must meet in order to be included as a +WHERE+
  1006. * SQL fragment, such as <tt>rank = 5</tt>.
  1007. * [foreign_key]
  1008. * Specify the foreign key used for the association. By default this is guessed to be the name
  1009. * of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association
  1010. * will use "person_id" as the default <tt>foreign_key</tt>.
  1011. */
  1012. private static function get_has_one_association_objects($object, $model_name, $association_id, $has_one_assoc) {
  1013. $assoc_model_name = self::get_association_model_name('has_one', $association_id, $has_one_assoc);
  1014. $foreign_key = self::get_foreign_key('has_one', $model_name, $assoc_model_name, $has_one_assoc);
  1015. $conditions = array_merge(self::get_association_conditions($has_one_assoc), array($foreign_key => $object->id()));
  1016. $has_one_object = self::find_first($assoc_model_name, array('conditions' => $conditions));
  1017. $object->attributes[$association_id] = $has_one_object;
  1018. }
  1019. /**
  1020. * === options
  1021. *
  1022. * [class_name]
  1023. * Specify the class name of the association. Use it only if that name can't be inferred
  1024. * from the association name. So <tt>has_many = array('products')</tt> will by default be linked to the Product class, but
  1025. * if the real class name is SpecialProduct, you'll have to specify it with this option.
  1026. * [conditions]
  1027. * Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
  1028. * SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from the association are scoped if a hash
  1029. * is used.
  1030. * [foreign_key]
  1031. * Specify the foreign key used for the association. By default this is guessed to be the name
  1032. * of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ association will use "person_id"
  1033. * as the default <tt>foreign_key</tt>.
  1034. * [through]
  1035. * Specifies a relation table and composite columns of this relation table through which to perform the query.
  1036. * [relation_table] is the name of relation table.
  1037. * [column1] is foreign key of current model, default value is model_name suffixed with '_id'.
  1038. * [column2] is foreign key of association model, default value is association_model_name suffixed with '_id'.
  1039. */
  1040. private static function get_has_many_association_objects($object, $model_name, $association_id, $has_many_assoc) {
  1041. $assoc_model_name = self::get_association_model_name('has_many', $association_id, $has_many_assoc);
  1042. $foreign_key = self::get_foreign_key('has_many', $model_name, $assoc_model_name, $has_many_assoc);
  1043. $conditions = self::get_association_conditions($has_many_assoc);
  1044. $joins = self::get_many_to_many_joins($has_many_assoc, $assoc_model_name);
  1045. if($joins) {
  1046. // many_to_many relationship between tables.
  1047. $through = $has_many_assoc['through'];
  1048. if(isset($through['column1']) && $through['column1']) {
  1049. $foreign_key = $through['column1'];
  1050. }
  1051. $quoted_foreign_key = self::quote_column_name($foreign_key);
  1052. $conditions = self::sanitize_conditions($conditions);
  1053. if($conditions) {
  1054. $conditions = "$conditions AND ";
  1055. }
  1056. $quoted_relation_table = self::quote_table_name($through['relation_table']);
  1057. $conditions .= "$quoted_relation_table.$quoted_foreign_key = " . $object->id();
  1058. } else {
  1059. $conditions = array_merge($conditions, array($foreign_key => $object->id()));
  1060. }
  1061. $has_many_objects = self::find_all($assoc_model_name, array('joins' => $joins, 'conditions' => $conditions));
  1062. $object->attributes[$association_id] = $has_many_objects;
  1063. }
  1064. private static function eager_load_included_associations($association_id, $association_type, $include, $options) {
  1065. // parse find options for preload include related associations.
  1066. if(!isset($options['limit']) || $association_type != 'has_many') {
  1067. // if exists $options['limit'] and association type is has_many association
  1068. // can not avoid 1+N sql problem, should use lazy load.
  1069. if(is_array($include) && in_array($association_id, $include)) {
  1070. return true;
  1071. }
  1072. if(is_string($include) && $association_id == $include) {
  1073. return true;
  1074. }
  1075. }
  1076. return false;
  1077. }
  1078. /**
  1079. * if $model_name class has associations confiture
  1080. * return associations according to $options['include'], else return false.
  1081. */
  1082. private static function with_eager_load_associations($model_name, $options) {
  1083. if(!empty($options['include'])) {
  1084. $includes = array();
  1085. $eager_load = false;
  1086. $include = $options['include'];
  1087. $associations = self::get_associations($model_name);
  1088. foreach($associations as $assoc_type => $association) {
  1089. // $assoc_type is has_many/has_one/belongs_to
  1090. $includes[$assoc_type] = array();
  1091. foreach($association as $ass

Large files files are truncated, but you can click here to view the full file