* @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]
?>