Файл: onlinepoisk.wm-scripts.ru/vendor/AR/lib/Model.php
Строк: 1863
<?php
/**
* @package ActiveRecord
*/
namespace ActiveRecord;
/**
* The base class for your models.
*
* Defining an ActiveRecord model for a table called people and orders:
*
* <code>
* CREATE TABLE people(
* id int primary key auto_increment,
* parent_id int,
* first_name varchar(50),
* last_name varchar(50)
* );
*
* CREATE TABLE orders(
* id int primary key auto_increment,
* person_id int not null,
* cost decimal(10,2),
* total decimal(10,2)
* );
* </code>
*
* <code>
* class Person extends ActiveRecordModel {
* static $belongs_to = array(
* array('parent', 'foreign_key' => 'parent_id', 'class_name' => 'Person')
* );
*
* static $has_many = array(
* array('children', 'foreign_key' => 'parent_id', 'class_name' => 'Person'),
* array('orders')
* );
*
* static $validates_length_of = array(
* array('first_name', 'within' => array(1,50)),
* array('last_name', 'within' => array(1,50))
* );
* }
*
* class Order extends ActiveRecordModel {
* static $belongs_to = array(
* array('person')
* );
*
* static $validates_numericality_of = array(
* array('cost', 'greater_than' => 0),
* array('total', 'greater_than' => 0)
* );
*
* static $before_save = array('calculate_total_with_tax');
*
* public function calculate_total_with_tax() {
* $this->total = $this->cost * 0.045;
* }
* }
* </code>
*
* For a more in-depth look at defining models, relationships, callbacks and many other things
* please consult our {@link http://www.phpactiverecord.org/guides Guides}.
*
* @package ActiveRecord
* @see BelongsTo
* @see CallBack
* @see HasMany
* @see HasAndBelongsToMany
* @see Serialization
* @see Validations
*/
class Model
{
/**
* An instance of {@link Errors} and will be instantiated once a write method is called.
*
* @var Errors
*/
public $errors;
/**
* Contains model values as column_name => value
*
* @var array
*/
private $attributes = array();
/**
* Flag whether or not this model's attributes have been modified since it will either be null or an array of column_names that have been modified
*
* @var array
*/
private $__dirty = null;
/**
* Flag that determines of this model can have a writer method invoked such as: save/update/insert/delete
*
* @var boolean
*/
private $__readonly = false;
/**
* Array of relationship objects as model_attribute_name => relationship
*
* @var array
*/
private $__relationships = array();
/**
* Flag that determines if a call to save() should issue an insert or an update sql statement
*
* @var boolean
*/
private $__new_record = true;
/**
* Set to the name of the connection this {@link Model} should use.
*
* @var string
*/
static $connection;
/**
* Set to the name of the database this Model's table is in.
*
* @var string
*/
static $db;
/**
* Set this to explicitly specify the model's table name if different from inferred name.
*
* If your table doesn't follow our table name convention you can set this to the
* name of your table to explicitly tell ActiveRecord what your table is called.
*
* @var string
*/
static $table_name;
/**
* Set this to override the default primary key name if different from default name of "id".
*
* @var string
*/
static $primary_key;
/**
* Set this to explicitly specify the sequence name for the table.
*
* @var string
*/
static $sequence;
/**
* Allows you to create aliases for attributes.
*
* <code>
* class Person extends ActiveRecordModel {
* static $alias_attribute = array(
* 'the_first_name' => 'first_name',
* 'the_last_name' => 'last_name');
* }
*
* $person = Person::first();
* $person->the_first_name = 'Tito';
* echo $person->the_first_name;
* </code>
*
* @var array
*/
static $alias_attribute = array();
/**
* Whitelist of attributes that are checked from mass-assignment calls such as constructing a model or using update_attributes.
*
* This is the opposite of {@link attr_protected $attr_protected}.
*
* <code>
* class Person extends ActiveRecordModel {
* static $attr_accessible = array('first_name','last_name');
* }
*
* $person = new Person(array(
* 'first_name' => 'Tito',
* 'last_name' => 'the Grief',
* 'id' => 11111));
*
* echo $person->id; # => null
* </code>
*
* @var array
*/
static $attr_accessible = array();
/**
* Blacklist of attributes that cannot be mass-assigned.
*
* This is the opposite of {@link attr_accessible $attr_accessible} and the format
* for defining these are exactly the same.
*
* @var array
*/
static $attr_protected = array();
/**
* Delegates calls to a relationship.
*
* <code>
* class Person extends ActiveRecordModel {
* static $belongs_to = array(array('venue'),array('host'));
* static $delegate = array(
* array('name', 'state', 'to' => 'venue'),
* array('name', 'to' => 'host', 'prefix' => 'woot'));
* }
* </code>
*
* Can then do:
*
* <code>
* $person->state # same as calling $person->venue->state
* $person->name # same as calling $person->venue->name
* $person->woot_name # same as calling $person->host->name
* </code>
*
* @var array
*/
static $delegate = array();
/**
* Define customer setters methods for the model.
*
* You can also use this to define custom setters for attributes as well.
*
* <code>
* class User extends ActiveRecordModel {
* static $setters = array('password','more','even_more');
*
* # now to define the setter methods. Note you must
* # prepend set_ to your method name:
* function set_password($plaintext) {
* $this->encrypted_password = md5($plaintext);
* }
* }
*
* $user = new User();
* $user->password = 'plaintext'; # will call $user->set_password('plaintext')
* </code>
*
* If you define a custom setter with the same name as an attribute then you
* will need to use assign_attribute() to assign the value to the attribute.
* This is necessary due to the way __set() works.
*
* For example, assume 'name' is a field on the table and we're defining a
* custom setter for 'name':
*
* <code>
* class User extends ActiveRecordModel {
* static $setters = array('name');
*
* # INCORRECT way to do it
* # function set_name($name) {
* # $this->name = strtoupper($name);
* # }
*
* function set_name($name) {
* $this->assign_attribute('name',strtoupper($name));
* }
* }
*
* $user = new User();
* $user->name = 'bob';
* echo $user->name; # => BOB
* </code>
*
* @var array
*/
static $setters = array();
/**
* Define customer getter methods for the model.
*
* <code>
* class User extends ActiveRecordModel {
* static $getters = array('middle_initial','more','even_more');
*
* # now to define the getter method. Note you must
* # prepend get_ to your method name:
* function get_middle_initial() {
* return $this->middle_name{0};
* }
* }
*
* $user = new User();
* echo $user->middle_name; # will call $user->get_middle_name()
* </code>
*
* If you define a custom getter with the same name as an attribute then you
* will need to use read_attribute() to get the attribute's value.
* This is necessary due to the way __get() works.
*
* For example, assume 'name' is a field on the table and we're defining a
* custom getter for 'name':
*
* <code>
* class User extends ActiveRecordModel {
* static $getters = array('name');
*
* # INCORRECT way to do it
* # function get_name() {
* # return strtoupper($this->name);
* # }
*
* function get_name() {
* return strtoupper($this->read_attribute('name'));
* }
* }
*
* $user = new User();
* $user->name = 'bob';
* echo $user->name; # => BOB
* </code>
*
* @var array
*/
static $getters = array();
/**
* Constructs a model.
*
* When a user instantiates a new object (e.g.: it was not ActiveRecord that instantiated via a find)
* then @var $attributes will be mapped according to the schema's defaults. Otherwise, the given
* $attributes will be mapped via set_attributes_via_mass_assignment.
*
* <code>
* new Person(array('first_name' => 'Tito', 'last_name' => 'the Grief'));
* </code>
*
* @param array $attributes Hash containing names and values to mass assign to the model
* @param boolean $guard_attributes Set to true to guard attributes
* @param boolean $instantiating_via_find Set to true if this model is being created from a find call
* @param boolean $new_record Set to true if this should be considered a new record
* @return Model
*/
public function __construct(array $attributes=array(), $guard_attributes=true, $instantiating_via_find=false, $new_record=true)
{
$this->__new_record = $new_record;
// initialize attributes applying defaults
if (!$instantiating_via_find)
{
foreach (static::table()->columns as $name => $meta)
$this->attributes[$meta->inflected_name] = $meta->default;
}
$this->set_attributes_via_mass_assignment($attributes, $guard_attributes);
// since all attribute assignment now goes thru assign_attributes() we want to reset
// dirty if instantiating via find since nothing is really dirty when doing that
if ($instantiating_via_find)
$this->__dirty = array();
$this->invoke_callback('after_construct',false);
}
/**
* Magic method which delegates to read_attribute(). This handles firing off getter methods,
* as they are not checked/invoked inside of read_attribute(). This circumvents the problem with
* a getter being accessed with the same name as an actual attribute.
*
* @see read_attribute()
* @param string $name Name of an attribute
* @return mixed The value of the attribute
*/
public function &__get($name)
{
// check for getter
if (in_array("get_$name",static::$getters))
{
$name = "get_$name";
$value = $this->$name();
return $value;
}
return $this->read_attribute($name);
}
/**
* Determines if an attribute exists for this {@link Model}.
*
* @param string $attribute_name
* @return boolean
*/
public function __isset($attribute_name)
{
return array_key_exists($attribute_name,$this->attributes) || array_key_exists($attribute_name,static::$alias_attribute);
}
/**
* Magic allows un-defined attributes to set via $attributes
*
* @throws {@link UndefinedPropertyException} if $name does not exist
* @param string $name Name of attribute, relationship or other to set
* @param mixed $value The value
* @return mixed The value
*/
public function __set($name, $value)
{
if (array_key_exists($name, static::$alias_attribute))
$name = static::$alias_attribute[$name];
elseif (in_array("set_$name",static::$setters))
{
$name = "set_$name";
return $this->$name($value);
}
if (array_key_exists($name,$this->attributes))
return $this->assign_attribute($name,$value);
foreach (static::$delegate as &$item)
{
if (($delegated_name = $this->is_delegated($name,$item)))
return $this->$item['to']->$delegated_name = $value;
}
throw new UndefinedPropertyException(get_called_class(),$name);
}
public function __wakeup()
{
// make sure the models Table instance gets initialized when waking up
static::table();
}
/**
* Assign a value to an attribute.
*
* @param string $name Name of the attribute
* @param mixed &$value Value of the attribute
* @return mixed the attribute value
*/
public function assign_attribute($name, $value)
{
$table = static::table();
if (array_key_exists($name,$table->columns) && !is_object($value))
$value = $table->columns[$name]->cast($value,static::connection());
// convert php's DateTime to ours
if ($value instanceof DateTime)
$value = new DateTime($value->format('Y-m-d H:i:s T'));
// make sure DateTime values know what model they belong to so
// dirty stuff works when calling set methods on the DateTime object
if ($value instanceof DateTime)
$value->attribute_of($this,$name);
$this->attributes[$name] = $value;
$this->flag_dirty($name);
return $value;
}
/**
* Retrieves an attribute's value or a relationship object based on the name passed. If the attribute
* accessed is 'id' then it will return the model's primary key no matter what the actual attribute name is
* for the primary key.
*
* @param string $name Name of an attribute
* @return mixed The value of the attribute
* @throws {@link UndefinedPropertyException} if name could not be resolved to an attribute, relationship, ...
*/
public function &read_attribute($name)
{
// check for aliased attribute
if (array_key_exists($name, static::$alias_attribute))
$name = static::$alias_attribute[$name];
// check for attribute
if (array_key_exists($name,$this->attributes))
return $this->attributes[$name];
// check relationships if no attribute
if (array_key_exists($name,$this->__relationships))
return $this->__relationships[$name];
$table = static::table();
// this may be first access to the relationship so check Table
if (($relationship = $table->get_relationship($name)))
{
$this->__relationships[$name] = $relationship->load($this);
return $this->__relationships[$name];
}
if ($name == 'id')
{
if (count($this->get_primary_key()) > 1)
throw new Exception("TODO composite key support");
if (isset($this->attributes[$table->pk[0]]))
return $this->attributes[$table->pk[0]];
}
//do not remove - have to return null by reference in strict mode
$null = null;
foreach (static::$delegate as &$item)
{
if (($delegated_name = $this->is_delegated($name,$item)))
{
$to = $item['to'];
if ($this->$to)
{
$val =& $this->$to->$delegated_name;
return $val;
}
else
return $null;
}
}
throw new UndefinedPropertyException(get_called_class(),$name);
}
/**
* Flags an attribute as dirty.
*
* @param string $name Attribute name
*/
public function flag_dirty($name)
{
if (!$this->__dirty)
$this->__dirty = array();
$this->__dirty[$name] = true;
}
/**
* Returns hash of attributes that have been modified since loading the model.
*
* @return mixed null if no dirty attributes otherwise returns array of dirty attributes.
*/
public function dirty_attributes()
{
if (!$this->__dirty)
return null;
$dirty = array_intersect_key($this->attributes,$this->__dirty);
return !empty($dirty) ? $dirty : null;
}
/**
* Returns a copy of the model's attributes hash.
*
* @return array A copy of the model's attribute data
*/
public function attributes()
{
return $this->attributes;
}
/**
* Retrieve the primary key name.
*
* @return string The primary key for the model
*/
public function get_primary_key()
{
return Table::load(get_class($this))->pk;
}
/**
* Returns the actual attribute name if $name is aliased.
*
* @param string $name An attribute name
* @return string
*/
public function get_real_attribute_name($name)
{
if (array_key_exists($name,$this->attributes))
return $name;
if (array_key_exists($name,static::$alias_attribute))
return static::$alias_attribute[$name];
return null;
}
/**
* Returns array of validator data for this Model.
*
* Will return an array looking like:
*
* <code>
* array(
* 'name' => array(
* array('validator' => 'validates_presence_of'),
* array('validator' => 'validates_inclusion_of', 'in' => array('Bob','Joe','John')),
* 'password' => array(
* array('validator' => 'validates_length_of', 'minimum' => 6))
* )
* );
* </code>
*
* @return array An array containing validator data for this model.
*/
public function get_validation_rules()
{
require_once 'Validations.php';
$validator = new Validations($this);
return $validator->rules();
}
/**
* Returns an associative array containing values for all the attributes in $attributes
*
* @param array $attributes Array containing attribute names
* @return array A hash containing $name => $value
*/
public function get_values_for($attributes)
{
$ret = array();
foreach ($attributes as $name)
{
if (array_key_exists($name,$this->attributes))
$ret[$name] = $this->attributes[$name];
}
return $ret;
}
/**
* Retrieves the name of the table for this Model.
*
* @return string
*/
public static function table_name()
{
return static::table()->table;
}
/**
* Returns the attribute name on the delegated relationship if $name is
* delegated or null if not delegated.
*
* @param string $name Name of an attribute
* @param array $delegate An array containing delegate data
* @return delegated attribute name or null
*/
private function is_delegated($name, &$delegate)
{
if ($delegate['prefix'] != '')
$name = substr($name,strlen($delegate['prefix'])+1);
if (is_array($delegate) && in_array($name,$delegate['delegate']))
return $name;
return null;
}
/**
* Determine if the model is in read-only mode.
*
* @return boolean
*/
public function is_readonly()
{
return $this->__readonly;
}
/**
* Determine if the model is a new record.
*
* @return boolean
*/
public function is_new_record()
{
return $this->__new_record;
}
/**
* Throws an exception if this model is set to readonly.
*
* @throws ActiveRecordReadOnlyException
* @param string $method_name Name of method that was invoked on model for exception message
*/
private function verify_not_readonly($method_name)
{
if ($this->is_readonly())
throw new ReadOnlyException(get_class($this), $method_name);
}
/**
* Flag model as readonly.
*
* @param boolean $readonly Set to true to put the model into readonly mode
*/
public function readonly($readonly=true)
{
$this->__readonly = $readonly;
}
/**
* Retrieve the connection for this model.
*
* @return Connection
*/
public static function connection()
{
return static::table()->conn;
}
/**
* Returns the {@link Table} object for this model.
*
* Be sure to call in static scoping: static::table()
*
* @return Table
*/
public static function table()
{
return Table::load(get_called_class());
}
/**
* Creates a model and saves it to the database.
*
* @param array $attributes Array of the models attributes
* @param boolean $validate True if the validators should be run
* @return Model
*/
public static function create($attributes, $validate=true)
{
$class_name = get_called_class();
$model = new $class_name($attributes);
$model->save($validate);
return $model;
}
/**
* Save the model to the database.
*
* This function will automatically determine if an INSERT or UPDATE needs to occur.
* If a validation or a callback for this model returns false, then the model will
* not be saved and this will return false.
*
* If saving an existing model only data that has changed will be saved.
*
* @param boolean $validate Set to true or false depending on if you want the validators to run or not
* @return boolean True if the model was saved to the database otherwise false
*/
public function save($validate=true)
{
$this->verify_not_readonly('save');
return $this->is_new_record() ? $this->insert($validate) : $this->update($validate);
}
/**
* Issue an INSERT sql statement for this model's attribute.
*
* @see save
* @param boolean $validate Set to true or false depending on if you want the validators to run or not
* @return boolean True if the model was saved to the database otherwise false
*/
private function insert($validate=true)
{
$this->verify_not_readonly('insert');
if (($validate && !$this->_validate() || !$this->invoke_callback('before_create',false)))
return false;
$table = static::table();
if (!($attributes = $this->dirty_attributes()))
$attributes = $this->attributes;
$pk = $this->get_primary_key();
$use_sequence = false;
if ($table->sequence && !isset($attributes[$pk[0]]))
{
if (($conn = static::connection()) instanceof OciAdapter)
{
// terrible oracle makes us select the nextval first
$attributes[$pk[0]] = $conn->get_next_sequence_value($table->sequence);
$table->insert($attributes);
$this->attributes[$pk[0]] = $attributes[$pk[0]];
}
else
{
// unset pk that was set to null
if (array_key_exists($pk[0],$attributes))
unset($attributes[$pk[0]]);
$table->insert($attributes,$pk[0],$table->sequence);
$use_sequence = true;
}
}
else
$table->insert($attributes);
// if we've got an autoincrementing/sequenced pk set it
if (count($pk) == 1)
{
$column = $table->get_column_by_inflected_name($pk[0]);
if ($column->auto_increment || $use_sequence)
$this->attributes[$pk[0]] = $table->conn->insert_id($table->sequence);
}
$this->invoke_callback('after_create',false);
$this->__new_record = false;
return true;
}
/**
* Issue an UPDATE sql statement for this model's dirty attributes.
*
* @see save
* @param boolean $validate Set to true or false depending on if you want the validators to run or not
* @return boolean True if the model was saved to the database otherwise false
*/
private function update($validate=true)
{
$this->verify_not_readonly('update');
if ($validate && !$this->_validate())
return false;
if ($this->is_dirty())
{
$pk = $this->values_for_pk();
if (empty($pk))
throw new ActiveRecordException("Cannot update, no primary key defined for: " . get_called_class());
if (!$this->invoke_callback('before_update',false))
return false;
$dirty = $this->dirty_attributes();
static::table()->update($dirty,$pk);
$this->invoke_callback('after_update',false);
}
return true;
}
/**
* Deletes this model from the database and returns true if successful.
*
* @return boolean
*/
public function delete()
{
$this->verify_not_readonly('delete');
$pk = $this->values_for_pk();
if (empty($pk))
throw new ActiveRecordException("Cannot delete, no primary key defined for: " . get_called_class());
if (!$this->invoke_callback('before_destroy',false))
return false;
static::table()->delete($pk);
$this->invoke_callback('after_destroy',false);
return true;
}
/**
* Helper that creates an array of values for the primary key(s).
*
* @return array An array in the form array(key_name => value, ...)
*/
public function values_for_pk()
{
return $this->values_for(static::table()->pk);
}
/**
* Helper to return a hash of values for the specified attributes.
*
* @param array $attribute_names Array of attribute names
* @return array An array in the form array(name => value, ...)
*/
public function values_for($attribute_names)
{
$filter = array();
foreach ($attribute_names as $name)
$filter[$name] = $this->$name;
return $filter;
}
/**
* Validates the model.
*
* @return boolean True if passed validators otherwise false
*/
private function _validate()
{
require_once 'Validations.php';
$validator = new Validations($this);
$validation_on = 'validation_on_' . ($this->is_new_record() ? 'create' : 'update');
foreach (array('before_validation', "before_$validation_on") as $callback)
{
if (!$this->invoke_callback($callback,false))
return false;
}
$this->errors = $validator->validate();
foreach (array('after_validation', "after_$validation_on") as $callback)
$this->invoke_callback($callback,false);
if (!$this->errors->is_empty())
return false;
return true;
}
/**
* Returns true if the model has been modified.
*
* @return boolean true if modified
*/
public function is_dirty()
{
return empty($this->__dirty) ? false : true;
}
/**
* Run validations on model and returns whether or not model passed validation.
*
* @see is_invalid
* @return boolean
*/
public function is_valid()
{
return $this->_validate();
}
/**
* Runs validations and returns true if invalid.
*
* @see is_valid
* @return boolean
*/
public function is_invalid()
{
return !$this->_validate();
}
/**
* Updates a model's timestamps.
*/
public function set_timestamps()
{
$now = date('Y-m-d H:i:s');
if (isset($this->updated_at))
$this->updated_at = $now;
if (isset($this->created_at) && $this->is_new_record())
$this->created_at = $now;
}
/**
* Mass update the model with an array of attribute data and saves to the database.
*
* @param array $attributes An attribute data array in the form array(name => value, ...)
* @return boolean True if successfully updated and saved otherwise false
*/
public function update_attributes($attributes)
{
$this->set_attributes($attributes);
return $this->save();
}
/**
* Updates a single attribute and saves the record without going through the normal validation procedure.
*
* @param string $name Name of attribute
* @param mixed $value Value of the attribute
* @return boolean True if successful otherwise false
*/
public function update_attribute($name, $value)
{
$this->__set($name, $value);
return $this->update(false);
}
/**
* Mass update the model with data from an attributes hash.
*
* Unlike update_attributes() this method only updates the model's data
* but DOES NOT save it to the database.
*
* @see update_attributes
* @param array $attributes An array containing data to update in the form array(name => value, ...)
*/
public function set_attributes(array $attributes)
{
$this->set_attributes_via_mass_assignment($attributes, true);
}
/**
* Passing $guard_attributes as true will throw an exception if an attribute does not exist.
*
* @throws ActiveRecordUndefinedPropertyException
* @param array $attributes An array in the form array(name => value, ...)
* @param boolean $guard_attributes Flag of whether or not attributes should be guarded
*/
private function set_attributes_via_mass_assignment(array &$attributes, $guard_attributes)
{
//access uninflected columns since that is what we would have in result set
$table = static::table();
$exceptions = array();
$use_attr_accessible = !empty(static::$attr_accessible);
$use_attr_protected = !empty(static::$attr_protected);
$connection = static::connection();
foreach ($attributes as $name => $value)
{
// is a normal field on the table
if (array_key_exists($name,$table->columns))
{
$value = $table->columns[$name]->cast($value,$connection);
$name = $table->columns[$name]->inflected_name;
}
if ($guard_attributes)
{
if ($use_attr_accessible && !in_array($name,static::$attr_accessible))
continue;
if ($use_attr_protected && in_array($name,static::$attr_protected))
continue;
// set valid table data
try {
$this->$name = $value;
} catch (UndefinedPropertyException $e) {
$exceptions[] = $e->getMessage();
}
}
else
{
// ignore OciAdapter's limit() stuff
if ($name == 'ar_rnum__')
continue;
// set arbitrary data
$this->assign_attribute($name,$value);
}
}
if (!empty($exceptions))
throw new UndefinedPropertyException(get_called_class(),$exceptions);
}
/**
* Add a model to the given named ($name) relationship.
*
* @internal This should <strong>only</strong> be used by eager load
* @param Model $model
* @param $name of relationship for this table
* @return void
*/
public function set_relationship_from_eager_load(Model $model=null, $name)
{
$table = static::table();
if (($rel = $table->get_relationship($name)))
{
if ($rel->is_poly())
{
// if the related model is null and it is a poly then we should have an empty array
if (is_null($model))
return $this->__relationships[$name] = array();
else
return $this->__relationships[$name][] = $model;
}
else
return $this->__relationships[$name] = $model;
}
throw new RelationshipException("Relationship named $name has not been declared for class: {$table->class->getName()}");
}
/**
* Reloads the attributes and relationships of this object from the database.
*
* @return Model
*/
public function reload()
{
$this->__relationships = array();
$pk = array_values($this->get_values_for($this->get_primary_key()));
$this->set_attributes($this->find($pk)->attributes);
$this->reset_dirty();
return $this;
}
public function __clone()
{
$this->__relationships = array();
$this->reset_dirty();
return $this;
}
/**
* Resets the dirty array.
*
* @see dirty_attributes
*/
public function reset_dirty()
{
$this->__dirty = null;
}
/**
* A list of valid finder options.
*
* @var array
*/
static $VALID_OPTIONS = array('conditions', 'limit', 'offset', 'order', 'select', 'joins', 'include', 'readonly', 'group', 'from', 'having');
/**
* Enables the use of dynamic finders.
*
* Dynamic finders are just an easy way to do queries quickly without having to
* specify an options array with conditions in it.
*
* <code>
* SomeModel::find_by_first_name('Tito');
* SomeModel::find_by_first_name_and_last_name('Tito','the Grief');
* SomeModel::find_by_first_name_or_last_name('Tito','the Grief');
* SomeModel::find_all_by_last_name('Smith');
* SomeModel::count_by_name('Bob')
* SomeModel::count_by_name_or_state('Bob','VA')
* SomeModel::count_by_name_and_state('Bob','VA')
* </code>
*
* You can also create the model if the find call returned no results:
*
* <code>
* Person::find_or_create_by_name('Tito');
*
* # would be the equivalent of
* if (!Person::find_by_name('Tito'))
* Person::create(array('Tito'));
* </code>
*
* Some other examples of find_or_create_by:
*
* <code>
* Person::find_or_create_by_name_and_id('Tito',1);
* Person::find_or_create_by_name_and_id(array('name' => 'Tito', 'id' => 1));
* </code>
*
* @param string $method Name of method
* @param mixed $args Method args
* @return Model
* @throws {@link ActiveRecordException} if invalid query
* @see find
*/
public static function __callStatic($method, $args)
{
$options = static::extract_and_validate_options($args);
$create = false;
if (substr($method,0,17) == 'find_or_create_by')
{
$attributes = substr($method,17);
// can't take any finders with OR in it when doing a find_or_create_by
if (strpos($attributes,'_or_') !== false)
throw new ActiveRecordException("Cannot use OR'd attributes in find_or_create_by");
$create = true;
$method = 'find_by' . substr($method,17);
}
if (substr($method,0,7) === 'find_by')
{
$attributes = substr($method,8);
$options['conditions'] = SQLBuilder::create_conditions_from_underscored_string(static::table()->conn,$attributes,$args,static::$alias_attribute);
if (!($ret = static::find('first',$options)) && $create)
return static::create(SQLBuilder::create_hash_from_underscored_string($attributes,$args,static::$alias_attribute));
return $ret;
}
elseif (substr($method,0,11) === 'find_all_by')
{
$options['conditions'] = SQLBuilder::create_conditions_from_underscored_string(static::table()->conn,substr($method,12),$args,static::$alias_attribute);
return static::find('all',$options);
}
elseif (substr($method,0,8) === 'count_by')
{
$options['conditions'] = SQLBuilder::create_conditions_from_underscored_string(static::table()->conn,substr($method,9),$args,static::$alias_attribute);
return static::count($options);
}
throw new ActiveRecordException("Call to undefined method: $method");
}
/**
* Enables the use of build|create for associations.
*
* @param string $method Name of method
* @param mixed $args Method args
* @return mixed An instance of a given {@link AbstractRelationship}
*/
public function __call($method, $args)
{
//check for build|create_association methods
if (preg_match('/(build|create)_/', $method))
{
if (!empty($args))
$args = $args[0];
$association_name = str_replace(array('build_', 'create_'), '', $method);
if (($association = static::table()->get_relationship($association_name)))
{
//access association to ensure that the relationship has been loaded
//so that we do not double-up on records if we append a newly created
$this->$association_name;
$method = str_replace($association_name,'association', $method);
return $association->$method($this, $args);
}
}
throw new ActiveRecordException("Call to undefined method: $method");
}
/**
* Alias for self::find('all').
*
* @see find
* @return array array of records found
*/
public static function all(/* ... */)
{
return call_user_func_array('static::find',array_merge(array('all'),func_get_args()));
}
/**
* Get a count of qualifying records.
*
* <code>
* YourModel::count(array('conditions' => 'amount > 3.14159265'));
* </code>
*
* @see find
* @return int Number of records that matched the query
*/
public static function count(/* ... */)
{
$args = func_get_args();
$options = static::extract_and_validate_options($args);
$options['select'] = 'COUNT(*)';
if (!empty($args))
{
if (is_hash($args[0]))
$options['conditions'] = $args[0];
else
$options['conditions'] = call_user_func_array('static::pk_conditions',$args);
}
$table = static::table();
$sql = $table->options_to_sql($options);
$values = $sql->get_where_values();
return $table->conn->query_and_fetch_one($sql->to_s(),$values);
}
/**
* Determine if a record exists.
*
* <code>
* SomeModel::exists(123);
* SomeModel::exists(array('conditions' => array('id=? and name=?', 123, 'Tito')));
* SomeModel::exists(array('id' => 123, 'name' => 'Tito'));
* </code>
*
* @see find
* @return boolean
*/
public static function exists(/* ... */)
{
return call_user_func_array('static::count',func_get_args()) > 0 ? true : false;
}
/**
* Alias for self::find('first').
*
* @see find
* @return Model The first matched record or null if not found
*/
public static function first(/* ... */)
{
return call_user_func_array('static::find',array_merge(array('first'),func_get_args()));
}
/**
* Alias for self::find('last')
*
* @see find
* @return Model The last matched record or null if not found
*/
public static function last(/* ... */)
{
return call_user_func_array('static::find',array_merge(array('last'),func_get_args()));
}
/**
* Find records in the database.
*
* Finding by the primary key:
*
* <code>
* # queries for the model with id=123
* YourModel::find(123);
*
* # queries for model with id in(1,2,3)
* YourModel::find(1,2,3);
*
* # finding by pk accepts an options array
* YourModel::find(123,array('order' => 'name desc'));
* </code>
*
* Finding by using a conditions array:
*
* <code>
* YourModel::find('first', array('conditions' => array('name=?','Tito'),
* 'order' => 'name asc'))
* YourModel::find('all', array('conditions' => 'amount > 3.14159265'));
* YourModel::find('all', array('conditions' => array('id in(?)', array(1,2,3))));
* </code>
*
* Finding by using a hash:
*
* <code>
* YourModel::find(array('name' => 'Tito', 'id' => 1));
* YourModel::find('first',array('name' => 'Tito', 'id' => 1));
* YourModel::find('all',array('name' => 'Tito', 'id' => 1));
* </code>
*
* An options array can take the following parameters:
*
* <ul>
* <li><b>select:</b> A SQL fragment for what fields to return such as: '*', 'people.*', 'first_name, last_name, id'</li>
* <li><b>joins:</b> A SQL join fragment such as: 'JOIN roles ON(roles.user_id=user.id)' or a named association on the model</li>
* <li><b>include:</b> TODO not implemented yet</li>
* <li><b>conditions:</b> A SQL fragment such as: 'id=1', array('id=1'), array('name=? and id=?','Tito',1), array('name IN(?)', array('Tito','Bob')),
* array('name' => 'Tito', 'id' => 1)</li>
* <li><b>limit:</b> Number of records to limit the query to</li>
* <li><b>offset:</b> The row offset to return results from for the query</li>
* <li><b>order:</b> A SQL fragment for order such as: 'name asc', 'name asc, id desc'</li>
* <li><b>readonly:</b> Return all the models in readonly mode</li>
* <li><b>group:</b> A SQL group by fragment</li>
* </ul>
*
* @throws {@link RecordNotFound} if no options are passed or finding by pk and no records matched
* @return mixed An array of records found if doing a find_all otherwise a
* single Model object or null if it wasn't found. NULL is only return when
* doing a first/last find. If doing an all find and no records matched this
* will return an empty array.
*/
public static function find(/* $type, $options */)
{
$class = get_called_class();
if (func_num_args() <= 0)
throw new RecordNotFound("Couldn't find $class without an ID");
$args = func_get_args();
$options = static::extract_and_validate_options($args);
$num_args = count($args);
$single = true;
if ($num_args > 0 && ($args[0] === 'all' || $args[0] === 'first' || $args[0] === 'last'))
{
switch ($args[0])
{
case 'all':
$single = false;
break;
case 'last':
if (!array_key_exists('order',$options))
$options['order'] = join(' DESC, ',static::table()->pk) . ' DESC';
else
$options['order'] = SQLBuilder::reverse_order($options['order']);
// fall thru
case 'first':
$options['limit'] = 1;
$options['offset'] = 0;
break;
}
$args = array_slice($args,1);
$num_args--;
}
//find by pk
elseif (1 === count($args) && 1 == $num_args)
$args = $args[0];
// anything left in $args is a find by pk
if ($num_args > 0 && !isset($options['conditions']))
return static::find_by_pk($args, $options);
$options['mapped_names'] = static::$alias_attribute;
$list = static::table()->find($options);
return $single ? (!empty($list) ? $list[0] : null) : $list;
}
/**
* Finder method which will find by a single or array of primary keys for this model.
*
* @see find
* @param array $values An array containing values for the pk
* @param array $options An options array
* @return Model
* @throws {@link RecordNotFound} if a record could not be found
*/
public static function find_by_pk($values, $options)
{
$options['conditions'] = static::pk_conditions($values);
$list = static::table()->find($options);
$results = count($list);
if ($results != ($expected = count($values)))
{
$class = get_called_class();
if ($expected == 1)
{
if (!is_array($values))
$values = array($values);
throw new RecordNotFound("Couldn't find $class with ID=" . join(',',$values));
}
$values = join(',',$values);
throw new RecordNotFound("Couldn't find all $class with IDs ($values) (found $results, but was looking for $expected)");
}
return $expected == 1 ? $list[0] : $list;
}
/**
* Find using a raw SELECT query.
*
* <code>
* YourModel::find_by_sql("SELECT * FROM people WHERE name=?",array('Tito'));
* YourModel::find_by_sql("SELECT * FROM people WHERE name='Tito'");
* </code>
*
* @param string $sql The raw SELECT query
* @param array $values An array of values for any parameters that needs to be bound
* @return array An array of models
*/
public static function find_by_sql($sql, $values=null)
{
return static::table()->find_by_sql($sql, $values, true);
}
/**
* Determines if the specified array is a valid ActiveRecord options array.
*
* @param array $array An options array
* @param bool $throw True to throw an exception if not valid
* @return boolean True if valid otherwise valse
* @throws {@link ActiveRecordException} if the array contained any invalid options
*/
public static function is_options_hash($array, $throw=true)
{
if (is_hash($array))
{
$keys = array_keys($array);
$diff = array_diff($keys,self::$VALID_OPTIONS);
if (!empty($diff) && $throw)
throw new ActiveRecordException("Unknown key(s): " . join(', ',$diff));
$intersect = array_intersect($keys,self::$VALID_OPTIONS);
if (!empty($intersect))
return true;
}
return false;
}
/**
* Returns a hash containing the names => values of the primary key.
*
* @internal This needs to eventually support composite keys.
* @param mixed $args Primary key value(s)
* @return array An array in the form array(name => value, ...)
*/
public static function pk_conditions($args)
{
$table = static::table();
$ret = array($table->pk[0] => $args);
return $ret;
}
/**
* Pulls out the options hash from $array if any.
*
* @internal DO NOT remove the reference on $array.
* @param array &$array An array
* @return array A valid options array
*/
public static function extract_and_validate_options(array &$array)
{
$options = array();
if ($array)
{
$last = &$array[count($array)-1];
try
{
if (self::is_options_hash($last))
{
array_pop($array);
$options = $last;
}
}
catch (ActiveRecordException $e)
{
if (!is_hash($last))
throw $e;
$options = array('conditions' => $last);
}
}
return $options;
}
/**
* Returns a JSON representation of this model.
*
* @see Serialization
* @param array $options An array containing options for json serialization (see {@link Serialization} for valid options)
* @return string JSON representation of the model
*/
public function to_json(array $options=array())
{
return $this->serialize('Json', $options);
}
/**
* Returns an XML representation of this model.
*
* @see Serialization
* @param array $options An array containing options for xml serialization (see {@link Serialization} for valid options)
* @return string XML representation of the model
*/
public function to_xml(array $options=array())
{
return $this->serialize('Xml', $options);
}
/**
* Creates a serializer based on pre-defined to_serializer()
*
* An options array can take the following parameters:
*
* <ul>
* <li><b>only:</b> a string or array of attributes to be included.</li>
* <li><b>excluded:</b> a string or array of attributes to be excluded.</li>
* <li><b>methods:</b> a string or array of methods to invoke. The method's name will be used as a key for the final attributes array
* along with the method's returned value</li>
* <li><b>include:</b> a string or array of associated models to include in the final serialized product.</li>
* </ul>
*
* @param string $type Either Xml or Json
* @param array $options Options array for the serializer
* @return string Serialized representation of the model
*/
private function serialize($type, $options)
{
require_once 'Serialization.php';
$class = "ActiveRecord\{$type}Serializer";
$serializer = new $class($this, $options);
return $serializer->to_s();
}
/**
* Invokes the specified callback on this model.
*
* @param string $method_name Name of the call back to run.
* @param boolean $must_exist Set to true to raise an exception if the callback does not exist.
* @return boolean True if invoked or null if not
*/
private function invoke_callback($method_name, $must_exist=true)
{
return static::table()->callback->invoke($this,$method_name,$must_exist);
}
/**
* Executes a block of code inside a database transaction.
*
* <code>
* YourModel::transaction(function()
* {
* YourModel::create(array("name" => "blah"));
* });
* </code>
*
* If an exception is thrown inside the closure the transaction will
* automatically be rolled back. You can also return false from your
* closure to cause a rollback:
*
* <code>
* YourModel::transaction(function()
* {
* YourModel::create(array("name" => "blah"));
* throw new Exception("rollback!");
* });
*
* YourModel::transaction(function()
* {
* YourModel::create(array("name" => "blah"));
* return false; # rollback!
* });
* </code>
*
* @param Closure $closure The closure to execute. To cause a rollback have your closure return false or throw an exception.
* @return boolean True if the transaction was committed, False if rolled back.
*/
public static function transaction($closure)
{
$connection = static::connection();
try
{
$connection->transaction();
if ($closure() === false)
{
$connection->rollback();
return false;
}
else
$connection->commit();
}
catch (Exception $e)
{
$connection->rollback();
throw $e;
}
return true;
}
};
?>