PageRenderTime 31ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/firefly/active_record.php

https://github.com/yuweijun/blog
PHP | 2045 lines | 1422 code | 180 blank | 443 comment | 247 complexity | 13dc95f5ed824928a95587efa4975fa0 MD5 | raw 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 $assoc_id => $assoc_options) {
  1092. if(self::eager_load_included_associations($assoc_id, $assoc_type, $include, $options)) {
  1093. $eager_load = true;
  1094. $includes[$assoc_type][$assoc_id] = $assoc_options;
  1095. }
  1096. }
  1097. }
  1098. if($eager_load) {
  1099. return $includes;
  1100. }
  1101. }
  1102. return false;
  1103. }
  1104. private static function hashed_associations($associations) {
  1105. $assoc = array();
  1106. foreach($associations as $key => $value) {
  1107. if(is_string($value)) {
  1108. $assoc[$value] = array();
  1109. } else {
  1110. $assoc[$key] = $value;
  1111. }
  1112. }
  1113. return $assoc;
  1114. }
  1115. private static function reverse_sql_order($order_query) {
  1116. $orders = explode(',', $order_query);
  1117. foreach($orders as $key => $order) {
  1118. if(preg_match('/\s+asc\s*$/i', $order)) {
  1119. $orders[$key] = preg_replace('/\s+asc\s*$/i', ' DESC', $order);
  1120. }
  1121. elseif(preg_match('/\s+desc\s*$/i', $order)) {
  1122. $orders[$key] = preg_replace('/\s+desc\s*$/i', ' ASC', $order);
  1123. } else {
  1124. $orders[$key] .= " DESC";
  1125. }
  1126. }
  1127. return implode(', ', $orders);
  1128. }
  1129. private static function add_select_fields($table_name, $options) {
  1130. if(isset($options['select']) && $select = $options['select']) {
  1131. return $select;
  1132. } else {
  1133. if(isset($options['joins'])) {
  1134. return $table_name . ".*";
  1135. } else {
  1136. return "*";
  1137. }
  1138. }
  1139. }
  1140. private static function add_from_tables($table_name, $options) {
  1141. $sql = " FROM ";
  1142. if(isset($options['from']) && $from = $options['from']) {
  1143. return $sql . $from;
  1144. } else {
  1145. return $sql . $table_name;
  1146. }
  1147. }
  1148. private static function add_joins($options) {
  1149. $joins = "";
  1150. if(isset($options['joins']) && $joins = $options['joins']) {
  1151. $joins = " " . $options['joins'];
  1152. }
  1153. return $joins;
  1154. }
  1155. /**
  1156. * NOTICE: ignore 'joins' in $options when find with include associations.
  1157. */
  1158. private static function add_select_and_joins_with_associations($model_name, $quoted_table_name, $options, $include_associations) {
  1159. $select = array();
  1160. $joins = array();
  1161. $table_index = 0;
  1162. $quoted_primary_key = self::quoted_primary_key($model_name);
  1163. array_push($select, self::get_table_columns_aliases($table_index, $model_name, $quoted_table_name));
  1164. foreach($include_associations as $assoc_type => $associations) {
  1165. foreach ($associations as $association_id => $association) {
  1166. $assoc_model_name = self::get_association_model_name($assoc_type, $association_id, $association);
  1167. $assoc_quoted_table_name = self::quoted_table_name($assoc_model_name);
  1168. array_push($select, self::get_table_columns_aliases(++$table_index, $assoc_model_name, $assoc_quoted_table_name));
  1169. $foreign_key = self::get_foreign_key($assoc_type, $model_name, $assoc_model_name, $association);
  1170. $quoted_foreign_key = self::quote_column_name($foreign_key);
  1171. if($assoc_type == 'belongs_to') {
  1172. $assoc_quoted_primary_key = self::quoted_primary_key($assoc_model_name);
  1173. array_push($joins, " LEFT OUTER JOIN $assoc_quoted_table_name ON $quoted_table_name.$quoted_foreign_key = $assoc_quoted_table_name.$assoc_quoted_primary_key");
  1174. } else {
  1175. if(isset($association['through'])) {
  1176. // many_to_many association.
  1177. $through = $association['through'];
  1178. $assoc_quoted_primary_key = self::quoted_primary_key($assoc_model_name);
  1179. $relation_table = $through['relation_table'];
  1180. $quoted_relation_table = self::quote_table_name($relation_table);
  1181. if(isset($through['column1']) && $foreign_key = $through['column1']) {
  1182. $quoted_foreign_key = self::quote_column_name($foreign_key);
  1183. } else {
  1184. $foreign_key = $model_name . '_id';
  1185. $quoted_foreign_key = self::quote_column_name($foreign_key);
  1186. }
  1187. if(isset($through['column2']) && $assoc_foreign_key = $through['column2']) {
  1188. $assoc_quoted_foreign_key = self::quote_column_name($assoc_foreign_key);
  1189. } else {
  1190. $assoc_foreign_key = $assoc_model_name . '_id';
  1191. $assoc_quoted_foreign_key = self::quote_column_name($assoc_foreign_key);
  1192. }
  1193. array_push($joins, " LEFT OUTER JOIN $quoted_relation_table ON $quoted_relation_table.$quoted_foreign_key = $quoted_table_name.$quoted_primary_key");
  1194. array_push($joins, " LEFT OUTER JOIN $assoc_quoted_table_name ON $quoted_relation_table.$assoc_quoted_foreign_key = $assoc_quoted_table_name.$assoc_quoted_primary_key");
  1195. } else {
  1196. array_push($joins, " LEFT OUTER JOIN $assoc_quoted_table_name ON $quoted_table_name.$quoted_primary_key = $assoc_quoted_table_name.$quoted_foreign_key");
  1197. }
  1198. }
  1199. }
  1200. }
  1201. $sql = "SELECT ";
  1202. $sql .= join(', ', $select);
  1203. $sql .= " FROM " . $quoted_table_name;
  1204. $sql .= join('', $joins);
  1205. return $sql;
  1206. }
  1207. private static function get_table_columns_aliases($table_index, $model_name, $quoted_table_name) {
  1208. $aliases = array();
  1209. $columns = self::columns($model_name);
  1210. foreach($columns as $index => $column) {
  1211. $column = $columns[$index];
  1212. $quoted_column = self::quote_column_name($column);
  1213. $alias = $quoted_table_name . "." . $quoted_column . " AS t" . $table_index . "_c" . $index;
  1214. array_push($aliases, $alias);
  1215. }
  1216. return join(', ', $aliases);
  1217. }
  1218. private static function append_sql_options($options) {
  1219. $sql = "";
  1220. $sql .= self::add_group($options);
  1221. $sql .= self::add_order($options);
  1222. $sql .= self::add_limit($options);
  1223. $sql .= self::add_lock($options);
  1224. return $sql;
  1225. }
  1226. private static function add_conditions_with_eager_load($model_name, $quoted_table_name, $options, $include_associations) {
  1227. $conditions = "";
  1228. if(isset($options['conditions']) && $options['conditions']) {
  1229. $conditions .= " WHERE " . self::sanitize_conditions($options['conditions']);
  1230. }
  1231. foreach($include_associations as $assoc_type => $associations) {
  1232. foreach ($associations as $association_id => $association) {
  1233. if(isset($association['conditions']) && $association['conditions']) {
  1234. if($conditions == "") {
  1235. $conditions .= " WHERE " . self::sanitize_conditions($association['conditions']);
  1236. } else {
  1237. $conditions .= " AND " . self::sanitize_conditions($association['conditions']);
  1238. }
  1239. }
  1240. }
  1241. }
  1242. return $conditions;
  1243. }
  1244. /**
  1245. * Adds a sanitized version of +conditions+ to the +sql+ string.
  1246. */
  1247. private static function add_conditions($options) {
  1248. $conditions = "";
  1249. if(isset($options['conditions']) && $options['conditions']) {
  1250. $conditions .= " WHERE " . self::sanitize_conditions($options['conditions']);
  1251. }
  1252. return $conditions;
  1253. }
  1254. private static function add_order($options) {
  1255. $order = "";
  1256. if(isset($options['order']) && $order = $options['order']) {
  1257. $order = " ORDER BY $order";
  1258. }
  1259. return $order;
  1260. }
  1261. private static function add_group($options) {
  1262. $group = "";
  1263. if(isset($options['group']) && $group = $options['group']) {
  1264. $group = " GROUP BY $group";
  1265. if(isset($options['having']) && $having = $options['having']) {
  1266. $group .= " HAVING $having";
  1267. }
  1268. }
  1269. return $group;
  1270. }
  1271. private static function add_limit($options) {
  1272. $limit = "";
  1273. if(isset($options['limit']) && $options['limit']) {
  1274. $limit = " LIMIT ";
  1275. if(isset($options['offset']) && $offset = $options['offset']) {
  1276. $limit .= $offset . ", ";
  1277. }
  1278. $limit .= $options['limit'];
  1279. }
  1280. return $limit;
  1281. }
  1282. /**
  1283. * not implement optimistic locking
  1284. * because of different database connection don't share the same first level cache of active record object.
  1285. */
  1286. private static function add_lock($options) {
  1287. $lock = "";
  1288. if(isset($options['lock']) && $lock = $options['lock']) {
  1289. if($lock === true) {
  1290. return " FOR UPDATE";
  1291. }
  1292. if(is_string($lock)) {
  1293. // such as "LOCK IN SHARE MODE" in mysql.
  1294. return " " . $lock;
  1295. }
  1296. }
  1297. return $lock;
  1298. }
  1299. private static function add_limit_without_offset($options) {
  1300. if(isset($options['limit']) && $options['limit']) {
  1301. return " LIMIT " . $options['limit'];
  1302. }
  1303. return "";
  1304. }
  1305. /**
  1306. * Include config/database.php file, and return config array.
  1307. */
  1308. private static function load_configurations() {
  1309. if(empty(self::$configurations)) {
  1310. $config_file = self::get_configuration_file();
  1311. $config = array();
  1312. require($config_file);
  1313. self::$configurations = $config[ENVIRONMENT];
  1314. }
  1315. return self::$configurations;
  1316. }
  1317. private static function columns($model_name) {
  1318. if(empty(self::$cached_table_columns[$model_name])) {
  1319. $table_name = self::get_table_name_by_model($model_name);
  1320. $fields = array();
  1321. $columns = self::get_connection()->columns($table_name);
  1322. foreach($columns as $column) {
  1323. array_push($fields, $column[0]);
  1324. }
  1325. self::$cached_table_columns[$model_name] = $fields;
  1326. }
  1327. return self::$cached_table_columns[$model_name];
  1328. }
  1329. private static function do_action($hook, $parameters = '') {
  1330. if (function_exists('do_action')) {
  1331. do_action($hook, $parameters);
  1332. }
  1333. }
  1334. /**
  1335. * fetcb rows by sql
  1336. */
  1337. private static function fetch_rows($sql) {
  1338. return self::get_connection()->fetch_rows($sql);
  1339. }
  1340. // ======= ActiveRecord public instance methods =======
  1341. private $new_active_record = true;
  1342. private $lower_class_name = false;
  1343. private $session_active_record = false;
  1344. private $active_record_errors = array();
  1345. public $attributes;
  1346. /**
  1347. * New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
  1348. * attributes but not yet saved (pass a hash with key names matching the associated table column names).
  1349. * In both instances, valid attribute keys are determined by the column names of the associated table --
  1350. * hence you can't have attributes that aren't part of the table columns.
  1351. */
  1352. public function __construct($attributes = null) {
  1353. $this->attributes = $attributes ? $attributes : array();
  1354. $this->after_initialize();
  1355. $this->do_actions('after_initialize');
  1356. }
  1357. /**
  1358. * Returns a clone of the record that hasn't been assigned an id yet and is treated as a new record.
  1359. * Note that this is a "shallow" clone: it copies the object's attributes only, not its associations.
  1360. * The extent of a "deep" clone is application-specific and is therefore left to the application to implement according to its need.
  1361. */
  1362. public function __clone() {
  1363. // set id = null;
  1364. $id = self::get_primary_key_by_model($this->get_object_model_name());
  1365. $this->attributes[$id] = null;
  1366. }
  1367. /**
  1368. * A model instance's primary key is always available as model.id
  1369. * whether you name it the default 'id' or set it to something else.
  1370. */
  1371. public function id() {
  1372. $attr_name = self::get_primary_key_by_model($this->get_object_model_name());
  1373. $column = $this->attributes[$attr_name];
  1374. return $column;
  1375. }
  1376. /**
  1377. * Saves the model.
  1378. *
  1379. * If the model is new a record gets created in the database, otherwise the existing record gets updated.
  1380. *
  1381. * There's a series of callbacks associated with +save+.
  1382. * If any of the <tt>before_*</tt> callbacks return +false+ the action is cancelled and +save+ returns +false+.
  1383. *
  1384. */
  1385. public function save($validate = true) {
  1386. return $this->create_or_update($validate);
  1387. }
  1388. /**
  1389. * Deletes the record in the database.
  1390. * This method will revoke callbacks such as before_destroy and after_destroy.
  1391. */
  1392. public function destroy() {
  1393. self::transaction_start();
  1394. $result = $this->delete_with_callbacks();
  1395. if($result === false) {
  1396. self::transaction_rollback();
  1397. } else {
  1398. self::transaction_commit();
  1399. }
  1400. return $result;
  1401. }
  1402. /**
  1403. * Returns an instance of the specified +klass+ with the attributes of the current record.
  1404. * This is mostly useful in relation to single-table inheritance structures where you want a subclass to appear as the superclass.
  1405. */
  1406. public function becomes($klass) {
  1407. $attrs = $this->attributes;
  1408. // set id = null;
  1409. $attrs[self::get_primary_key_by_model($this->get_object_model_name())] = null;
  1410. $new_klass_obj = new $klass($attrs);
  1411. return $new_klass_obj;
  1412. }
  1413. /**
  1414. * Updates a single attribute and saves the record without going through the normal validation procedure.
  1415. */
  1416. public function update_attribute($name, $value) {
  1417. $model_name = $this->get_object_model_name();
  1418. $readonly_attributes = self::get_readonly_attributes($model_name);
  1419. if(in_array($name, $readonly_attributes)) {
  1420. // it should throw exception when update readonly attribute.
  1421. throw new FireflyException("'$name' is a readonly attribute, cannot be updated!");
  1422. }
  1423. $this->attributes[$name] = $value;
  1424. $this->save();
  1425. }
  1426. /**
  1427. * Updates all the attributes from the passed-in Hash and saves the record.
  1428. * If the object is invalid, the saving will fail and false will be returned.
  1429. */
  1430. public function update_attributes($attributes) {
  1431. // remove readonly attributes firstly.
  1432. $this->attributes = array_merge($this->attributes, $this->remove_readonly_attributes($attributes));
  1433. return $this->save();
  1434. }
  1435. /**
  1436. * Reloads the attributes of this object from the database.
  1437. */
  1438. public function reload() {
  1439. $model_name = $this->get_object_model_name();
  1440. $id = $this->id();
  1441. self::$cached_active_records[$model_name][$id] = null;
  1442. $obj = self::find_one($model_name, $id);
  1443. $this->attributes = $obj->attributes;
  1444. }
  1445. /**
  1446. * Returns an array of names for the attributes available on this object sorted alphabetically.
  1447. */
  1448. public function attribute_names() {
  1449. return array_keys($this->attributes);
  1450. }
  1451. public function is_new_object() {
  1452. return $this->new_active_record;
  1453. }
  1454. public function is_session_object() {
  1455. if (func_num_args() == 1) {
  1456. $this->session_active_record = func_get_arg(0);
  1457. }
  1458. return $this->session_active_record;
  1459. }
  1460. // =============================== Errors ===============================
  1461. /**
  1462. * Adds an error message ($message) to the ($attribute), which will be returned on a call to $this->get_errors_on($attribute).
  1463. */
  1464. public function add_error($attribute, $message = 'invalid') {
  1465. $this->active_record_errors[$attribute][] = $message;
  1466. }
  1467. public function clear_errors() {
  1468. $this->active_record_errors = array();
  1469. }
  1470. public function has_errors() {
  1471. return !empty($this->active_record_errors);
  1472. }
  1473. /**
  1474. * Returns false, if no errors are associated with the specified $attribute.
  1475. * Returns the error message, if one error is associated with the specified $attribute.
  1476. * Returns an array of error messages, if more than one error is associated with the specified $attribute.
  1477. */
  1478. public function get_errors_on($attribute) {
  1479. if(empty($this->active_record_errors[$attribute])) {
  1480. return false;
  1481. }
  1482. elseif(count($this->active_record_errors[$attribute]) == 1) {
  1483. return $this->active_record_errors[$attribute][0];
  1484. } else {
  1485. return $this->active_record_errors[$attribute];
  1486. }
  1487. }
  1488. // =============================== Callbacks ===============================
  1489. /**
  1490. * Callbacks are hooks into the life-cycle of an Active Record object that allows you to trigger logic
  1491. * before or after an alteration of the object state. This can be used to make sure that associated and
  1492. * dependent objects are deleted when destroy is called (by overwriting before_destroy) or to massage attributes
  1493. * before they're validated (by overwriting before_validation). As an example of the callbacks initiated, consider
  1494. * the ActiveRecord->save() call:
  1495. *
  1496. * - (-) save()
  1497. * - (1) before_validation()
  1498. * - (2) before_validation_on_create() / before_validation_on_update()
  1499. * - (-) validate()
  1500. * - (4) after_validation()
  1501. * - (5) after_validation_on_create() / after_validation_on_update()
  1502. * - (6) before_save()
  1503. * - (7) before_create() / before_update()
  1504. * - (-) create()
  1505. * - (8) after_create() / after_update()
  1506. * - (9) after_save()
  1507. * - (10) after_destroy()
  1508. * - (11) before_destroy()
  1509. * - (12) after_initialize()
  1510. *
  1511. * That's a total of 17 callbacks, which gives you immense power to react and prepare for each state in the Active Record lifecycle.
  1512. *
  1513. * Examples:
  1514. * class CreditCard extends ActiveRecord {
  1515. * // Strip everything but digits, so the user can specify "555 234 34" or "5552-3434" or both will mean "55523434"
  1516. * function before_validation_on_create {
  1517. * if(!empty($this->number)){
  1518. * $this->number = ereg_replace('[^0-9]*','',$this->number);
  1519. * }
  1520. * }
  1521. * }
  1522. *
  1523. * class Subscription extends ActiveRecord {
  1524. * var $before_create = 'recordSignup';
  1525. *
  1526. * function recordSignup() {
  1527. * $this->signed_up_on = date("Y-m-d");
  1528. * }
  1529. * }
  1530. *
  1531. * == Canceling callbacks ==
  1532. *
  1533. * If a before* callback returns false, all the later callbacks and the associated action are cancelled. If an after* callback returns
  1534. * false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined.
  1535. *
  1536. * Override this methods to hook Active Records
  1537. *
  1538. */
  1539. public function before_validation() {
  1540. return true;
  1541. }
  1542. public function validate() {
  1543. return true;
  1544. }
  1545. /**
  1546. * The after_initialize callback will be called whenever an Active Record object is instantiated,
  1547. * either by direcly using new or when a record is loaded from the database.
  1548. */
  1549. public function after_initialize() {
  1550. return true;
  1551. }
  1552. public function validate_on_create() {
  1553. return true;
  1554. }
  1555. public function validate_on_update() {
  1556. return true;
  1557. }
  1558. public function before_validation_on_create() {
  1559. return true;
  1560. }
  1561. public function before_validation_on_update() {
  1562. return true;
  1563. }
  1564. public function before_save() {
  1565. return true;
  1566. }
  1567. public function after_save() {
  1568. return true;
  1569. }
  1570. public function before_update() {
  1571. return true;
  1572. }
  1573. public function after_update() {
  1574. return true;
  1575. }
  1576. public function after_validation() {
  1577. return true;
  1578. }
  1579. public function after_validation_on_create() {
  1580. return true;
  1581. }
  1582. public function after_validation_on_update() {
  1583. return true;
  1584. }
  1585. public function before_create() {
  1586. return true;
  1587. }
  1588. public function after_create() {
  1589. return true;
  1590. }
  1591. public function after_destroy() {
  1592. return true;
  1593. }
  1594. public function before_destroy() {
  1595. return true;
  1596. }
  1597. // ======= ActiveRecord private instance methods =======
  1598. private function get_object_model_name() {
  1599. if ($this->lower_class_name == false) {
  1600. $this->lower_class_name = strtolower(get_class($this));
  1601. }
  1602. return $this->lower_class_name;
  1603. }
  1604. private function create_or_update($validate) {
  1605. if($validate && !$this->is_valid()) {
  1606. return false;
  1607. }
  1608. return $this->save_with_callbacks();
  1609. }
  1610. private function save_with_callbacks() {
  1611. $success = true;
  1612. self::transaction_start();
  1613. if($this->before_save()) {
  1614. $this->do_actions('before_save');
  1615. $result = $this->new_active_record ? $this->create_with_callbacks() : $this->update_with_callbacks();
  1616. if($result === false) {
  1617. // if result is false, create or update failure, should rollback transaction.
  1618. $success = false;
  1619. } else {
  1620. if($this->after_save()) {
  1621. $this->do_actions('after_save');
  1622. } else {
  1623. $success = false;
  1624. }
  1625. }
  1626. } else {
  1627. $success = false;
  1628. }
  1629. if($success === false) {
  1630. self::transaction_rollback();
  1631. $this->do_actions('after_rollback');
  1632. } else {
  1633. self::transaction_commit();
  1634. $this->do_actions('after_commit');
  1635. }
  1636. return $success;
  1637. }
  1638. /**
  1639. * Creates a record with values matching those of the instance attributes and returns its id.
  1640. */
  1641. private function create_with_callbacks() {
  1642. if($this->before_create()) {
  1643. $this->do_actions('before_create');
  1644. if(!$id = $this->get_last_insert_id()) {
  1645. return false;
  1646. }
  1647. if($this->after_create()) {
  1648. $this->do_actions('after_create');
  1649. } else {
  1650. Logger::warn("Callback after_create failed, but record has been created!");
  1651. return false;
  1652. }
  1653. $this->new_active_record = false;
  1654. $this->id = $id;
  1655. return $this;
  1656. } else {
  1657. return false;
  1658. }
  1659. }
  1660. /**
  1661. * Updates the associated record with values matching those of the instance attributes.
  1662. * Returns the number of affected rows.
  1663. */
  1664. private function update_with_callbacks() {
  1665. if($this->before_update()) {
  1666. $this->do_actions('before_update');
  1667. $table_columns = $this->attributes_from_column_definition($this->attributes);
  1668. $safe_attributes = $this->remove_readonly_attributes($table_columns);
  1669. $quoted_str = $this->quoted_column_values($safe_attributes);
  1670. $model_name = $this->get_object_model_name();
  1671. $table_name = self::quoted_table_name($model_name);
  1672. $primary_key = self::quoted_primary_key($model_name);
  1673. if($quoted_str == '') {
  1674. return true;
  1675. } else {
  1676. $sql = "UPDATE $table_name SET $quoted_str WHERE $primary_key=" . self::quote_column_value($this->id());
  1677. self::get_connection()->update($sql);
  1678. if($this->after_update()) {
  1679. $this->do_actions('after_update');
  1680. } else {
  1681. Logger::warn("Callback after_update failed, but record has been updated!");
  1682. return false;
  1683. }
  1684. return $this;
  1685. }
  1686. } else {
  1687. return false;
  1688. }
  1689. }
  1690. private function delete_with_callbacks() {
  1691. $model_name = $this->get_object_model_name();
  1692. $primary_key = self::get_primary_key_by_model($model_name);
  1693. $options = array('conditions' => array($primary_key => $this->id()));
  1694. $sql = self::construct_delete_sql($model_name, $options);
  1695. if($this->before_destroy()) {
  1696. $this->do_actions('before_destroy');
  1697. self::get_connection()->delete($sql);
  1698. if($this->after_destroy()) {
  1699. $this->do_actions('after_destroy');
  1700. } else {
  1701. Logger::warn("Callback after_destroy failed, but record has been deleted!");
  1702. return false;
  1703. }
  1704. self::remove_objects_from_caches($model_name, $options);
  1705. return true;
  1706. } else {
  1707. return false;
  1708. }
  1709. }
  1710. /**
  1711. * Returns true if no errors were added otherwise false.
  1712. */
  1713. private function is_valid() {
  1714. $this->clear_errors();
  1715. if($this->before_validation() && $this->do_actions('before_validation')) {
  1716. $this->validate();
  1717. $this->after_validation();
  1718. $this->do_actions('after_validation');
  1719. if($this->is_new_object()) {
  1720. if($this->before_validation_on_create()) {
  1721. $this->do_actions('before_validation_on_create');
  1722. $this->validate_on_create();
  1723. $this->after_validation_on_create();
  1724. $this->do_actions('after_validation_on_create');
  1725. }
  1726. } else {
  1727. if($this->before_validation_on_update()) {
  1728. $this->do_actions('before_validation_on_update');
  1729. $this->validate_on_update();
  1730. $this->after_validation_on_update();
  1731. $this->do_actions('after_validation_on_update');
  1732. }
  1733. }
  1734. }
  1735. return !$this->has_errors();
  1736. }
  1737. private function get_last_insert_id() {
  1738. $table_columns = $this->attributes_from_column_definition($this->attributes);
  1739. $safe_attributes = $this->remove_readonly_attributes($table_columns);
  1740. $quoted_attrs = $this->quotes_attributes($safe_attributes);
  1741. $table_name = self::quoted_table_name($this->get_object_model_name());
  1742. if($sequence = self::$sequence_class_name && class_exists($sequence)) {
  1743. // create primary id by other class ($sequence_class_name class must implement public function get_next_primary_id),
  1744. // because primary id may not auto increment.
  1745. $seq = new $sequence;
  1746. $id = $seq->get_next_primary_id();
  1747. $model_name = $this->get_object_model_name();
  1748. $primary_key = self::get_primary_key_by_model($model_name);
  1749. $pk = self::quote_column_name($primary_key);
  1750. $quoted_attrs[$pk] = self::quote_column_value($id);
  1751. }
  1752. if(empty($quoted_attrs)) {
  1753. $id = self::get_connection()->empty_insert_statement($table_name);
  1754. } else {
  1755. $columns = implode(', ', array_keys($quoted_attrs));
  1756. $values = implode(', ', array_values($quoted_attrs));
  1757. $sql = "INSERT INTO " . $table_name . " (" . $columns . ") VALUES (" . $values . ")";
  1758. $id = self::get_connection()->create($sql);
  1759. }
  1760. return $id;
  1761. }
  1762. /**
  1763. * Calls the $method using the reference to each registered actions.
  1764. */
  1765. private function do_actions($method) {
  1766. if (function_exists('do_action')) {
  1767. do_action($this->get_object_model_name() . '.' . $method, $this);
  1768. }
  1769. return true;
  1770. }
  1771. /**
  1772. * Removes attributes which have been marked as readonly.
  1773. */
  1774. private function remove_readonly_attributes($attributes) {
  1775. $class_name = $this->get_object_model_name();
  1776. $attr = array();
  1777. foreach($attributes as $key => $value) {
  1778. // filter attributes remain safe attributes, and remove default attributes.
  1779. if(!in_array($key, self::get_readonly_attributes($class_name))) {
  1780. $attr[$key] = $value;
  1781. }
  1782. }
  1783. return $attr;
  1784. }
  1785. /**
  1786. * Returns a copy of the attributes hash where all the values have been safely quoted for use in an SQL statement.
  1787. * For create statement.
  1788. */
  1789. private function quotes_attributes($attributes) {
  1790. $quoted_attrs = array();
  1791. foreach($attributes as $key => $value) {
  1792. $column = self::quote_column_name($key);
  1793. $quoted_attrs[$column] = self::quote_column_value($value);
  1794. }
  1795. return $quoted_attrs;
  1796. }
  1797. /**
  1798. * For update statement.
  1799. */
  1800. private function quoted_column_values($attributes) {
  1801. return self::sanitize_assignment($attributes);
  1802. }
  1803. private function attributes_from_column_definition($attributes) {
  1804. $model_name = $this->get_object_model_name();
  1805. $columns = self::columns($model_name);
  1806. $attrs = array();
  1807. $keys = array_keys($attributes);
  1808. foreach($columns as $column_name) {
  1809. if(in_array($column_name, $keys)) {
  1810. $attrs[$column_name] = $attributes[$column_name];
  1811. } else {
  1812. if(in_array($column_name, array('created_on', 'created_at')) && $this->new_active_record) {
  1813. $attrs[$column_name] = $this->set_record_timestamps($column_name);
  1814. }
  1815. if(in_array($column_name, array('updated_on', 'updated_at'))) {
  1816. $attrs[$column_name] = $this->set_record_timestamps($column_name);
  1817. }
  1818. }
  1819. }
  1820. return $attrs;
  1821. }
  1822. private function set_record_timestamps($column_name) {
  1823. $time = time();
  1824. $date = date('Y-m-d');
  1825. if($column_name == 'created_at' || $column_name == 'updated_at') {
  1826. return $date;
  1827. }
  1828. // created_on or updated_on.
  1829. return $time;
  1830. }
  1831. public function __set($key, $value) {
  1832. $this->attributes[$key] = $value;
  1833. }
  1834. public function __get($key) {
  1835. if(!$this->session_active_record && array_key_exists($key, $this->attributes)) {
  1836. return $this->attributes[$key];
  1837. } else {
  1838. $model_name = $this->get_object_model_name();
  1839. if(in_array($key, self::get_association_keys($model_name))) {
  1840. // for has_many, has_one and belongs_to properties.
  1841. self::get_association_objects($this, array('include' => $key));
  1842. return array_key_exists($key, $this->attributes) ? $this->attributes[$key] : null;
  1843. } else {
  1844. // for table columns which value is null.
  1845. $columns = self::columns($model_name);
  1846. if(in_array($key, $columns)) {
  1847. return array_key_exists($key, $this->attributes) ? $this->attributes[$key] : null;
  1848. }
  1849. }
  1850. throw new FireflyException("Undefined activerecord attribute: " . $key);
  1851. }
  1852. }
  1853. /**
  1854. * __callStatic() PHP 5.3.0.
  1855. */
  1856. public function __call($method, $args) {
  1857. throw new FireflyException("Unkown method has been called on ActiveRecord Object: " . $method);
  1858. }
  1859. public function __toString() {
  1860. return var_export($this->attributes, true);
  1861. }
  1862. }
  1863. ?>