* @copyright Copyright (c) 2008, Ian White * @version 0.8 * @package ActiveRecordPackage */ //include_once "ActiveDebugger.php"; include_once "ActiveTable.php"; include_once "ActiveSet.php"; include_once "ActiveRelationshipSets.php"; include_once "ActiveExceptions.php"; include_once "Database.php"; abstract class ActiveRecord implements Iterator, ArrayAccess, Countable { /* * record_cache manages references to all of the objects loaded by ActiveRecord or ActiveSet from * the database so only one copy of a given object exists at a time. * * @access private * @static */ private static $record_cache = array(); /* * set_cache holds references to every set in existence within this http request. * * @access private * @static */ private static $set_cache = array(); /* * The singular array allows you to specify class names in this project * that have a non-standard plural. It converts singular -> plural. * * @access public * @static */ public static $singular = array ( ); /* * The plural array allows you to specify class names in this project * that have a non-standard plural. It converts plural -> singular. * * @access public * @static */ public static $plural = array ( ); protected static $context = 'name'; // // Instance Variables // // Data associated with the ActiveRecord private $values; private $update; protected $table; // Record Relationships public static $alias = array(); public static $protected = array(); public static $private = array(); public static $has_many = array(); public static $has_and_belongs_to_many = array(); public static $has_one = array(); protected $relationships = array(); protected $relationships_processed; protected $db; private $needs_load = false; private $new = true; private $deleted = false; private $iter = 0; protected $attributes; // Cache of ActiveRelationshipSets private $relationship_objects; // // Public Methods // // // Active Record constructor. // Has three optional arguments an // $id - an integer which will cause the record to load itself // from the database as the record with that id // $table - either the string name of the table to use for the ActiveRecord, // or a table object to use. // $relationship - a boolean value that tells the ActiveRecord whether or not to // process the relationships specified by the child class. // public final function __construct() { $this->table = ActiveTable::get(get_class($this)); $this->db = new Database; $this->relationships_processed = false; $this->values = array(); $this->update = array(); $this->init(); } // Called after the constructor to allow the user to perform any initialization they need. public function init() { } // // Save Active Record to the database. // If a record has been loaded from the database and changed, this can be use to write those // changes back to the database. If the ActiveRecord has never been created, this will throw // a NotSavedException. If the database fils to update the record it will throw a // DatabaseException. // public final function save($attrs = NULL) { // If this record has been deleted, it cannot be saved. If it can be saved, call // the user's pre_save method. if(!$this->deleted) { $this->pre_save(); } else { return false; } // If this record has never been saved before, it must be inserted into the database if($this->new) { $column_names = $this->table->get_insert_columns_array($attrs); $sql_values = ""; $sql_columns = ""; // For each column, find any updated values foreach($column_names as $attr_name => $column_name) { $value_name = $this->table->get_real_column($attr_name); if( $this->update[$value_name] || $this->table->is_auto_updated($attr_name) ) { $value = ($this->table->is_auto_updated($attr_name)) ? $this->table->{"create_{$attr_name}"}() : "'" . mysql_real_escape_string($this->values[$value_name]) . "'"; $sql_values .= $value . ","; $sql_columns .= "$column_name,"; } // If a column is required (NOT NULL) but has no value, throw an exception. // This should be modified to see if the database has a default value for this column. ---- else if( !$this->table->is_null($attr_name) ) { throw new ActiveRecordException("The column '$attr_name' in the table '{$this->table}' is required for insert."); } } // Strip the final ',' char from each string $sql_values = chop($sql_values, ","); $sql_columns = chop($sql_columns, ","); // Run the query $query = "INSERT INTO `{$this->table}` ({$sql_columns}) VALUES ({$sql_values})"; $obj_id = $this->db->query($query); // If the object was successfully inserted, its id will be returned. Setup the object // to reflect its new saved state and add the object to the cache. if($obj_id) { $this->new = false; $this->values['id'] = $obj_id; $this->update = NULL; $this->needs_load = false; ActiveRecord::cacheRecord("{$this->table}", $obj_id, $this); } else { $class = get_class($this); throw new Exception("The '$class' object could not be inserted into the database. Query: {$query}"); } return true; } // If this record has been saved, it must be updated else { // Check to see if any updates have been made to this object. If not, return false. if(count($this->update) == 0) return false; // For each column that has been updated, add a clause to the sql query updating that // field in the database $columns = $this->table->get_columns_array($attrs); $updates = ""; foreach ($columns as $attr => $column) { $value_name = $this->table->get_real_column($attr); //echo "checking {$value_name}... {$this->values[$value_name]}
"; if($this->update[$value_name]) { if($this->values[$value_name] != NULL) { $value = mysql_real_escape_string($this->values[$value_name]); $updates .= "{$column} = '{$value}',"; } else { $updates .= "{$column} = NULL,"; } } // If a column is auto-updated, call its auto-update function for the proper // auto-update trigger. if($this->table->is_auto_updated($attr)) { $updates .= $this->table->{"update_{$attr}"}(); } } // Clean the extra ',' from the end of the query. $updates = chop($updates, ","); // Construct the query for the record with this record's id. $query = "UPDATE `{$this->table}` SET $updates WHERE `{$this->table}`.`id`='{$this->values['id']}'"; //echo "$query
"; // Notify any sets with references to this object that it has been altered and may no // longer be a part of the set if the update was successful. Otherwise, throw an // exception. if($this->db->query($query)) { $this->update = array(); } else { throw new DatabaseException("This ActiveRecord object where $where could not be saved in table '{$this->table}'."); } } // Call the user's post_save method. $this->post_save(); } public final function set_save($attrs) { if($this->deleted) { return false; } $this->pre_save(); foreach($attrs as $attr) { unset($this->update[$attr]); } $this->post_save(); } public function pre_save() { } public function post_save() { } [section removed] // // This is a magic method that allows access to the values of the Active Record. // If the table in the database that this ActiveRecord belongs to has a column, it // will be accessible as an instance variable of this class through the __get() method. // This throws an ActiveRecordException if the column does not exist. // protected function attr_get($name) { // Check to see if this record has been deleted. if( $this->deleted ) { throw new ActiveRecordException("This ActiveRecord has been deleted."); } $table_name = $this->table->get_real_column($name); $table_name = ($table_name) ? $table_name : $name; $method = method_exists($this, "_get_{$name}") ? "_get_{$name}" : NULL; $return_value = NULL; // Check to see if the variable requested is the name of a column in the table // definition of this record. $local_vars = get_object_vars($this); if($local_vars[$name] ) { $return_value = $this->$name; if(!$method) return $return_value; } else if( $this->table->exists($name) ) { // Check to see if this column was aliased to hold an object. if( $type = $this->table->alias($name) ) { // If so, get that object. use the column value as the object id, and the alias // value as the object class. if($this->values[$table_name]) { $return_value = ActiveRecord::getRecord($type, $this->values[$table_name]); } else $return_value = false; } else { // Otherwise, return the value of that attribute. if(isset($this->values[$table_name])) { $return_value = $this->values[$table_name]; } else { $return_value = NULL; } } if(!$method) return $return_value; } // If not, check to see if it is the name of an object held as a foreign key by this // object. else if( $this->table->exists("{$name}_id") ) { if(isset($this->values["{$table_name}_id"])) { // Request it from the cache, and return it. $return_value = ActiveRecord::getRecord($name, $this->values["{$table_name}_id"]); } else $return_value = NULL; if(!$method) return $return_value; } // If not, check to see if this record has a relationship by the requested name. else { // Make sure that this record has been created in the database. if(!$this->new) { if(!$this->relationships_processed) $this->process_relationships(); if( isset($this->relationships[$name]) ) { $return_value = $this->relationships[$name]; if(!$method) return $return_value; } } } if($method) return $this->$method($return_value); throw new ActiveRecordException("Column '$name' does not exist in table '{$this->table}'"); } public function __get($name) { if($this->table->is_private($name)) { print_r(debug_backtrace(), true); //throw new ActiveRecordException("Column '$name' does not exist in table '{$this->table}'"); } return $this->attr_get($name); } [section removed] public final function get_type($attr) { $attr = strtolower($attr); if($type = $this->table->getAlias($attr)){ return $type; } else if($this->offsetExists($attr)) { if($this->table->exists($attr)) { return false; } else if($this->relationships[$attr]) { return $attr; } else if($this->table->exists("{$attr}_id")) { return $attr; } } throw new Exception("The attribute '{$attr}' does not exist for the object {$this->table}."); } // // Iterator Methods // public final function rewind() { if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true); $this->iter = 0; } public final function current() { if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true); // If we have already inspected the current element, // it's id will be cached. $field = $this->attributes[$this->iter]; return $this->$field; } public final function key() { if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true); return $this->attributes[$this->iter]; } public final function next() { $this->iter++; } public final function valid() { if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true); return ($this->attributes[$this->iter] != NULL); } // // Countable Methods // public final function count() { if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true); return count($this->attributes); } // // ArrayAccess Methods // public final function offsetExists($index) { if(!$this->attributes) $this->attributes = ActiveRecord::attributes(get_class($this), true, true); return in_array(strtolower($index), $this->attributes); } public final function offsetGet($index) { return $this->$index; } public final function offsetSet($index, $record) { return ($this->$index = $record); } public final function offsetUnset($index) { return ($this->$index = NULL); } // // Protected Methods // // // All auto-update variables need create_[varname]() and update_[varname]() methods. // protected final function is_private($attr) { $table_private = $this->table->get_private_array(); $class = get_class_vars(get_class($this)); if(in_array($attr, $class['private']) || $this->table->is_private($attr)) { return true; } else { return false; } } protected final function is_protected($attr) { $table_protected = $this->table->get_protected_array(); $class = get_class_vars(get_class($this)); if(in_array($attr, $class['protected']) || $this->table->is_protected($attr)) { return true; } else { return $this->is_private($attr); } } [section removed] // // Static Methods // public static final function attributes($object, $include_immutable = false, $include_ids=false) { $object = ucfirst($object); $table = ActiveTable::get($object); $reflect = new ReflectionClass($object); $static_properties = $reflect->getStaticProperties(); $all_variables = $reflect->getProperties(); $all_methods = $reflect->getMethods(); $ar_reflect = new ReflectionClass("ActiveRecord"); $ar_variables = $ar_reflect->getProperties(); // Get public object attributes explosed as instance variables $var_attrs = array(); foreach($all_variables as $var) { if(!$var->isStatic()) { $var_attrs[] = $var->getName(); } } $ar_vars = array(); foreach($ar_variables as $var) { $ar_vars[] = $var->getName(); } // Get Relationship attributes $relationship_attrs = array_merge($static_properties['has_one'], $static_properties['has_many'], $static_properties['has_and_belongs_to_many'] ); $var_attrs = array_merge(array_diff($var_attrs, $ar_vars), $relationship_attrs); // Get public object attributes exposed via _get_ and _set_ methods $method_attrs = array(); foreach($all_methods as $method) { if($method->isPublic() && !$method->isStatic() ) { if($include_immutable) { if(substr($method->getName(), 0, 5) == "_set_") { $method_attrs[] = substr($method->getName(), 5); } } if(substr($method->getName(), 0, 5) == "_get_") { $method_attrs[] = substr($method->getName(), 5); } } } // Get attributes defined by the objects' table $declared_table_attrs = array_keys($table->get_columns_array()); // Get undeclared table attributes $table_attrs = array(); foreach($declared_table_attrs as $attr) { if(substr($attr, -3) == '_id') { if($include_ids) $table_attrs[] = $attr; $table_attrs[] = substr($attr, 0, -3); } else if($attr == 'id' && !$include_ids) { continue; } else { $table_attrs[] = $attr; } } $all_attrs = array_unique(array_merge($table_attrs, $method_attrs, $var_attrs)); $non_private_attrs = array_diff($all_attrs, $static_properties['private']); if(!$include_immutable) { $table_protected_attrs = $table->get_protected_array(); $class_protected_attrs = $static_properties['protected']; $all_protected_attrs = array_unique(array_merge($table_protected_attrs, $class_protected_attrs)); $public_attrs = array_diff($non_private_attrs, $all_protected_attrs); return $public_attrs; } else { return $non_private_attrs; } } public function getForm($include_protected=false) { $attrs = ActiveRecord::attributes(get_class($this), $include_protected, false); $form = array(); foreach($attrs as $attr) { if(method_exists($this, "_form_{$attr}")) { $method = "_form_{$attr}"; $form[$attr] = $this->$method(); } else if($this->is_protected($attr)) { $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>"{$this->$attr}", 'editable'=>false); } else if($this->table->exists($attr)) { if($alias = $this->table->getAlias($attr)) { $accepts = ActiveRecord::pluralize($alias); $finder = "Find_{$accepts}_by_id"; $set = call_user_func(array($finder, 'with'), array('value'=>0, 'bool'=>'>')); $html = ""; if(!$this->table->exists($attr) || $this->table->is_null($attr)) { if(!$this->$attr) { $html .= ""; } else { $html .= ""; } } foreach($set as $record) { if($this->$attr && $this->$attr->id == $record->id) { $html .= ""; } else { $html .= ""; } } $html = ""; $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>$html, 'editable'=>true); } else { if($this->table->get_type($attr) == 'bool') { $checked = ($this->$attr) ? "checked=\"checked\"" : ""; $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>"", 'editable'=>true); } else { $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>"$attr}\" />", 'editable'=>true); } } } else if(is_a($this->$attr, 'ActiveSet')) { $accepts = ActiveRecord::pluralize($this->$attr->accepts()); $finder = "Find_{$accepts}_by_id"; $set = call_user_func(array($finder, 'with'), array('value'=>0, 'bool'=>'>')); $html = ""; if(!$this->table->exists($attr) || $this->table->is_null($attr)) { if(count($this->$attr)) { $html .= ""; } else { $html .= ""; } } foreach($set as $record) { if($this->$attr->is_member($record)) { $html .= ""; } else { $html .= ""; } } if(is_a($this->$attr, 'ActiveHasOneSet')) { $html = ""; } else { $html = ""; } $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>$html, 'editable'=>true); } else if($this->table->exists("{$attr}_id")) { $accepts = ActiveRecord::pluralize($attr); $finder = "Find_{$accepts}_by_id"; $table_attr = "{$attr}_id"; $set = call_user_func(array($finder, 'with'), array('value'=>0, 'bool'=>'>')); $html = ""; if(!$this->table->exists($table_attr) || $this->table->is_null($table_attr)) { if($this->$table_attr > 0) { $html .= ""; } else { $html .= ""; } } foreach($set as $record) { if($this->$table_attr && $this->$attr->id == $record->id) { $html .= ""; } else { $html .= ""; } } $html = ""; $form[$attr] = array('title'=>ucwords(ActiveRecord::humanize($attr)), 'element'=>$html, 'editable'=>true); } } return $form; } [section removed] public function __toString() { $class = ucwords(ActiveRecord::humanize("{$this->table}")); return "$class #{$this->id}"; } public static function pluralize($name) { if(!$name) return ""; $name = strtolower($name); $name_array = explode("_", $name); $last_name_word = array_splice($name_array, -1, 1); if((substr($last_name_word[0],-1) != "s" || array_key_exists($last_name_word[0], ActiveRecord::$singular)) && !array_key_exists($last_name_word[0], ActiveRecord::$plural)) { // Find all instances $last_name_word = array_key_exists($last_name_word[0], ActiveRecord::$singular) ? ActiveRecord::$singular[$last_name_word[0]] : $last_name_word[0] . 's'; $name_start = (count($name_array) > 1) ? implode("_", $name_array) : $name_array[0]; $name_start .= ($name_start) ? '_' : ''; $name = $name_start . $last_name_word; } // Otherwise, if not pluaral and class name starts Find_x_by_ else { // Find only first instance $name_start = implode("_", $name_array); $name_start .= ($name_start) ? "_" : ""; $name = $name_start . $last_name_word[0]; } return $name; } public static function singularize($name) { if(!$name) return ""; $name = strtolower($name); $name_array = explode("_", $name); $last_name_word = array_splice($name_array, -1, 1); if((substr($last_name_word[0],-1) == "s" || array_key_exists($last_name_word[0], ActiveRecord::$plural)) && !array_key_exists($last_name_word[0], ActiveRecord::$singular)) { // Find all instances $last_name_word = array_key_exists($last_name_word[0], ActiveRecord::$plural) ? ActiveRecord::$plural[$last_name_word[0]] : substr($last_name_word[0], 0, -1); $name_start = (count($name_array) > 1) ? implode("_", $name_array) : $name_array[0]; $name_start .= ($name_start) ? '_' : ''; $name = $name_start . $last_name_word; } // Otherwise, if not pluaral and class name starts Find_x_by_ else { // Find only first instance $name_start = implode("_", $name_array); $name_start .= ($name_start) ? "_" : ""; $name = $name_start . $last_name_word[0]; } return $name; } public static function humanize($name) { return strtr($name, "_", " "); } public static function computerize($name) { return strtr($name, " ", "_"); } public static function link_context() { ActiveRecord::$context = 'link'; } public static function name_context() { ActiveRecord::$context = 'name'; } } [section removed] ?>