<?php // -*- mode:php; tab-width:4; indent-tabs-mode:t; c-basic-offset:4; -*-
#CMS - CMS Made Simple
#(c)2004-2007 by Ted Kulp (ted@cmsmadesimple.org)
#This project's homepage is: http://cmsmadesimple.org
#
#This program is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#$Id$
/**
* Base class for all things ORM. All classes that want to be part of
* the ORM system need to extend this class. They also need to call the
* static register_orm_class() method after the class is defined in order
* to be reigstered for the system (and allow things like find_by_* to
* work).
*
* @author Ted Kulp
* @since 2.0
* @version $Revision$
* @modifiedby $LastChangedBy$
* @lastmodified $Date$
* @license GPL
**/
abstract class CmsObjectRelationalMapping extends CmsObject implements ArrayAccess
{
/**
* The ORM version number. This basically is a number that
* should be incremented when an object has a major change.
*
* Adding or removing fields from the table doesn't really
* constitute a change. It's more like changes to a table
* name, enabling or disabling a sequence or changing the
* name of an id field. Since some areas of CMSMS store
* serialized versions of objects, there could be a
* discrepency between the current version and the version
* that was just unserialized. We can use the version number
* to test to see if it's the same.
*
* If not set, then it defaults to 1. If you never really
* change the object, then you shouldn't need to modify it.
*/
var $orm_version = 1;
/**
* Used to define any default settings for this object. Not
* all fields need to be defined, as they'll come out of the
* database field names anyway.
*/
var $params = array();
/**
* Used to define a variation between a database field and
* what the property name should be. Takes a hash of
* 'database field name' => 'property name'
*/
var $field_maps = array();
/**
* Used to define a different table name for this object if it
* doesn't match the predetermined name based on the object's class
* name. The prefix in config.php will be applied automatically.
*/
var $table = '';
/**
* Used to define an alternate field for the id. This basically says
* whether or not we insert or update.
*/
var $id_field = 'id';
/**
* Used to define a sequence to use for creating a new id to use. If
* blank, then the auto increment for the database is used for the id.
*/
var $sequence = '';
/**
* Used to store validation error messages if a save does not go as
* expected.
*/
var $validation_errors = array();
/**
* Used to store any association relationships.
**/
var $associations = array();
/**
* Used to define which field holds the record create date.
*/
var $create_date_field = 'create_date';
/**
* Used to define which field holds the record modified date.
*/
var $modified_date_field = 'modified_date';
/**
* Used to only update objects (not insert) that have changed
* any of their properties. This means you should be using properites
* ($obj->some_field or $obj->SetSomeField()) so that the dirty bit
* gets flipped properly.
*/
var $dirty = false;
/**
* Used in situations where we're doing a bit of polymorphism. The type
* field will store the name of the class that this object currently is.
* Then, when it's loaded, we will automatically instantiate that type of
* object again and not just go by the name of the class that called the
* find. If you want this functionality to not exist, make this variable
* blank.
*/
var $type_field = 'type';
function __construct()
{
parent::__construct();
//Setup the predefined fields in the $params array. Relax: The definitions are cached.
$fields = $this->get_columns_in_table();
if (count($fields) > 0) {
foreach ($fields as $field=>$data) {
if (array_key_exists($field, $this->field_maps)) $field = $this->field_maps[$field];
if (!array_key_exists($field, $this->params)) {
$this->params[$field] = '';
}
}
}
$this->setup(true);
}
public function __wakeup()
{
$this->setup();
}
/**
* Method for setting up various pieces of the object. This is mainly used to setup
* associations on objects that will be used in sessions so that they get properly setup
* again after the object comes out of serialization.
*
* @param bool Whether or not this is the first time this was called from the constructor
* @return void
* @author Ted Kulp
**/
public function setup($first_time = false)
{
}
/**
* Used to create a has_many association. This should be called in the constructor of
* the data object. Any associations are lazy loaded on the first call to them and are
* cached for the life of the object.
*
* @param string The name of the association. It will then be called via
* $obj->assication_name. Make sure it's not the same name as a
* parameter, or it will never get called.
* @param string The name of the class on the other end of the association. This should
* be the name that would be used when calling from the orm (cmsms()->child_class_name).
* @param string The name of the field in the association class that contains the matching id to
* this object.
* @param array Extra parameters that should be sent to the query. Allows for things like order by, joins,
* etc when the association is queried.
* @return void
* @author Ted Kulp
**/
protected function create_has_many_association($association_name, $child_class_name, $child_field, $extra_params = array())
{
cms_orm()->create_has_many_association($this, $association_name, $child_class_name, $child_field, $extra_params);
}
/**
* Used to create a has_one association. This should be called in the constructor of
* the data object. Any associations are lazy loaded on the first call to them and are
* cached for the life of the object.
*
* @param string The name of the association. It will then be called via
* $obj->assication_name. Make sure it's not the same name as a
* parameter, or it will never get called.
* @param string The name of the class on the other end of the association. This should
* be the name that would be used when calling from the orm (cmsms()->child_class_name).
* @param string The name of the field in the association class that contains the matching id to
* this object.
* @param array Extra parameters that should be sent to the query. Allows for things like order by, joins,
* etc when the association is queried.
* @return void
* @author Ted Kulp
**/
protected function create_has_one_association($association_name, $child_class_name, $child_field, $extra_params = array())
{
cms_orm()->create_has_one_association($this, $association_name, $child_class_name, $child_field, $extra_params);
}
/**
* Used to create a belongs_to association. This should be called in the constructor of
* the data object. Any associations are lazy loaded on the first call to them and are
* cached for the life of the object.
*
* @param string The name of the association. It will then be called via
* $obj->assication_name. Make sure it's not the same name as a
* parameter, or it will never get called.
* @param string The name of the class on the other end of the association. This should
* be the name that would be used when calling from the orm (cmsms()->belongs_to_class_name).
* @param string The name of the field in the this class that contains the matching id to
* the given belongs_to_class_name.
* @param array Extra parameters that should be sent to the query. Allows for things like order by, joins,
* etc when the association is queried.
* @return void
* @author Ted Kulp
**/
protected function create_belongs_to_association($association_name, $belongs_to_class_name, $child_field, $extra_params = array())
{
cms_orm()->create_belongs_to_association($this, $association_name, $belongs_to_class_name, $child_field, $extra_params);
}
/**
* Used to create a belongs_to association. This should be called in the constructor of
* the data object. Any associations are lazy loaded on the first call to them and are
* cached for the life of the object.
*
* @param string The name of the association. It will then be called via
* $obj->assication_name. Make sure it's not the same name as a
* parameter, or it will never get called.
* @param string The name of the class on the other end of the association. This should
* be the name that would be used when calling from the orm (cmsms()->belongs_to_class_name).
* @param string The name of the field in the this class that contains the matching id to
* the given belongs_to_class_name.
* @param array Extra parameters that should be sent to the query. Allows for things like order by, joins,
* etc when the association is queried.
* @return void
* @author Ted Kulp
**/
protected function create_has_and_belongs_to_many_association($association_name, $child_class, $join_table, $join_other_id_field, $join_this_id_field, $extra_params = array())
{
cms_orm()->create_has_and_belongs_to_many_association($this, $association_name, $child_class, $join_table, $join_other_id_field, $join_this_id_field, $extra_params);
}
/**
* Used to see if an association has been cached on the object yet.
*
* @return boolean Whether or not the association has been cached
* @author Ted Kulp
**/
public function has_association($name)
{
return array_key_exists($name, $this->associations);
}
/**
* Get the association that has been previously cached on this object.
*
* @return mixed The array or object that was cached
* @author Ted Kulp
**/
public function get_association($name)
{
return $this->associations[$name];
}
/**
* Set the object or array to cache so we don't need a call to the database
* if it's used again.
*
* @return void
* @author Ted Kulp
**/
public function set_association($name, $value)
{
$this->associations[$name] = $value;
}
protected function assign_acts_as($name)
{
cms_orm()->create_acts_as($this, $name);
}
/**
* Used for the ArrayAccessor implementation.
*
* @param string The key to set with the given value
* @param mixed The value to set for the given key
* @return void
* @author Ted Kulp
**/
function offsetSet($key, $value)
{
if (array_key_exists($key, $this->field_maps)) $key = $this->field_maps[$key];
$this->params[$key] = $value;
$this->dirty = true;
}
/**
* Used for the ArrayAccessor implementation.
*
* @param string The key to look up
* @return mixed The value of the $obj['field']
* @author Ted Kulp
**/
function offsetGet($key)
{
if (array_key_exists($key, $this->params))
{
return $this->params[$key];
}
}
/**
* Used for the ArrayAccessor implementation.
*
* @param string The key to unset
* @return bool Whether or not it does exist
* @author Ted Kulp
**/
function offsetUnset($key)
{
if (array_key_exists($key, $this->params))
{
unset($this->params[$key]);
}
}
/**
* Used for the ArrayAccessor implementation.
*
* @param string The key to lookup to see if it exists
* @return bool Whether or not it does exist
* @author Ted Kulp
**/
function offsetExists($offset)
{
return array_key_exists($offset, $this->params);
}
/**
* "Static" method to register this class with the orm system. This must be called
* right after an ORM class has been defined.
*
* @param $classname Name of the class to register with the ORM system
* @return void
* @author Ted Kulp
*/
static function register_orm_class($classname)
{
global $gCms;
$ormclasses =& $gCms->orm;
$name = underscore($classname);
$ormclasses[$name] = new $classname;
return FALSE;
}
/**
* Getter overload method. Called when an $obj->field and field
* does not exist in the object's variable list.
*
* @param string The field to look up
* @return mixed The value for that field, if it exists
* @author Ted Kulp
**/
function __get($n)
{
if (array_key_exists($n, $this->params))
{
if (method_exists($this, 'get_' . $n))
return call_user_func_array(array($this, 'get_'.$n), array($val));
else
return $this->params[$n];
}
if (cms_orm()->has_association($this, $n))
{
return cms_orm()->process_association($this, $n);
}
}
/**
* Setter overload method. Called when an $obj->field = '' and field
* does not exist in the object's variable list.
*
* @param string The field to set
* @param mixed The value to set for said field
* @return void
* @author Ted Kulp
**/
function __set($n, $val)
{
if (array_key_exists($n, $this->field_maps)) $n = $this->field_maps[$n];
if (method_exists($this, 'set_' . $n))
call_user_func_array(array($this, 'set_'.$n), array($val));
else
$this->params[$n] = $val;
$this->dirty = true;
}
/**
* Caller overload method. Called when an $obj->method() is called and
* that method does not exist.
*
* @param string The name of the method called
* @param array The parameters sent along with that method call
* @return mixed The result of the method call
* @author Ted Kulp
**/
function __call($function, $arguments)
{
$function_converted = underscore($function);
if (array_key_exists($function, $this->field_maps)) $function_converted = $this->field_maps[$function];
if (starts_with($function, 'find_by_'))
{
return $this->find_by_($function, $arguments);
}
else if (starts_with($function, 'find_all_by_'))
{
return $this->find_all_by_($function, $arguments);
}
else if (starts_with($function, 'find_count_by_'))
{
return $this->find_count_by_($function, $arguments);
}
else if (starts_with($function_converted, 'set_'))
{
#This handles the SetSomeParam() dynamic function calls
return $this->__set(substr($function_converted, 4), $arguments[0]);
}
else
{
//It's possible an acts_as class has this method
$acts_as_list = cms_orm()->get_acts_as($this);
if (count($acts_as_list) > 0)
{
foreach ($acts_as_list as $one_acts_as)
{
if (method_exists($one_acts_as, $function))
{
return call_user_func_array(array($one_acts_as, $function), $arguments);
}
}
}
#This handles the SomeParam() dynamic function calls
return $this->__get($function_converted);
}
}
/**
* Private helper function for processing dynaimc find_by methods. It essentially does several things...
* 1. Split out any "and" or "or" clauses in a dynamic find method
* 2. Pops the corresponding arguments off of the array so they don't get processed further
* 3. Creates the conditions clause and returns it
*
* @param string The field (or fields in the case of an "and" or "or" lookup)
* @param array Reference to the arguments passed to the method. The array is modified as necessary.
* @return array Conditions clause after processing
* @author Ted Kulp
**/
private function split_conditions($field, &$arguments)
{
//Figure out if we need to replace the field from the field mappings
$new_map = array_flip($this->field_maps); //Flip the keys, since this is the reverse operation
$numparams = 1;
$params