PageRenderTime 46ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/inc/classes/activerecord/ActiveRecord.php

https://github.com/petski/SnapElements
PHP | 433 lines | 369 code | 18 blank | 46 comment | 45 complexity | a7fc474ca90198a834517143541be3d0 MD5 | raw file
  1. <?php
  2. if (!class_exists('ActiveRecordInflector'))
  3. require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'inflector.php';
  4. require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Association.php';
  5. require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'BelongsTo.php';
  6. require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'HasMany.php';
  7. require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'HasOne.php';
  8. require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'config.php';
  9. require_once dirname(__FILE__) . DIRECTORY_SEPARATOR . 'db_adapters' .DIRECTORY_SEPARATOR . AR_ADAPTER.'.php';
  10. class ActiveRecord {
  11. protected $columns = array();
  12. protected $attributes = array();
  13. protected $associations = array();
  14. protected $is_modified = false;
  15. protected $frozen = false;
  16. protected $primary_key = 'id';
  17. protected $table_name;
  18. protected static $query_count = 0;
  19. protected static $dbh;
  20. public $new_record = true;
  21. private $assoc_types = array('belongs_to', 'has_many', 'has_one');
  22. function __construct($params=null, $new_record=true, $is_modified=false) {
  23. /* setup associations */
  24. foreach ($this->assoc_types as $type) {
  25. if (isset($this->$type)) {
  26. $class_name = ActiveRecordInflector::classify($type);
  27. foreach ($this->$type as $assoc) {
  28. $assoc = self::decode_if_json($assoc);
  29. /* handle association sent in as array with options */
  30. if (is_array($assoc)) {
  31. $key = key($assoc);
  32. $this->$key = new $class_name($this, $key, current($assoc));
  33. }
  34. else
  35. $this->$assoc = new $class_name($this, $assoc);
  36. }
  37. }
  38. }
  39. /* setup attributes */
  40. if (is_array($params)) {
  41. foreach ($params as $key => $value)
  42. $this->$key = $value;
  43. $this->is_modified = $is_modified;
  44. $this->new_record = $new_record;
  45. }
  46. }
  47. function __get($name) {
  48. if (array_key_exists($name, $this->attributes))
  49. return $this->attributes[$name];
  50. elseif (array_key_exists($name, $this->associations))
  51. return $this->associations[$name]->get($this);
  52. elseif (in_array($name, $this->columns))
  53. return null;
  54. elseif (preg_match('/^(.+?)_ids$/', $name, $matches)) {
  55. /* allow for $p->comment_ids type gets on HasMany associations */
  56. $assoc_name = ActiveRecordInflector::pluralize($matches[1]);
  57. if ($this->associations[$assoc_name] instanceof HasMany)
  58. return $this->associations[$assoc_name]->get_ids($this);
  59. }
  60. throw new ActiveRecordException("attribute called '$name' doesn't exist",
  61. ActiveRecordException::AttributeNotFound);
  62. }
  63. function __set($name, $value) {
  64. if ($this->frozen)
  65. throw new ActiveRecordException("Can not update $name as object is frozen.", ActiveRecordException::ObjectFrozen);
  66. /* allow for $p->comment_ids type sets on HasMany associations */
  67. if (preg_match('/^(.+?)_ids$/', $name, $matches)) {
  68. $assoc_name = ActiveRecordInflector::pluralize($matches[1]);
  69. }
  70. if (in_array($name, $this->columns)) {
  71. $this->attributes[$name] = $value;
  72. $this->is_modified = true;
  73. }
  74. elseif ($value instanceof Association) {
  75. /* call from constructor to setup association */
  76. $this->associations[$name] = $value;
  77. }
  78. elseif (array_key_exists($name, $this->associations)) {
  79. /* call like $comment->post = $mypost */
  80. $this->associations[$name]->set($value, $this);
  81. }
  82. elseif (isset($assoc_name)
  83. && array_key_exists($assoc_name, $this->associations)
  84. && $this->associations[$assoc_name] instanceof HasMany) {
  85. /* allow for $p->comment_ids type sets on HasMany associations */
  86. $this->associations[$assoc_name]->set_ids($value, $this);
  87. }
  88. else
  89. throw new ActiveRecordException("attribute called '$name' doesn't exist",
  90. ActiveRecordException::AttributeNotFound);
  91. }
  92. /* on any ActiveRecord object we can make method calls to a specific assoc.
  93. Example:
  94. $p = Post::find(1);
  95. $p->comments_push($comment);
  96. This calls push([$comment], $p) on the comments association
  97. */
  98. function __call($name, $args) {
  99. // find longest available association that matches beginning of method
  100. $longest_assoc = '';
  101. foreach (array_keys($this->associations) as $assoc) {
  102. if (strpos($name, $assoc) === 0 &&
  103. strlen($assoc) > strlen($longest_assoc)) {
  104. $longest_assoc = $assoc;
  105. }
  106. }
  107. if ($longest_assoc !== '') {
  108. list($null, $func) = explode($longest_assoc.'_', $name, 2);
  109. return $this->associations[$longest_assoc]->$func($args, $this);
  110. }
  111. else {
  112. throw new ActiveRecordException("method or association not found for ($name)", ActiveRecordException::MethodOrAssocationNotFound);
  113. }
  114. }
  115. /* various getters */
  116. function get_columns() { return $this->columns; }
  117. function get_primary_key() { return $this->primary_key; }
  118. function is_frozen() { return $this->frozen; }
  119. function is_new_record() { return $this->new_record; }
  120. function is_modified() { return $this->is_modified; }
  121. function set_modified($val) { $this->is_modified = $val; }
  122. static function get_query_count() { return self::$query_count; }
  123. /* Json helper will decode a string as Json if it starts with a [ or {
  124. if the Json::decode fails, we return the original value
  125. */
  126. static function decode_if_json($json) {
  127. require_once dirname(__FILE__) .DIRECTORY_SEPARATOR. 'Zend' .DIRECTORY_SEPARATOR. 'Json.php';
  128. if (is_string($json) && preg_match('/^\s*[\{\[]/', $json) != 0) {
  129. try {
  130. $json = Zend_Json::decode($json);
  131. } catch (Zend_Json_Exception $e) { }
  132. }
  133. return $json;
  134. }
  135. /*
  136. DB specific stuff
  137. */
  138. static function &get_dbh() {
  139. if (!self::$dbh) {
  140. self::$dbh = call_user_func_array(array(AR_ADAPTER."Adapter", __FUNCTION__),
  141. array(AR_HOST, AR_DB, AR_USER, AR_PASS, AR_DRIVER));
  142. }
  143. return self::$dbh;
  144. }
  145. static function query($query) {
  146. $dbh =& self::get_dbh();
  147. #var_dump($query);
  148. self::$query_count++;
  149. return call_user_func_array(array(AR_ADAPTER."Adapter", __FUNCTION__),
  150. array($query, $dbh));
  151. }
  152. static function quote($string, $type = null) {
  153. $dbh =& self::get_dbh();
  154. return call_user_func_array(array(AR_ADAPTER.'Adapter', __FUNCTION__),
  155. array($string, $dbh, $type));
  156. }
  157. static function last_insert_id($resource = null) {
  158. $dbh =& self::get_dbh();
  159. return call_user_func_array(array(AR_ADAPTER.'Adapter', __FUNCTION__),
  160. array($dbh, $resource));
  161. }
  162. function update_attributes($attributes) {
  163. foreach ($attributes as $key => $value)
  164. $this->$key = $value;
  165. return $this->save();
  166. }
  167. function save() {
  168. if (method_exists($this, 'before_save'))
  169. $this->before_save();
  170. foreach ($this->associations as $name => $assoc) {
  171. if ($assoc instanceOf BelongsTo && $assoc->needs_saving()) {
  172. /* save the object referenced by this association */
  173. $this->$name->save();
  174. /* after our save, $this->$name might have new id;
  175. we want to update the foreign key of $this to match;
  176. we update this foreign key already as a side-effect
  177. when calling set() on an association
  178. */
  179. $this->$name = $this->$name;
  180. }
  181. }
  182. if ($this->new_record) {
  183. if (method_exists($this, 'before_create'))
  184. $this->before_create();
  185. /* insert new record */
  186. foreach ($this->columns as $column) {
  187. if ($column == $this->primary_key) continue;
  188. $columns[] = '`' . $column . '`';
  189. if (is_null($this->$column))
  190. $values[] = 'NULL';
  191. else
  192. $values[] = self::quote($this->$column);
  193. }
  194. $columns = implode(", ", $columns);
  195. $values = implode(", ", $values);
  196. $query = "INSERT INTO {$this->table_name} ($columns) VALUES ($values)";
  197. $res = self::query($query);
  198. $this->{$this->primary_key} = self::last_insert_id();
  199. $this->new_record = false;
  200. $this->is_modified = false;
  201. if (method_exists($this, 'after_create'))
  202. $this->after_create();
  203. }
  204. elseif ($this->is_modified) {
  205. if (method_exists($this, 'before_update'))
  206. $this->before_update();
  207. /* update existing record */
  208. $col_vals = array();
  209. foreach ($this->columns as $column) {
  210. if ($column == $this->primary_key) continue;
  211. $value = is_null($this->$column) ? 'NULL' : self::quote($this->$column);
  212. $col_vals[] = "`$column` = $value";
  213. }
  214. $columns_values = implode(", ", $col_vals);
  215. $query = "UPDATE {$this->table_name} SET $columns_values "
  216. . " WHERE {$this->primary_key} = {$this->{$this->primary_key}} "
  217. . " LIMIT 1";
  218. $res = self::query($query);
  219. $this->new_record = false;
  220. $this->is_modified = false;
  221. if (method_exists($this, 'after_update'))
  222. $this->after_update();
  223. }
  224. foreach ($this->associations as $name => $assoc) {
  225. if ($assoc instanceOf HasOne && $assoc->needs_saving()) {
  226. /* again sorta weird, this will update foreign key as needed */
  227. $this->$name = $this->$name;
  228. /* save the object referenced by this association */
  229. $this->$name->save();
  230. }
  231. elseif ($assoc instanceOf HasMany && $assoc->needs_saving()) {
  232. $assoc->save_as_needed($this);
  233. }
  234. }
  235. if (method_exists($this, 'after_save'))
  236. $this->after_save();
  237. }
  238. function destroy() {
  239. if (method_exists($this, 'before_destroy'))
  240. $this->before_destroy();
  241. foreach ($this->associations as $name => $assoc) {
  242. $assoc->destroy($this);
  243. }
  244. $query = "DELETE FROM {$this->table_name} "
  245. . "WHERE {$this->primary_key} = {$this->{$this->primary_key}} "
  246. . "LIMIT 1";
  247. self::query($query);
  248. $this->frozen = true;
  249. if (method_exists($this, 'after_destroy'))
  250. $this->after_destroy();
  251. return true;
  252. }
  253. /* transform_row -- transforms a row into its various objects
  254. accepts: row from SQL query (array), lookup array of column names
  255. return: object keyed by table names and real columns names
  256. */
  257. static function transform_row($row, $col_lookup) {
  258. $object = array();
  259. foreach ($row as $col_name => $col_value) {
  260. /* set $object["table_name"]["column_name"] = $col_value */
  261. $object[$col_lookup[$col_name]["table"]][$col_lookup[$col_name]["column"]] = $col_value;
  262. }
  263. return $object;
  264. }
  265. static function find($class, $id, $options=null) {
  266. $class = str_replace('Base', '', $class);
  267. $query = self::generate_find_query($class, $id, $options);
  268. $rows = self::query($query['query']);
  269. #var_dump($query['query']);
  270. #$objects = self::transform_rows($rows, $query['column_lookup']);
  271. $base_objects = array();
  272. foreach ($rows as $row) {
  273. /* if we've done a join we have some fancy footwork to do
  274. we're going to process one row at a time.
  275. each row has a "base" object and objects that've been joined.
  276. the base object is whatever class we've been passed as $class.
  277. we only want to create one instance of each unique base object.
  278. as we see more rows we may be re-using an exising base object to
  279. append more join objects to its association.
  280. */
  281. if (count($query['column_lookup']) > 0) {
  282. $objects = self::transform_row($row, $query['column_lookup']);
  283. $ob_key = md5(serialize($objects[ActiveRecordInflector::tableize($class)]));
  284. /* set cur_object to base object for this row; reusing if possible */
  285. if (array_key_exists($ob_key, $base_objects)) {
  286. $cur_object = $base_objects[$ob_key];
  287. }
  288. else {
  289. $cur_object = new $class($objects[ActiveRecordInflector::tableize($class)], false);
  290. $base_objects[$ob_key] = $cur_object;
  291. }
  292. /* now add association data as needed */
  293. foreach ($objects as $table_name => $attributes) {
  294. if ($table_name == ActiveRecordInflector::tableize($class)) continue;
  295. foreach ($cur_object->associations as $assoc_name => $assoc) {
  296. if ($table_name == ActiveRecordInflector::pluralize($assoc_name))
  297. $assoc->populate_from_find($attributes);
  298. }
  299. }
  300. }
  301. else {
  302. $item = new $class($row, false);
  303. array_push($base_objects, $item);
  304. }
  305. }
  306. if (count($base_objects) == 0 && (is_array($id) || is_numeric($id)))
  307. throw new ActiveRecordException("Couldn't find anything.", ActiveRecordException::RecordNotFound);
  308. return (is_array($id) || $id == 'all') ?
  309. array_values($base_objects) :
  310. array_shift($base_objects);
  311. }
  312. function generate_find_query($class_name, $id, $options=null) {
  313. //$dbh =& $this->get_dbh();
  314. $item = new $class_name;
  315. $options = self::decode_if_json($options);
  316. /* first sanitize what we can */
  317. if (is_array($id)) {
  318. foreach ($id as $k => $v) {
  319. $id[$k] = self::quote($v);
  320. }
  321. }
  322. elseif ($id != 'all' && $id != 'first') {
  323. $id = self::quote($id);
  324. }
  325. /* regex for limit, order, group */
  326. $regex = '/^[A-Za-z0-9\-_ ,\(\)]+$/';
  327. if (!isset($options['limit']) || !preg_match($regex, $options['limit']))
  328. $options['limit'] = '';
  329. if (!isset($options['order']) || !preg_match($regex, $options['order']))
  330. $options['order'] = '';
  331. if (!isset($options['group']) || !preg_match($regex, $options['group']))
  332. $options['group'] = '';
  333. if (!isset($options['offset']) || !is_numeric($options['offset']))
  334. $options['offset'] = '';
  335. $select = '*';
  336. if (is_array($id))
  337. $where = "{$item->primary_key} IN (" . implode(",", $id) . ")";
  338. elseif ($id == 'first')
  339. $limit = '1';
  340. elseif ($id != 'all')
  341. $where = "{$item->table_name}.{$item->primary_key} = $id";
  342. if (isset($options['conditions']))
  343. $where = (isset($where) && $where) ? $where . " AND (" . $options['conditions'] .")"
  344. : $options['conditions'];
  345. if ($options['offset'])
  346. $offset = $options['offset'];
  347. if ($options['limit'] && !isset($limit))
  348. $limit = $options['limit'];
  349. if (isset($options['select']))
  350. $select = $options['select'];
  351. $joins = array();
  352. $tables_to_columns = array();
  353. $column_lookup = array();
  354. if (isset($options['include'])) {
  355. array_push($tables_to_columns,
  356. array(ActiveRecordInflector::tableize(get_class($item)) => $item->get_columns()));
  357. $includes = preg_split('/[\s,]+/', $options['include']);
  358. // get join part of query from association and column names
  359. foreach ($includes as $include) {
  360. if (isset($item->associations[$include])) {
  361. list($cols, $join) = $item->associations[$include]->join();
  362. array_push($joins, $join);
  363. array_push($tables_to_columns, $cols);
  364. }
  365. }
  366. // set the select variable so all column names are unique
  367. $selects = array();
  368. foreach ($tables_to_columns as $table_key => $columns) {
  369. foreach ($columns as $table => $cols)
  370. foreach ($cols as $key => $col) {
  371. array_push($selects, "$table.`$col` AS t{$table_key}_r$key");
  372. $column_lookup["t{$table_key}_r{$key}"]["table"] = $table;
  373. $column_lookup["t{$table_key}_r{$key}"]["column"] = $col;
  374. }
  375. }
  376. $select = implode(", ", $selects);
  377. }
  378. // joins (?), include
  379. $query = "SELECT $select FROM {$item->table_name}";
  380. $query .= (count($joins) > 0) ? " " . implode(" ", $joins) : "";
  381. $query .= (isset($where)) ? " WHERE $where" : "";
  382. $query .= ($options['group']) ? " GROUP BY {$options['group']}" : "";
  383. $query .= ($options['order']) ? " ORDER BY {$options['order']}" : "";
  384. $query .= (isset($limit) && $limit) ? " LIMIT $limit" : "";
  385. $query .= (isset($offset) && $offset) ? " OFFSET $offset" : "";
  386. return array('query' => $query, 'column_lookup' => $column_lookup);
  387. }
  388. }
  389. class ActiveRecordException extends Exception {
  390. const RecordNotFound = 0;
  391. const AttributeNotFound = 1;
  392. const UnexpectedClass = 2;
  393. const ObjectFrozen = 3;
  394. const HasManyThroughCantAssociateNewRecords = 4;
  395. const MethodOrAssocationNotFound = 5;
  396. }
  397. interface DatabaseAdapter {
  398. static function get_dbh($host="localhost", $db=null, $user=null, $password=null, $driver="mysql");
  399. static function query($query, $dbh=null);
  400. static function quote($string, $dbh=null, $type=null);
  401. static function last_insert_id($dbh=null, $resource=null);
  402. }
  403. ?>