PageRenderTime 54ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/database/ActiveRecord.php

https://github.com/wilkerlucio/fishy-framework
PHP | 1244 lines | 727 code | 229 blank | 288 comment | 75 complexity | 87287a640670e4ee1b5baffdef388075 MD5 | raw file
  1. <?php
  2. /*
  3. * Copyright 2008 Wilker Lucio <wilkerlucio@gmail.com>
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. require_once(dirname(__FILE__) . '/../libraries/Inflector.php');
  18. require_once(dirname(__FILE__) . '/FieldAct.php');
  19. require_once(dirname(__FILE__) . '/Validators.php');
  20. require_once(dirname(__FILE__) . '/DbCommand.php');
  21. require_once(dirname(__FILE__) . '/TableDescriptor.php');
  22. require_once(dirname(__FILE__) . '/ModelCache.php');
  23. require_once(dirname(__FILE__) . '/relations/ActiveRelationOne.php');
  24. require_once(dirname(__FILE__) . '/relations/ActiveRelationMany.php');
  25. /**
  26. * Active Record class for data access layer
  27. *
  28. * @package DB
  29. * @author wilker
  30. * @version 0.1.3
  31. */
  32. abstract class ActiveRecord
  33. {
  34. protected $_attributes;
  35. private $_exists;
  36. private $_relations;
  37. private $_validators;
  38. private $_errors;
  39. private $_scopes;
  40. private $_scopes_enabled;
  41. private $_field_act;
  42. /**
  43. * Creates a new record
  44. *
  45. * @return void
  46. * @author Wilker
  47. **/
  48. public function __construct($params = array())
  49. {
  50. $this->_exists = false;
  51. $this->_attributes = array();
  52. $this->_relations = array();
  53. $this->_validators = array();
  54. $this->_errors = array();
  55. $this->_scopes = array();
  56. $this->_scopes_enabled = array();
  57. $this->_field_act = array();
  58. $this->initialize_fields();
  59. $this->fill($params);
  60. $this->setup();
  61. }
  62. /**
  63. * Get a shared instace of current model
  64. *
  65. * @param string $model_name The name of model
  66. * @return mixed The model instance
  67. */
  68. public static function model($model_name)
  69. {
  70. $model_name = ucfirst($model_name);
  71. $modelcache = ModelCache::get_instance();
  72. return @$modelcache->$model_name;
  73. }
  74. public function setup() {}
  75. /**
  76. * Fill object with attributes with a given data array using key/value scheme
  77. *
  78. * @param $data Array containing key as index and values as data
  79. * @return void
  80. * @author Wilker
  81. **/
  82. public function fill($data)
  83. {
  84. foreach ($data as $key => $value) {
  85. $this->$key = $value;
  86. }
  87. }
  88. /**
  89. * Return the name of primary key field
  90. *
  91. * @return string
  92. * @author Wilker
  93. **/
  94. public function primary_key()
  95. {
  96. return 'id';
  97. }
  98. /**
  99. * Shotcut to get ID value of current register, this method is equivalent to
  100. * use:
  101. *
  102. * $pk = $this->primary_key();
  103. * return $this->$pk;
  104. *
  105. * @return string
  106. * @author Wilker
  107. */
  108. public function primary_key_value()
  109. {
  110. $pk = $this->primary_key();
  111. return $this->read_attribute($pk);
  112. }
  113. /**
  114. * Get record table
  115. *
  116. * @return string
  117. * @author Wilker
  118. **/
  119. public function table()
  120. {
  121. $class = Inflect::pluralize(get_class($this));
  122. $table = $class[0];
  123. for ($i = 1; $i < strlen($class); $i++) {
  124. $char = $class[$i];
  125. if (ord($char) > 64 && ord($char) < 91) {
  126. $table .= '_' . strtolower($char);
  127. } else {
  128. $table .= $char;
  129. }
  130. }
  131. return strtolower($table);
  132. }
  133. /**
  134. * Initialize instance attributes
  135. *
  136. * @return void
  137. * @author Wilker
  138. **/
  139. protected function initialize_fields()
  140. {
  141. $autospecial_fields = array(
  142. 'created_at' => 'datetime',
  143. 'updated_at' => 'datetime'
  144. );
  145. $fields = $this->fields();
  146. foreach ($fields as $field) {
  147. $this->_attributes[$field] = null;
  148. if (isset($autospecial_fields[$field])) {
  149. $this->register_field_as($autospecial_fields[$field], array($field));
  150. }
  151. }
  152. }
  153. /**
  154. * Verify if the record exists at database
  155. *
  156. * @return boolean true if record exists, false otherwise
  157. * @author wilker
  158. */
  159. public function exists()
  160. {
  161. return $this->_exists;
  162. }
  163. /**
  164. * Find records at database
  165. *
  166. * @param $what Type of search
  167. * @param $options Options of search
  168. * @return mixed
  169. * @author wilker
  170. */
  171. public function find($what = 'all', $options = array())
  172. {
  173. $options = array_merge(array(
  174. 'conditions' => '',
  175. 'order' => $this->primary_key() . ' ASC',
  176. 'limit' => '',
  177. 'offset' => false,
  178. 'select' => '*',
  179. 'from' => '`' . $this->table() . '`',
  180. 'groupby' => '',
  181. 'joins' => array()
  182. ), $options);
  183. switch ($what) {
  184. case 'all':
  185. return $this->find_every($options);
  186. break;
  187. case 'first':
  188. return $this->find_initial($options);
  189. break;
  190. case 'last':
  191. return $this->find_last($options);
  192. break;
  193. default:
  194. return $this->find_from_ids($what, $options);
  195. break;
  196. }
  197. }
  198. /**
  199. * Wrapper for find with all as first argument
  200. *
  201. * @return mixed
  202. * @author wilker
  203. */
  204. public function all()
  205. {
  206. $args = func_get_args();
  207. array_unshift($args, 'all');
  208. return call_user_func_array(array($this, 'find'), $args);
  209. }
  210. /**
  211. * Wrapper for find with first as first argument
  212. *
  213. * @return mixed
  214. * @author wilker
  215. */
  216. public function first()
  217. {
  218. $args = func_get_args();
  219. array_unshift($args, 'first');
  220. return call_user_func_array(array($this, 'find'), $args);
  221. }
  222. /**
  223. * Wrapper for find with last as first argument
  224. *
  225. * @return mixed
  226. * @author wilker
  227. */
  228. public function last()
  229. {
  230. $args = func_get_args();
  231. array_unshift($args, 'last');
  232. return call_user_func_array(array($this, 'find'), $args);
  233. }
  234. /**
  235. * Find records by specifique fields
  236. *
  237. * @return array
  238. * @author Wilker
  239. **/
  240. public function dynamic_find($type, $fields, $values)
  241. {
  242. $fields = explode('_and_', $fields);
  243. $conditions = array();
  244. foreach ($fields as $field) {
  245. $value = $this->sanitize(array_shift($values));
  246. $conditions[] = "`$field` = '$value'";
  247. }
  248. $conditions = implode(" AND ", $conditions);
  249. $options = array(
  250. 'conditions' => $conditions
  251. );
  252. if (count($values) > 0) {
  253. $user_options = array_shift($values);
  254. if (is_array($user_options)) {
  255. $options = array_merge($options, $user_options);
  256. }
  257. }
  258. switch ($type) {
  259. case 'all_by':
  260. return $this->all($options);
  261. case 'by':
  262. return $this->first($options);
  263. }
  264. }
  265. /**
  266. * Get the number of rows at table
  267. *
  268. * @return integer
  269. * @author Wilker
  270. **/
  271. public function count($options = array())
  272. {
  273. $options['select'] = 'count(*) as total';
  274. $result = $this->first($options);
  275. return $result->total;
  276. }
  277. /**
  278. * Save object into database, if the object exists, the instance is
  279. * only updated at database
  280. *
  281. * @return boolean
  282. * @author wilker
  283. */
  284. public function save()
  285. {
  286. $this->create_or_update();
  287. }
  288. /**
  289. * Delete current register from database
  290. *
  291. * @return boolean
  292. * @author wilker
  293. */
  294. public function destroy()
  295. {
  296. //check if record exists before delete
  297. if (!$this->exists()) {
  298. return false;
  299. }
  300. $this->before_destroy();
  301. $pk = $this->primary_key();
  302. $pk_val = $this->sanitize($this->$pk);
  303. $table = $this->table();
  304. $sql = "DELETE FROM `$table` WHERE `$pk` = '$pk_val'";
  305. DbCommand::execute($sql);
  306. $this->_exists = false;
  307. $this->after_destroy();
  308. return true;
  309. }
  310. /**
  311. * Truncate table
  312. *
  313. * @return void
  314. * @author wilker
  315. */
  316. public function truncate()
  317. {
  318. $table = $this->table();
  319. $sql = "TRUNCATE `$table`";
  320. DbCommand::execute($sql);
  321. }
  322. /**
  323. * Map result data into object
  324. *
  325. * @return mixed
  326. * @author Wilker
  327. **/
  328. protected function map_object($data)
  329. {
  330. $class = get_class($this);
  331. $object = new $class();
  332. foreach ($data as $key => $value) {
  333. $object->write_attribute($key, $value);
  334. }
  335. return $object;
  336. }
  337. private function construct_finder_sql($options)
  338. {
  339. $sql = "SELECT {$options['select']} ";
  340. $sql .= "FROM {$options['from']} ";
  341. $this->add_joins($sql, $options['joins']);
  342. $this->add_conditions($sql, $options['conditions']);
  343. $this->add_groupby($sql, $options['groupby']);
  344. $this->add_order($sql, $options['order']);
  345. $this->add_limit($sql, $options['limit'], $options['offset']);
  346. return $sql;
  347. }
  348. private function add_joins(&$sql, $joins)
  349. {
  350. if (is_array($joins)) {
  351. $cur_table = $this->table();
  352. $cur_key = $this->primary_key();
  353. $cur_fk = strtolower(get_class($this)) . '_id';
  354. foreach ($joins as $join) {
  355. $model = ActiveRecord::model($join);
  356. $join_table = $model->table();
  357. $join_key = $model->primary_key();
  358. $join_fk = strtolower(get_class($model)) . '_id';
  359. $sql .= "INNER JOIN `{$join_table}` ON `{$join_table}`.`{$cur_fk}` = `{$cur_table}`.`{$cur_key}` ";
  360. }
  361. } elseif (is_string($joins)) {
  362. $sql .= $joins . " ";
  363. }
  364. }
  365. private function add_conditions(&$sql, $conditions)
  366. {
  367. $nest = array();
  368. foreach ($this->_scopes_enabled as $scope) {
  369. $nest[] = $this->build_conditions($this->_scopes[$scope]);
  370. }
  371. if ($conditions) {
  372. $nest[] = $this->build_conditions($conditions);
  373. }
  374. if (count($nest) > 0) {
  375. $sql .= 'WHERE (' . implode(') AND (', $nest) . ') ';
  376. }
  377. }
  378. private function build_conditions($conditions)
  379. {
  380. $sql = '';
  381. if (is_array($conditions)) {
  382. if (array_keys($conditions) === range(0, count($conditions) - 1)) {
  383. $query = array_shift($conditions);
  384. for($i = 0; $i < strlen($query); $i++) {
  385. if ($query[$i] == '?') {
  386. if (count($conditions) == 0) {
  387. throw new QueryMismatchParamsException('The number of question marks is more than provided params');
  388. }
  389. $sql .= $this->prepare_for_value(array_shift($conditions));
  390. } else {
  391. $sql .= $query[$i];
  392. }
  393. }
  394. } else {
  395. $factors = array();
  396. foreach ($conditions as $key => $value) {
  397. $matches = array();
  398. if (preg_match("/([a-z_].*?)\s*((?:[><!=\s]|LIKE|IS|NOT)+)/i", $key, $matches)) {
  399. $key = $matches[1];
  400. $op = strtoupper($matches[2]);
  401. } else {
  402. if ($value === null) {
  403. $op = 'IS';
  404. } elseif (is_array($value)) {
  405. $op = 'IN';
  406. } else {
  407. $op = "=";
  408. }
  409. }
  410. $value = $this->prepare_for_value($value);
  411. $factors[] = "`$key` $op $value";
  412. }
  413. $sql .= implode(" AND ", $factors);
  414. }
  415. } else {
  416. $sql .= $conditions;
  417. }
  418. return $sql;
  419. }
  420. private function add_groupby(&$sql, $order)
  421. {
  422. if ($order) {
  423. $sql .= "GROUP BY $order ";
  424. }
  425. }
  426. private function add_order(&$sql, $order)
  427. {
  428. if ($order) {
  429. $sql .= "ORDER BY $order ";
  430. }
  431. }
  432. private function add_limit(&$sql, $limit, $offset)
  433. {
  434. if ($limit) {
  435. if ($offset !== false) {
  436. $sql .= "LIMIT $offset, $limit ";
  437. } else {
  438. $sql .= "LIMIT $limit ";
  439. }
  440. }
  441. }
  442. private function find_every($options)
  443. {
  444. return $this->find_by_sql($this->construct_finder_sql($options));
  445. }
  446. private function find_initial($options)
  447. {
  448. $options['limit'] = 1;
  449. $data = $this->find_every($options);
  450. return count($data) > 0 ? $data[0] : null;
  451. }
  452. private function find_last($options)
  453. {
  454. if ($options['order']) {
  455. $options['order'] = $this->reverse_sql_order($options['order']);
  456. }
  457. return $this->find_initial($options);
  458. }
  459. private function find_from_ids($ids, $options)
  460. {
  461. $pk = $this->primary_key();
  462. if (is_array($ids)) {
  463. $options['conditions'] = "`$pk` in ('" . implode("','", $this->sanitize_array($ids)) . "')";
  464. } else {
  465. $id = $this->sanitize($ids);
  466. $options['conditions'] = "`$pk` = '$id'";
  467. }
  468. return is_array($ids) ? $this->find_every($options) : $this->find_initial($options);
  469. }
  470. private function reverse_sql_order($order)
  471. {
  472. $reversed = explode(',', $order);
  473. foreach ($reversed as $k => $rev) {
  474. if (preg_match('/\s(asc|ASC)$/', $rev)) {
  475. $rev = preg_replace('/\s(asc|ASC)$/', ' DESC', $rev);
  476. } elseif (preg_match('/\s(desc|DESC)$/', $rev)) {
  477. $rev = preg_replace('/\s(desc|DESC)$/', ' ASC', $rev);
  478. } elseif (!preg_match('/\s(acs|ASC|desc|DESC)$/', $rev)) {
  479. $rev .= " DESC";
  480. }
  481. $reversed[$k] = $rev;
  482. }
  483. return implode(',', $reversed);
  484. }
  485. /**
  486. * Find records by using a sql statement, avoid to use this method if you
  487. * can do it in another way (like using default find methods)
  488. *
  489. * @param $sql SQL Statement
  490. * @return array Array of objects returned by query
  491. */
  492. public function find_by_sql($sql)
  493. {
  494. $data = DbCommand::all($sql);
  495. $data = array_map(array($this, 'map_object'), $data);
  496. foreach ($data as $model) {
  497. $model->_exists = true;
  498. }
  499. return $data;
  500. }
  501. private function create_or_update()
  502. {
  503. $this->before_save();
  504. if ($this->exists()) {
  505. $this->before_update();
  506. $this->validate_ex();
  507. $this->save_relations();
  508. $this->update();
  509. $this->after_update();
  510. } else {
  511. $this->before_create();
  512. $this->validate_ex();
  513. $this->save_relations();
  514. $this->create();
  515. $this->after_create();
  516. }
  517. $this->after_save();
  518. }
  519. private function create()
  520. {
  521. $this->write_magic_time('created_at');
  522. $this->write_magic_time('updated_at');
  523. $pk = $this->primary_key();
  524. $table = $this->table();
  525. $fields = $this->map_real_fields();
  526. $sql_fields = implode("`,`", array_keys($fields));
  527. $sql_values = implode(",", array_map(array($this, 'prepare_for_value'), $fields));
  528. $sql = "INSERT INTO `$table` (`$sql_fields`) VALUES ($sql_values);";
  529. DbCommand::execute($sql);
  530. $this->$pk = DbCommand::insert_id();
  531. $this->_exists = true;
  532. }
  533. private function update()
  534. {
  535. $this->write_magic_time('updated_at');
  536. $pk = $this->primary_key();
  537. $pk_value = $this->sanitize($this->$pk);
  538. $table = $this->table();
  539. $fields = $this->map_real_fields();
  540. $sql_set = array();
  541. foreach ($fields as $key => $value) {
  542. $sql_set[] = "`$key` = " . $this->prepare_for_value($value);
  543. }
  544. $sql_set = implode(",", $sql_set);
  545. $sql = "UPDATE `$table` SET $sql_set WHERE `$pk` = '$pk_value';";
  546. DbCommand::execute($sql);
  547. }
  548. private function save_relations()
  549. {
  550. foreach ($this->_relations as $rel) {
  551. $rel->save();
  552. }
  553. }
  554. public function fields()
  555. {
  556. $descriptor = TableDescriptor::get_instance();
  557. $table = $this->table();
  558. return $descriptor->$table;
  559. }
  560. private function map_real_fields()
  561. {
  562. $pk = $this->primary_key();
  563. $data = array();
  564. $fields = $this->fields();
  565. foreach ($fields as $field) {
  566. if ($field != $pk) {
  567. $data[$field] = isset($this->_attributes[$field]) ? $this->_attributes[$field] : null;
  568. }
  569. }
  570. return $data;
  571. }
  572. private function map_real_fields_sanitized()
  573. {
  574. return $this->sanitize_array($this->map_real_fields());
  575. }
  576. private function sanitize($data)
  577. {
  578. if ($data === null) {
  579. return 'NULL';
  580. } elseif (is_array($data)) {
  581. return '(\'' . implode('\', \'', $this->sanitize_array($data)) . '\')';
  582. }
  583. return mysql_real_escape_string($data);
  584. }
  585. private function sanitize_array($data)
  586. {
  587. return array_map(array($this, "sanitize"), $data);
  588. }
  589. private function prepare_for_value($value)
  590. {
  591. $sanitized = $this->sanitize($value);
  592. if (is_string($value)) {
  593. return "'$sanitized'";
  594. } else {
  595. return $sanitized;
  596. }
  597. }
  598. public function read_all_attributes()
  599. {
  600. return $this->_attributes;
  601. }
  602. public function read_attribute($attribute)
  603. {
  604. return isset($this->_attributes[$attribute]) ? $this->_attributes[$attribute] : null;
  605. }
  606. public function write_attribute($attribute, $value)
  607. {
  608. $this->_attributes[$attribute] = $value;
  609. }
  610. private function write_magic_time($field)
  611. {
  612. $fields = $this->fields();
  613. if (in_array($field, $fields)) {
  614. $date = date('Y-m-d H:i:s');
  615. $this->write_attribute($field, $date);
  616. }
  617. }
  618. /**
  619. * Handles access to dynamic properties
  620. *
  621. * @return mixed
  622. * @author wilker
  623. */
  624. public function __get($attribute)
  625. {
  626. //check for method accessor
  627. if (method_exists($this, 'get_' . $attribute)) {
  628. return call_user_func(array($this, 'get_' . $attribute));
  629. }
  630. //check for id
  631. if ($attribute == 'id') {
  632. return $this->primary_key_value();
  633. }
  634. //chech for field act command
  635. if (isset($this->_field_act[$attribute])) {
  636. $data = $this->_field_act[$attribute];
  637. if ($data[0] & FIELD_ACT_GET) {
  638. return FieldAct::get($data[1], $data[2]);
  639. }
  640. }
  641. //check for named scope
  642. if (isset($this->_scopes[$attribute])) {
  643. return $this->append_scope($attribute);
  644. }
  645. //check for relation
  646. if (isset($this->_relations[$attribute])) {
  647. return $this->_relations[$attribute]->get_data();
  648. }
  649. //get table attribute
  650. return $this->read_attribute($attribute);
  651. //dispatch exception
  652. //throw new ActiveRecordInvalidAttributeException();
  653. }
  654. /**
  655. * Handles access to write dynamic properties
  656. *
  657. * @return void
  658. * @author wilker
  659. */
  660. public function __set($attribute, $value)
  661. {
  662. //chech for field act command
  663. if (isset($this->_field_act[$attribute])) {
  664. $data = $this->_field_act[$attribute];
  665. if ($data[0] & FIELD_ACT_SET) {
  666. $args = $data[2];
  667. $obj = array_shift($args);
  668. $field = array_shift($args);
  669. array_unshift($args, $value);
  670. array_unshift($args, $field);
  671. array_unshift($args, $obj);
  672. if ($data[0] & FIELD_ACT_SET) {
  673. $this->write_attribute($attribute, FieldAct::set($data[1], $args));
  674. }
  675. return;
  676. }
  677. }
  678. //check for method accessor
  679. if (method_exists($this, 'set_' . $attribute)) {
  680. call_user_func(array($this, 'set_' . $attribute), $value);
  681. } elseif (isset($this->_relations[$attribute])) {
  682. $this->_relations[$attribute]->set_data($value);
  683. } else {
  684. //set attribute
  685. $this->write_attribute($attribute, $value);
  686. }
  687. }
  688. /**
  689. * Handles access to dynamic methods
  690. *
  691. * @return mixed
  692. * @author wilker
  693. */
  694. public function __call($name, $arguments)
  695. {
  696. //for use in preg matches
  697. $matches = array();
  698. //chech for field act command
  699. if (isset($this->_field_act[$name])) {
  700. $data = $this->_field_act[$name];
  701. if ($data[0] & FIELD_ACT_CALL) {
  702. $args = $data[2];
  703. foreach ($arguments as $arg) {
  704. $args[] = $arg;
  705. }
  706. return FieldAct::call($data[1], $args);
  707. }
  708. }
  709. //do a get
  710. if (preg_match('/^get_(.+)/', $name, $matches)) {
  711. $var_name = $matches[1];
  712. return $this->$var_name ? $this->$var_name : $arguments[0];
  713. }
  714. //try to catch validator assign
  715. if (substr($name, 0, 9) == 'validates') {
  716. return $this->register_validator($name, $arguments);
  717. }
  718. //try to catch field act assign
  719. if (substr($name, 0, 9) == 'field_as_') {
  720. return $this->register_field_as(substr($name, 9), $arguments);
  721. }
  722. //try to catch dynamic find
  723. if (preg_match("/^find_(all_by|by)_(.*)/", $name, $matches)) {
  724. return $this->dynamic_find($matches[1], $matches[2], $arguments);
  725. }
  726. //send to model try to parse
  727. $this->call($name, $arguments);
  728. }
  729. public function call($name)
  730. {
  731. throw new Exception("Method $name is not found in " . get_class($this));
  732. }
  733. public function __toString() {
  734. $base = "ActiveRecord::" . get_class($this);
  735. if ($this->exists()) {
  736. $pk = $this->primary_key_value();
  737. $base .= "($pk)";
  738. }
  739. return $base;
  740. }
  741. /**
  742. * Get all data of model
  743. *
  744. * @return string
  745. * @author Wilker
  746. **/
  747. public function inspect()
  748. {
  749. $out = $this->__toString();
  750. foreach ($this->_attributes as $key => $value) {
  751. $out .= "\n" . $key . " => " . $value;
  752. }
  753. $out .= "\n";
  754. return $out;
  755. }
  756. /**
  757. * Estabilishy has one connection with another record
  758. *
  759. * @return void
  760. * @author wilker
  761. */
  762. protected function has_one($expression, $options = array())
  763. {
  764. list($model, $name) = $this->parse_relation_expression($expression);
  765. $this->_relations[$name] = new ActiveRelationOne($this, $model, $options);
  766. }
  767. /**
  768. * Estabilishy a connection with many related records
  769. *
  770. * @return void
  771. * @author wilker
  772. */
  773. protected function has_many($expression, $options = array())
  774. {
  775. list($model, $name) = $this->parse_relation_expression($expression);
  776. $this->_relations[$name] = new ActiveRelationMany($this, Inflect::singularize($model), $options);
  777. }
  778. /**
  779. * Aliases to has one
  780. *
  781. * @return void
  782. * @author wilker
  783. */
  784. protected function belongs_to($model, $options = array())
  785. {
  786. $this->has_one($model, $options);
  787. }
  788. /**
  789. * Gives a relation expression and return elements
  790. *
  791. * @param string $expression Expression to be evaluated
  792. * @return array Array containing: [0] => name of relation, [1] => foreign model
  793. */
  794. protected function parse_relation_expression($expression)
  795. {
  796. $parts = explode(' as ', $expression);
  797. $model = $parts[0];
  798. if (count($parts) > 1) {
  799. $name = $parts[1];
  800. } else {
  801. $name = $model;
  802. $model = ucfirst($model);
  803. }
  804. return array($model, $name);
  805. }
  806. /**
  807. * Get a description of all of model relations
  808. *
  809. * @return array
  810. */
  811. public function describe_relations()
  812. {
  813. $relations = array();
  814. foreach ($this->_relations as $key => $rel) {
  815. $relations[] = $this->describe_relation($key);
  816. }
  817. return $relations;
  818. }
  819. /**
  820. * Get the definition of a relation
  821. *
  822. * @param string $rel The name of relation to check
  823. * @return array Array with relation data
  824. */
  825. public function describe_relation($rel)
  826. {
  827. if (!isset($this->_relations[$rel])) {
  828. return null;
  829. }
  830. $r = $this->_relations[$rel];
  831. return array(
  832. 'name' => $rel,
  833. 'instance' => $r,
  834. 'kind' => get_class($r),
  835. 'model' => $r->get_foreign_model(),
  836. 'loaded' => $r->is_loaded()
  837. );
  838. }
  839. /**
  840. * Compare two arrays of to get different records of then
  841. *
  842. * @param array $col1 First array
  843. * @param array $col2 Second array
  844. * @return array Array with difference between first and second collections
  845. */
  846. public static function model_diff($col1, $col2)
  847. {
  848. $keeplist = array();
  849. foreach ($col1 as $item) {
  850. $keep = true;
  851. foreach ($col2 as $item2) {
  852. if ($item->equal($item2)) {
  853. $keep = false;
  854. break;
  855. }
  856. }
  857. if ($keep) {
  858. $keeplist[] = $item;
  859. }
  860. }
  861. return $keeplist;
  862. }
  863. /**
  864. * Test if current object is equals to another
  865. *
  866. * @param object $obj2 The model to test
  867. * @return boolean
  868. */
  869. public function equal($obj2)
  870. {
  871. return ($obj2->table() == $this->table()) && ($obj2->primary_key_value() == $this->primary_key_value());
  872. }
  873. //Validators
  874. private function register_validator($validator, $arguments)
  875. {
  876. array_unshift($arguments, $this);
  877. $this->_validators[] = array($validator, $arguments);
  878. }
  879. protected function validate_ex()
  880. {
  881. if (!$this->is_valid()) {
  882. throw new InvalidRecordException('This record has some invalid fields, please fix problems and try again');
  883. }
  884. }
  885. /**
  886. * Test if current object is valid
  887. *
  888. * @return boolean
  889. */
  890. public function is_valid()
  891. {
  892. $this->_errors = array();
  893. $valid = $this->validate();
  894. foreach ($this->_validators as $validator) {
  895. list($method, $arguments) = $validator;
  896. if (!call_user_func_array(array('ActiveRecord_Validators', $method), $arguments)) {
  897. $valid = false;
  898. }
  899. }
  900. return $valid;
  901. }
  902. /**
  903. * Inject one error at object
  904. *
  905. * @param string $field The name of field that has the error
  906. * @param string $error Error message
  907. * @return void
  908. */
  909. public function add_error($field, $error)
  910. {
  911. $this->_errors[$field][] = $error;
  912. }
  913. /**
  914. * Check if a field contains errors
  915. *
  916. * @param string $field The name of field to check
  917. * @return boolean
  918. */
  919. public function field_has_errors($field)
  920. {
  921. return isset($this->_errors[$field]);
  922. }
  923. /**
  924. * Get a flatten array with all errors
  925. *
  926. * @return array
  927. */
  928. public function problems()
  929. {
  930. $flat = array();
  931. foreach ($this->_errors as $field_errors) {
  932. foreach ($field_errors as $error) {
  933. $flat[] = $error;
  934. }
  935. }
  936. return $flat;
  937. }
  938. /**
  939. * Get all errors of one field
  940. *
  941. * @param string $field The name of field
  942. * @return array
  943. */
  944. public function field_problems($field)
  945. {
  946. return isset($this->_errors[$field]) ? $this->_errors[$field] : array();
  947. }
  948. /**
  949. * Override this method to enable custom validations
  950. */
  951. public function validate() { return true; }
  952. //Conversors
  953. public function to_array()
  954. {
  955. return $this->_attributes;
  956. }
  957. public function to_json()
  958. {
  959. return json_encode($this->to_array());
  960. }
  961. //Named Scopes
  962. /**
  963. * Create a new scope into model
  964. *
  965. * @param string $scope The name of new scope
  966. * @param mixed $conditions The conditions of scope, this variable can be like conditions statement of query
  967. * @return void
  968. */
  969. protected function named_scope($scope, $conditions)
  970. {
  971. $this->_scopes[$scope] = $conditions;
  972. }
  973. private function append_scope($scope)
  974. {
  975. $class = get_class($this);
  976. $scoped = new $class;
  977. foreach ($this->_scopes_enabled as $se) {
  978. $scoped->_scopes_enabled[] = $se;
  979. }
  980. $scoped->_scopes_enabled[] = $scope;
  981. return $scoped;
  982. }
  983. //Field act helpers
  984. protected function register_field_as($name, $arguments)
  985. {
  986. $field = $arguments[0];
  987. array_unshift($arguments, $this);
  988. $formats = FieldAct::formats($name);
  989. $this->_field_act[$field] = array($formats, $name, $arguments);
  990. }
  991. //Tree Helpers
  992. /**
  993. * Make a tree like relations to model
  994. *
  995. * @param string $parent_field The name of field that make relation possible
  996. * @return void
  997. */
  998. protected function act_as_tree($parent_field = 'parent_id')
  999. {
  1000. $this->has_many(strtolower(Inflect::pluralize(get_class($this))) . ' as childs', array('foreign_field' => $parent_field));
  1001. $this->belongs_to(strtolower(get_class($this)) . ' as parent', array('foreign_field' => $parent_field));
  1002. }
  1003. //Events
  1004. protected function before_save() {}
  1005. protected function after_save() {}
  1006. protected function before_update() {}
  1007. protected function after_update() {}
  1008. protected function before_create() {}
  1009. protected function after_create() {}
  1010. protected function before_destroy() {}
  1011. protected function after_destroy() {}
  1012. } // END abstract class ActiveRecord
  1013. //Exceptions
  1014. class InvalidRecordException extends Exception {}
  1015. class QueryMismatchParamsException extends Exception {}