PicoraActiveRecord

Database independent object relational mapper.

implements ArrayAccess

Documentation for this class is coming soon.

Method Overview

Return Visibility Name Parameters
void final static public addRelationship (string $class_name, string $relationship_type, string $related_class_name, string $foreign_key, array $params)
object static public build (string $class_name, array $data)
object static public create (string $class_name, array $data)
int static public count (string $class_name, array $params)
bool final static public connect (string $connection_string)
string final static public escape (mixed $value)
mixed final static public executeQuery (string $sql)
array static public findAll (string class_name, mixed $params_or_sql)
mixed static public findAllByField (string $class_name, string $field, mixed $value, mixed $params)
mixed static public findByField (string $class_name, string $field, mixed $value)
mixed static public find (string $class_name, mixed $id_or_params)
mixed final static public getCurrentConnection ()
array final static public getRelationshipList (string $class_name)
string final static public getCurrentConnectionType ()
object static public merge (string $class_name, mixed $id_or_params, array $data)
string final static public primaryKeyNameFromClassName (string $class_name)
string final static public tableNameFromClassName (string $class_name)
object final public __construct (mixed $data)
void final public addError (string $field, string $message)
void final public clearErrorList ()
mixed final public delete ()
void final public getErrorList ()
unknown public isValid ()
bool final public reload ()
bool public save ()
mixed final public updateAttribute (string $key, mixed $value)

Method Detail

final static public addRelationship()

Parameter Type Name Description
string $class_name
string $relationship_type
string $related_class_name
string $foreign_key
array $params

static public build()

Parameter Type Name Description
string $class_name
array $data

Builds a new subclass instance that is not yet saved.

static public create()

Parameter Type Name Description
string $class_name
array $data

Builds a new subclass instance and saves it.

static public count()

Parameter Type Name Description
string $class_name
array $params

Params can contain contain anything that can be passed to findAll()

final static public connect()

Parameter Type Name Description
string $connection_string

Example connection strings:

PicoraActiveRecord::connect('sqlite://relative_path_to_sqlite_database');
PicoraActiveRecord::connect('mysql://user:password@host/database_name');

final static public escape()

Parameter Type Name Description
mixed $value

Escapes (or quotes) any scalar value based on the connection type.

final static public executeQuery()

Parameter Type Name Description
string $sql

static public findAll()

Parameter Type Name Description
string class_name
mixed $params_or_sql

Find all instances of a given $class_name. You can pass a complete SQL statement, or an array of $params which may contain any of the following:

  • where - string WHERE SQL fragment or array of (string field_name => mixed value) pairs
  • order - string ORDER BY SQL fragment
  • offset - int LIMIT SQL fragment
  • limit - int LIMIT SQL fragment
  • tables - array of tables to include (defaults to the table belonging to the current $class_name)
  • columns - array of columns to include (defaults to *)
  • join - array of (string joinclass => string foreignkey) pairs
$articles = PicoraActiveRecord::findAll('Article');
$articles = PicoraActiveRecord::findAll('Article',array(
    'where' => array('category'=>'News'),
    'order' => 'id DESC'
));

static public findAllByField()

Parameter Type Name Description
string $class_name
string $field
mixed $value
mixed $params

static public findByField()

Parameter Type Name Description
string $class_name
string $field
mixed $value

static public find()

Parameter Type Name Description
string $class_name
mixed $id_or_params

Similar to findAll() but returns only a single instance no matter how many records are found. Returns false if no record is found.

PicoraActiveRecord::find('Article',1);
PicoraActiveRecord::find('Article',array('where'=>'id = 1'));

final static public getCurrentConnection()

Returns a PDO object, SQLiteDatabase object or a MySQL connection resource

final static public getRelationshipList()

Parameter Type Name Description
string $class_name

final static public getCurrentConnectionType()

Returns 'sqlite', 'mysql' or 'pdo'

static public merge()

Parameter Type Name Description
string $class_name
mixed $id_or_params
array $data

Finds a given instance, merges $data onto the object, then returns it without saving it.

final static public primaryKeyNameFromClassName()

Parameter Type Name Description
string $class_name

final static public tableNameFromClassName()

Parameter Type Name Description
string $class_name

final public __construct()

Parameter Type Name Description
mixed $data

final public addError()

Parameter Type Name Description
string $field
string $message

final public clearErrorList()

final public delete()

Delete a given record from the database.

final public getErrorList()

public isValid()

Does nothing by default, you can override this in a subclass.

final public reload()

Updates all properties in the record from the database.

public save()

Saves a given record, validating it in the process it.

final public updateAttribute()

Parameter Type Name Description
string $key
mixed $value

Updates a given attribute in the record, saving that attribute in the database without performing validation.

This class triggers the following events, which you can observe with the following syntax:

PicoraEvent::observe('event_name','my_function'); //or
PicoraEvent::observe('event_name',array($my_object,'my_instance_method')); //or
PicoraEvent::observe('event_name',array('MyClass','my_static_method'));
Return Name Signature Description
void CLASS_NAME.beforeCreate (PicoraActiveRecord object)
void CLASS_NAME.afterCreate (PicoraActiveRecord object)
void CLASS_NAME.beforeSave (PicoraActiveRecord object)
void CLASS_NAME.afterSave (PicoraActiveRecord object)
void CLASS_NAME.beforeDelete (PicoraActiveRecord object)
void CLASS_NAME.afterDelete (PicoraActiveRecord object)
void CLASS_NAME.afterFind (PicoraActiveRecord object)

Declared in: PicoraActiveRecord.php

class PicoraActiveRecord implements ArrayAccess {
    //class
    static protected $__connection__ = false;
    static protected $__connection_type__ = false;
    static protected $__table_names__ = array();
    static protected $__primary_keys__ = array();
    static protected $__relationships__ = array();
    static protected $__protected_field_names__ = array('__class_name__','__error_list__');
    static protected $__behaviors__ = array();
    /**
     * Example connection strings:
     * 
     * <pre class="highlighted"><code class="php">PicoraActiveRecord::connect('sqlite://relative_path_to_sqlite_database');
     * PicoraActiveRecord::connect('mysql://user:password@host/database_name');</code></pre>
     *
     * @param string $connection_string
     * @return bool
     */
    final static public function connect($connection_string,$username = false,$password = false,$driver_options = false){
        if(preg_match('|sqlite\://(.+)|',$connection_string)){
            self::$__connection_type__ = 'sqlite';
            if(!preg_match('|sqlite\://(.+)|',$connection_string,$match))
                throw new Exception('SQLite connection string should be in the following format: sqlite://path/to/file.db');
            $file = $match[1];
            $error = '';
            if(!is_writable($file) || !is_readable($file))
                throw new Exception($file." must be writable.");
            if(!is_writable(dirname($file)) || !is_readable(dirname($file)))
                throw new Exception(dirname($file)."/ must be writable.");
            self::$__connection__ = new SQLiteDatabase($file,0666,$error);
            if(!self::$__connection__)
                throw new Exception($error);
            self::$__connection__->busyTimeout(30000);
            self::$__connection__->query('PRAGMA short_column_names = 1;');
            return true;
        }elseif(preg_match('|mysql\://(.+)|',$connection_string)){
            self::$__connection_type__ = 'mysql';
            if(!preg_match('|mysql\://([^:]+):?([^@]*)@([^/]+)/(.+)|',$connection_string,$match))
                throw new Exception('MySql connection string should be in the following format: mysql://username:password@host/database_name');
            self::$__connection__ = mysql_connect($match[3],$match[1],$match[2],true);
            if(!self::$__connection__)
                throw new Exception('Could not connect to MySQL. Tried to connect to MySQL at '.$match[3].'. MySQL issued this error: "'.mysql_error().'"');
            if(!mysql_select_db($match[4],self::$__connection__))
                throw new Exception('Could not Select Database. Tried to select the database '.$match[4].'. MySQL issued this error: "'.mysql_error().'"');
            @mysql_query("SET NAMES 'utf8';",self::$__connection__);
            return true;
        }else{
            self::$__connection_type__ = 'pdo';
            self::$__connection__ = new PDO($connection_string,$username,$password,$driver_options);
            return true;
        }
    }
    /**
     * Returns a PDO object, SQLiteDatabase object or a MySQL connection resource
     * @return mixed 
     */
    final static public function getCurrentConnection(){
        return self::$__connection__;
    }
    /**
     * Returns 'sqlite', 'mysql' or 'pdo'
     * @return string
     */
    final static public function getCurrentConnectionType(){
        return self::$__connection_type__;
    }
    final static public function addBehavior($class_name,$behavior_name){
        if(!isset(self::$__behaviors__[$class_name]))
            self::$__behaviors__[$class_name] = array();
        self::$__behaviors__[$class_name][$behavior_name] = array_slice(func_get_args(),2);
    }
    final static public function getBehaviorList($class_name){
        return (!isset(self::$__behaviors__[$class_name])) ? array() : self::$__behaviors__[$class_name];
    }
    /**
     * @param string $class_name
     * @param string $relationship_type
     * @param string $related_class_name
     * @param string $foreign_key
     * @param array $params
     * @return void
     */
    final static public function addRelationship($class_name,$relationship_type,$related_class_name,$foreign_key,$params = false){
        if(!isset(self::$__relationships__[$class_name]))
            self::$__relationships__[$class_name] = array();
        self::$__relationships__[$class_name][] = ($relationship_type == 'has_and_belongs_to_many')
            ? array_slice(func_get_args(),1)
            : array_merge(array_slice(func_get_args(),1,3),($params ? $params : array()))
        ;
    }
    /**
     * @param string $class_name
     * @return array
     */
    final static public function getRelationshipList($class_name){
        return (!isset(self::$__relationships__[$class_name])) ? array() : self::$__relationships__[$class_name];
    }
    /**
     * @param string $class_name
     * @return string
     */
    final static public function tableNameFromClassName($class_name){
        if(!isset(self::$__table_names__[$class_name])){
            if(!class_exists($class_name))
                $table_name = $class_name;
            else{
                $table_name = (defined($class_name.'::TABLE_NAME') ? constant($class_name.'::TABLE_NAME') : null);
                if($table_name === null)
                    $table_name = PicoraSupport::pluralize(strtolower(preg_replace('/([a-z])([A-Z])/e',"'\\1_'.strtolower('\\2')",str_replace('ActiveRecord','',$class_name))));
            }
            self::$__table_names__[$class_name] = $table_name;
        }
        return self::$__table_names__[$class_name];
    }
    /**
     * @param string $class_name
     * @return string
     */
    final static public function primaryKeyNameFromClassName($class_name){
        if(!isset(self::$__primary_keys__[$class_name])){
            if(!class_exists($class_name))
                $primary_key = 'id';
            else{
                $primary_key = (defined($class_name.'::PRIMARY_KEY') ? constant($class_name.'::PRIMARY_KEY') : null);
                if($primary_key === null)
                    $primary_key = 'id';
            }
            self::$__primary_keys__[$class_name] = $primary_key;
        }
        return self::$__primary_keys__[$class_name];
    }
    /**
     * Escapes (or quotes) any scalar value based on the connection type.
     * @param mixed $value
     * @return string
     */
    final static public function escape($value){
        switch(self::$__connection_type__){
            case 'mysql': return mysql_real_escape_string($value,self::$__connection__);
            case 'sqlite': return sqlite_escape_string($value);
            case 'pdo': return self::$__connection__->quote($value);
        }
    }
    final static protected function getLastInsertId(){
        switch(self::$__connection_type__){
            case 'mysql': return mysql_insert_id(self::$__connection__);
            case 'sqlite': return self::$__connection__->lastInsertRowId();
            case 'pdo': return self::$__connection__->lastInsertId();
        }
    }
    /**
     * @param string $sql
     * @return mixed
     */
    final static public function executeQuery($sql){
        if(!self::$__connection__)
            throw new Exception('No active database connection for PicoraActiveRecord was found.');
        switch(self::$__connection_type__){
            case 'mysql': $response = mysql_query($sql,self::$__connection__); break;
            case 'sqlite': $response = self::$__connection__->query($sql); break;
            case 'pdo': $response = self::$__connection__->query($sql); break;
        }
        if(!$response){
            switch(self::$__connection_type__){
                case 'mysql': $error = mysql_error(self::$__connection__); break;
                case 'sqlite': $error = self::$__connection__->lastError(); break;
                case 'pdo': $error = implode(',',self::$__connection__->errorInfo()); break;
            }
            throw new Exception('SQL Query Error:'.$error.'<br/><br/>in query<br/><br/>'.$sql);
        }
        return $response;
    }
    final static protected function buildSQL($class_name,$params,$isCountSQL = false){
        if(isset($params['where']) && is_array($params['where'])){
            $where = '';
            foreach($params['where'] as $key => $value)
                $where .= $key." = '".self::escape($value)."' AND ";
            $params['where'] = substr($where,0,-5);
        }
        if(is_string($params))
            throw new Exception('!');
        $params['tables'] = (isset($params['tables'])
            ? (is_string($params['tables']) ? array($params['tables']) : $params['tables'])
            : array()
        );
        if(count($params['tables']) == 0)
            $params['tables'] = array(self::tableNameFromClassName($class_name));
        $sql = ($isCountSQL)
            ? 'SELECT COUNT(*) AS count FROM '.$params['tables'][0].(isset($params['join']) ? self::buildJoins($class_name,$params['join']) : '').(isset($params['where']) ? ' WHERE '.$params['where'] : '')
            : 'SELECT '.(isset($params['columns']) ? implode(',',$params['columns']) : '*').' FROM '.(isset($params['mock']) ? $params['mock'] : implode(',',$params['tables'])).' '.
                (isset($params['join']) ? self::buildJoins($class_name,$params['join']) : '').
                (isset($params['where']) ? ' WHERE '.$params['where'] : '').
                (isset($params['order']) ? ' ORDER BY '.$params['order'] : '').
                (isset($params['offset'],$params['limit']) ? ' LIMIT '.$params['offset'].','.$params['limit'] : '').
                (!isset($params['offset']) && isset($params['limit']) ? ' LIMIT '.$params['limit'] : '')
        ;
        return $sql;
    }
    final static protected function buildJoins($class_name,$joins){
        if(is_string($joins))
            return $joins;
        $table_name = self::tableNameFromClassName($class_name);
        $joinSQL = '';
        foreach($joins as $join_class => $key){
            $join_table_name = self::tableNameFromClassName($join_class);
            $joinSQL .= ' LEFT JOIN '.$join_table_name.' ON '.$table_name.'.'.$key.' = '.$join_table_name.'.id';
        }
        return $joinSQL;
    }
    /**
     * Builds a new subclass instance that is not yet saved.
     * @param string $class_name
     * @param array $data
     * @return object
     */
    static public function build($class_name,$data = false){
        return new $class_name($data);
    }
    /**
     * Builds a new subclass instance and saves it.
     * @param string $class_name
     * @param array $data
     * @return object
     */
    static public function create($class_name,$data = false){
        $record = self::build($class_name,$data);
        $record->save();
        return $record;
    }
    /**
     * Params can contain contain anything that can be passed to findAll()
     * @param string $class_name
     * @param array $params
     * @return int
     */
    static public function count($class_name,$params = false){
        if(!$params)
             $params = array();
        if(is_string($params))
            $params = array('where'=>$params);
        $result = self::executeQuery(self::buildSQL($class_name,$params,true));
        switch(self::$__connection_type__){
            case 'mysql': $row = mysql_fetch_array($result,MYSQL_ASSOC); break;
            case 'sqlite': $row = $result->fetch(SQLITE_ASSOC); break;
            case 'pdo': $row = $result->fetch(PDO::FETCH_ASSOC); break;
        }
        return (int)$row['count'];
    }
    /**
     * Similar to findAll() but returns only a single instance no matter how many records are found. Returns false if no record is found.
     * <pre class="highlighted"><code class="php">PicoraActiveRecord::find('Article',1);
     * PicoraActiveRecord::find('Article',array('where'=>'id = 1'));</code></pre>
     * @param string $class_name
     * @param mixed $id_or_params
     * @return mixed
     */
    static public function find($class_name,$id_or_params){
        $params = (!is_array($id_or_params)) ? array('where'=>"id = '".self::escape($id_or_params)."'") : $id_or_params;
        $params['limit'] = 1;
        $results = self::findAll($class_name,$params);
        return (isset($results[0])) ? $results[0] : false;
    }
    /**
     * Find all instances of a given $class_name. You can pass a complete SQL statement, or an array of $params which may contain any of the following:
     * 
     * - where - string WHERE SQL fragment or array of (string field_name => mixed value) pairs
     * - order - string ORDER BY SQL fragment
     * - offset - int LIMIT SQL fragment
     * - limit - int LIMIT SQL fragment
     * - tables - array of tables to include (defaults to the table belonging to the current $class_name)
     * - columns - array of columns to include (defaults to *)
     * - join - array of (string join_class => string foreign_key) pairs
     * 
     * <pre class="highlighted"><code class="php">$articles = PicoraActiveRecord::findAll('Article');
     * $articles = PicoraActiveRecord::findAll('Article',array(
     *     'where' => array('category'=>'News'),
     *     'order' => 'id DESC'
     * ));</code></pre>
     * @param string class_name
     * @param mixed $params_or_sql
     * @return array
     */
    static public function findAll($class_name,$params_or_sql = array()){
        $results = array();
        $response = self::executeQuery((!is_array($params_or_sql) ? $params_or_sql : self::buildSQL($class_name,$params_or_sql)));
        $target_class_name = (class_exists($class_name) ? $class_name : 'PicoraActiveRecord');
        switch(self::$__connection_type__){
            case 'mysql':
                while($record = mysql_fetch_object($response,$class_name)){
                    $record->notifyObservers('afterFind');
                    $results[] = $record;
                }
                break;
            case 'sqlite':
                while($record = $response->fetchObject($class_name)){
                    $record->notifyObservers('afterFind');
                    $results[] = $record;
                }
                break;
            case 'pdo':
                $results = $response->fetchAll(PDO::FETCH_CLASS);
                break;
        }
        return $results;
    }
    /**
     * @param string $class_name
     * @param string $field
     * @param mixed $value
     * @return mixed
     */
    static public function findByField($class_name,$field,$value = false){
        $results = self::findAllByField($class_name,$field,$value);
        return (isset($results[0])) ? $results[0] : false;
    }
    /**
     * @param string $class_name
     * @param string $field
     * @param mixed $value
     * @param mixed $params
     * @return mixed
     */
    static public function findAllByField($class_name,$field,$value = false,$params = false){
        $params = (is_array($value) ? $value : (is_array($params) ? $params : array()));
        $params['where'] = ($value ? array($field => $value) : $field);
        return self::findAll($class_name,$params);
    }
    /**
     * Finds a given instance, merges $data onto the object, then returns it without saving it.
     * @param string $class_name
     * @param mixed $id_or_params
     * @param array $data
     * @return object
     */
    static public function merge($class_name,$id_or_params,$data){
        $record = self::find($class_name,$id_or_params);
        if(!$record)
            return false;
        foreach($data as $key => $value)
            $record->{$key} = $value;
        return $record;
    }
    //instance
    protected $__class_name__ = false;
    protected $__error_list__ = array();
    /**
     * @param mixed $data
     * @return object
     */
       final public function __construct($data = false){
        if(!$this->__class_name__)
            $this->__class_name__ = get_class($this);
        $primary_key = self::primaryKeyNameFromClassName($this->__class_name__);
        if(!isset($this->{$primary_key}))
        $this->{$primary_key} = false;
        if($data)
            foreach($data as $key => $value)
                $this->{$key} = $value;
    }
    public function getKeyForFlash($field = false){
        return $this->__class_name__.'.'.($this->{self::primaryKeyNameFromClassName($this->__class_name__)} === false ? 'new' : $this->{self::primaryKeyNameFromClassName($this->__class_name__)}).($field ? '.'.$field : '');
    }
    /**
     * @param string $field
     * @param string $message
     * @return void
     */
    final public function addError($field,$message = ''){
        if(!isset($this->__error_list__[$field]))
            $this->__error_list__[$field] = array();
        if(!in_array($message,$this->__error_list__[$field])){
            $this->__error_list__[$field][] = $message;
            $flash =& PicoraController::getFlash();
            $key = $this->getKeyForFlash($field);
            if(!isset($flash[$key]))
                $flash[$key] = array();
            $flash[$key][] = $message;
        }
    }
    /**
     * @return void
     */
    final public function clearErrorList(){
        $this->__error_list__ = array();
        $flash =& PicoraController::getFlash();
        $key = $this->getKeyForFlash($field);
        if(isset($flash[$key]))
            unset($flash[$key]);
    }
    /**
     * @return void
     */
    final public function getErrorList(){
        return $this->__error_list__;
    }
    /**
     * Does nothing by default, you can override this in a subclass.
     */
    public function isValid(){}
    /**
     * Saves a given record, validating it in the process it.
     * @return bool
     */
    public function save(){
        if($this->isValid() === false || count($this->__error_list__) > 0)
            return false;
        $response = null;
        if($this->notifyObservers('beforeSave') === false)
            return false;
        if(!$this->id){
            if($this->notifyObservers('beforeCreate') === false)
                return false;
            $keys = '';
            $values = '';
            foreach($this->toArray() as $key => $value){
                if($key == self::primaryKeyNameFromClassName($this->__class_name__))
                    continue;
                $keys .= $key.',';
                $values .= "'".self::escape($value)."',";
            }
            if(self::executeQuery('INSERT INTO '.self::tableNameFromClassName($this->__class_name__).' ('.substr($keys,0,-1).') VALUES ('.substr($values,0,-1).')')){
                $this->id = self::getLastInsertId();
                foreach(self::getRelationshipList($this->__class_name__) as $r)
                    if($r[0] == 'belongs_to' && isset($r['counter']) && $c = $this->{'get'.$r[1]}())
                        $c->updateAttribute($r['counter'],intval($c->{$r['counter']}) + 1);
                $response = true;
            }else
                $response = false;
            $this->notifyObservers('afterCreate');
        }else{
            $sql = 'UPDATE '.self::tableNameFromClassName($this->__class_name__).' SET ';
            foreach($this->toArray() as $key => $value)
                $sql .= $key."='".self::escape($value)."',";
            $response = (self::executeQuery(substr($sql,0,-1)." WHERE ".self::primaryKeyNameFromClassName($this->__class_name__)." = '".self::escape($this->{self::primaryKeyNameFromClassName($this->__class_name__)})."'"));
        }
        $this->notifyObservers('afterSave');
        return $response;
    }
    /**
     * Delete a given record from the database.
     * @return mixed
     */
    final public function delete(){
        if(!$this->id)
            throw new Exception('Could not delete '.$this->__class_name__.' instance. Object had no id field.');
        if($this->notifyObservers('beforeDelete') === false)
            return false;
        $response = self::executeQuery('DELETE FROM '.self::tableNameFromClassName($this->__class_name__)." WHERE ".self::primaryKeyNameFromClassName($this->__class_name__)." = '".self::escape($this->{self::primaryKeyNameFromClassName($this->__class_name__)})."'");
        $this->notifyObservers('afterDelete');
        foreach(self::getRelationshipList($this->__class_name__) as $r){
            if($r[0] == 'has_one' && isset($r['dependent']) && $r['dependent'] && $child = $this->{'get'.$r[1]}())
                $child->delete();
            elseif($r[0] == 'has_many' && isset($r['dependent']) && $r['dependent']){
                foreach($this->{'get'.$r[1].'List'}() as $child)
                    $child->delete();
            }
        }
        foreach(self::getRelationshipList($this->__class_name__) as $r)
            if($r[0] == 'belongs_to' && isset($r['counter']) && $c = $this->{'get'.$r[1]}())
                $c->updateAttribute($r['counter'],max(0,intval($c->{$r['counter']}) - 1));
        return $response;
    }
    /**
     * Updates all properties in the record from the database.
     * @return bool
     */
    final public function reload(){
        if(!$this->{self::primaryKeyNameFromClassName($this->__class_name__)} || !($row = self::find($this->__class_name__,$this->{self::primaryKeyNameFromClassName($this->__class_name__)})))
            return false;
        foreach($row as $key => $value)
            $this->{$key} = $value;
        return true;
    }
    /**
     * Updates a given attribute in the record, saving that attribute in the database without performing validation.
     * @param string $key
     * @param mixed $value
     * @return mixed
     */
    final public function updateAttribute($key,$value){
        $this->{$key} = $value;
        if(!$this->id)
            return false;
        return self::executeQuery('UPDATE '.self::tableNameFromClassName($this->__class_name__).' SET '.$key." = '".self::escape($value)."' WHERE ".self::primaryKeyNameFromClassName($this->__class_name__)." = '".self::escape($this->{self::primaryKeyNameFromClassName($this->__class_name__)})."'");
    }
    //callbacks    
    final protected function notifyObservers($method_name){
        foreach(PicoraEvent::getObserverList($this->__class_name__.'.'.$method_name) as $callback)
            call_user_func($callback,$this);
        return $this->{$method_name}();
    }
    public function beforeCreate(){}
    public function afterCreate(){}
    public function beforeSave(){}
    public function afterSave(){}
    public function beforeDelete(){}
    public function afterDelete(){}
    public function afterFind(){}
    //ArrayAccess
    final public function toArray(){
        $a = get_object_vars($this);
        foreach(self::$__protected_field_names__ as $field)
            unset($a[$field]);
        return $a;
    }
    final public function offsetExists($key){
        return (!in_array($key,self::$__protected_field_names__)) ? isset($this->{$key}) : false;
    }
    final public function offsetGet($key){
        if(!in_array($key,self::$__protected_field_names__))
            return $this->{$key};
        else
            throw new Exception($key.' is a protected property of PicoraActiveRecord');
    }
    final public function offsetSet($key,$value){
        if(!in_array($key,self::$__protected_field_names__))
            $this->{$key} = $value;
        else
            throw new Exception($key.' is a protected property of PicoraActiveRecord');
    }
    final public function offsetUnset($key){
        if(!in_array($key,self::$__protected_field_names__))
            unset($this->{$key});
        else
            throw new Exception($key.' is a protected property of PicoraActiveRecord');
    }
    //relationships    
    public function __call($method,$arguments){
        foreach(self::getRelationshipList($this->__class_name__) as $relationship){
            $relationship_type = $relationship[0];
            $related_class_name = $relationship[1];
            $foreign_key = (isset($relationship[2])) ? $relationship[2] : PicoraSupport::singularize($related_class_name).'_id';
            switch($relationship_type){
                case 'has_one':
                    switch($method){
                        case 'get'.$related_class_name:
                            return self::find($related_class_name,$this->{$foreign_key});
                        case 'build'.$related_class_name:
                            return self::build($related_class_name,(isset($arguments[0]) ? $arguments[0] : array()));
                        case 'create'.$related_class_name:
                            $record = self::create($related_class_name,(isset($arguments[0]) ? $arguments[0] : array()));
                            if($this->id)
                                $this->updateAttribute($foreign_key,$record->id);
                            return $record;
                    }
                    break;
                case 'has_many':
                    switch($method){
                        case 'delete'.$related_class_name:
                            $record = self::find($related_class_name,($arguments[0] instanceof PicoraActiveRecord ? $arguments[0]->{self::primaryKeyNameFromClassName(get_class($arguments[0]))} : $arguments[0]));
                            return (!$record) ? false : (bool)$record->delete();
                        case 'get'.$related_class_name.'List':
                        case 'get'.$related_class_name.'Count':
                            $params = (isset($arguments[0]) ? $arguments[0] : array());
                            $params['where'] = (isset($params['where']) ? PicoraSupport::formatPropertyString($params['where'],$this).' AND ' : '').$foreign_key.' = '.self::escape($this->{self::primaryKeyNameFromClassName($this->__class_name__)});
                            if('get'.$related_class_name.'Count' == $method)
                                return self::count($related_class_name,array('where' => $params['where']));
                            if(isset($relationship['order']) && !isset($params['order']))
                                $params['order'] = $relationship['order'];
                            $list = self::findAll($related_class_name,$params);
                            if('get'.$related_class_name.'List' == $method)
                                return $list;
                            foreach($list as $item)
                                if(!isset($arguments[0]) || (isset($arguments[0]) && $arguments[0] == $item->{self::primaryKeyNameFromClassName(get_class($item))}))
                                    $item->delete();
                            break;
                        case 'create'.$related_class_name:
                            return self::create($related_class_name,array_merge((isset($arguments[0]) ? $arguments[0] : array()),array($foreign_key => $this->{self::primaryKeyNameFromClassName($this->__class_name__)})));
                        case 'build'.$related_class_name:
                            return self::build($related_class_name,array_merge((isset($arguments[0]) ? $arguments[0] : array()),array($foreign_key => $this->{self::primaryKeyNameFromClassName($this->__class_name__)})));
                    }
                    break;
                case 'belongs_to':
                    switch($method){
                        case 'get'.$related_class_name:
                            return self::find($related_class_name,$this->{$foreign_key});
                        case 'build'.$related_class_name == $method:
                        case 'create'.$related_class_name == $method:
                            $record = self::build($related_class_name,(isset($arguments[0]) ? $arguments[0] : array()));
                            if(isset($relationship['counter']))
                                $record->{$relationship['counter']} = 1;
                            if('build'.$related_class_name == $method)
                                return $record;
                            if($record->save() && $this->{self::primaryKeyNameFromClassName($this->__class_name__)})
                                $this->updateAttribute($foreign_key,$record->{self::primaryKeyNameFromClassName(get_class($record))});
                            return $record;
                    }
                    break;
                case 'has_and_belongs_to_many':
                    $source_class_name = $this->__class_name__;
                    $source_table_name = self::tableNameFromClassName($this->__class_name__);
                    $target_class_name = $related_class_name;
                    $target_table_name = self::tableNameFromClassName($related_class_name);
                    $link_class_name = $relationship[2];
                    $link_table_name = self::tableNameFromClassName($relationship[2]);
                    $source_key = $relationship[3];
                    $target_key = $relationship[4];
                    $params = (isset($arguments[0]) ? $arguments[0] : array());
                    switch($method){
                        case 'set'.$related_class_name.'List':
                            $links = array();
                            self::executeQuery('DELETE FROM '.$link_table_name.' WHERE '.$source_key.' = '.self::escape($this->id));
                            foreach((isset($arguments[0]) ? $arguments[0] : array()) as $id)
                                $links[] = self::create($link_class_name,array(
                                    $source_key => $this->{self::primaryKeyNameFromClassName($this->__class_name__)},
                                    $target_key => ($id instanceof PicoraActiveRecord ? $id->{self::primaryKeyNameFromClassName(get_class($id))} : $id)
                                ));
                            return $links;
                        case 'add'.$related_class_name:
                            return self::create($link_class_name,array(
                                $source_key => $this->{self::primaryKeyNameFromClassName($this->__class_name__)},
                                $target_key => ($arguments[0] instanceof PicoraActiveRecord ? $arguments[0]->{self::primaryKeyNameFromClassName(get_class($arguments[0]))} : $arguments[0])
                            ));
                        case 'remove'.$related_class_name:
                            self::executeQuery('DELETE FROM '.$link_table_name.' WHERE '.$source_key.' = '.self::escape($this->{self::primaryKeyNameFromClassName($this->__class_name__)}).' AND '.$target_key.' = '.self::escape(($arguments[0] instanceof PicoraActiveRecord ? $arguments[0]->{self::primaryKeyNameFromClassName(get_class($argument[0]))} : $arguments[0])));
                            break;
                        case 'has'.$related_class_name:
                            return (bool)(self::find($link_class_name,array('where'=>array(
                                $source_key => $this->id,
                                $target_key => ($arguments[0] instanceof PicoraActiveRecord ? $arguments[0]->{self::primaryKeyNameFromClassName(get_class($arguments[0]))} : $arguments[0])
                            ))));
                        case 'get'.$related_class_name.'List':
                        case 'get'.$related_class_name.'Count':
                            $params['tables'] = $link_table_name;
                            $params['join'] = 'LEFT JOIN '.$target_table_name.' ON ('.$link_table_name.'.'.$target_key.' = '.$target_table_name.'.'.self::primaryKeyNameFromClassName($target_class_name).')';
                            $condition = $link_table_name.'.'.$source_key.' = '.$this->{self::primaryKeyNameFromClassName($this->__class_name__)};
                            $params['where'] = (isset($params['where']) ? $params['where'].' AND ' : '').(isset($relationship['where']) ? $relationship['where'].' AND '.$condition : $condition);
                            if(isset($relationship['order']) && !isset($params['order']))
                                $params['order'] = $relationship['order'];
                            if('get'.$related_class_name.'Count' == $method)
                                return self::count($target_class_name,array('where' => $params['where'],'join' => $params['join']));
                            $instance_list = self::findAll($target_class_name,$params);
                            foreach($instance_list as $instance)
                                unset($instance->{$target_key},$instance->{$source_key});
                            if('get'.$related_class_name.'List' == $method)
                                return $instance_list;
                            break;
                    }
                    break;
            }
        }
        foreach(PicoraEvent::getObserverList('PicoraActiveRecord.call') as $callback){
            $response = call_user_func($callback,$this,$method,$arguments);
            if(!is_null($response))
                return $response;
        }
        throw new Exception('Method "'.$method.'" of "'.$this->__class_name__.'" does not exist.');
    }
}