/framework/core/db/DbObject.php
PHP | 781 lines | 379 code | 88 blank | 314 comment | 42 complexity | cb9a094a3728fb4fd191f88fc0f9afa7 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1
- <?php
- class DbObject extends Object implements Iterator
- {
- /**
- * The assigned name of the table (defaults to the class name translated by the getDefaultTableName
- *
- * @var string
- */
- protected $tableName;
- /**
- * The field name(s) of the primary key in an array
- *
- * @var array
- */
- protected $primaryKey;
- protected $keyAssignedBy;
- private $missingKeyFields;
- private $bound;
- private $persisted;
- private $scalars;
- protected $relationships;
- const keyAssignedBy_db = 1;
- const keyAssignedBy_dev = 2;
- const keyAssignedBy_auto = 3;
- /**
- * This is the constructor. (honest, I swear)
- *
- * Some things to know about the defaults defined by this constructor:
- * Default primary key: id
- * Default keyAssignedBy: keyAssignedBy_db
- *
- * If keyAssignedBy is keyAssignedBy_db the primary_key array can contain no more than one field
- *
- * @param mixed $init Initial value for the primary key field (if this is supplied there can be only one field in the primary key)
- */
- function __construct($init = NULL)
- {
- // set up some sensible defaults
- $this->primaryKey = array('id');
- $this->tableName = $this->getDefaultTableName();
- $this->bound = false;
- $this->keyAssignedBy = self::keyAssignedBy_db;
- $this->scalars = array();
- $this->relationships = array();
- $this->persisted = NULL;
-
- $this->init($init);
- $this->missingKeyFields = count($this->primaryKey);
- if($this->keyAssignedBy == self::keyAssignedBy_db && count($this->primaryKey) != 1)
- trigger_error("in order for 'keyAssignedBy_db' to work you must have a single primary key field");
- if(is_array($init))
- {
- $this->assignScalars($init);
- }
- else if($init === NULL)
- {
- return;
- }
- else
- {
- assert(count($this->primaryKey) == 1);
- $this->assignScalars(array($this->primaryKey[0] => $init));
- }
- }
- /**
- * This is a second stage constructor that should be overridden in individual database objects to initialize the instance
- *
- */
- protected function init()
- {
- // override this function to setup relationships without having to handle the constructor chaining
- }
- /**
- * Returns the name of the table based on the name of the current class. If the class is called "personObject" the tablename will default to person_object.
- *
- * @return string
- */
- private function getDefaultTableName()
- {
- $name = get_class($this);
- // if there are any capitals after the firstone insert and underscore
- $name = $name[0] . preg_replace('/[A-Z]/', '_$0', substr($name, 1));
- // lowercase everything and return it
- return strtolower($name);
- }
- /**
- * Returns the name of the table associated with the db object
- *
- * @return string
- */
- public function getTableName()
- {
- return $this->tableName;
- }
- /**
- * Returns the value of the primary key of the record that this object is associated with. An error
- * will be thrown if there is more than one primary key.
- *
- * @return mixed
- */
- public function getId()
- {
- assert(count($this->primaryKey) == 1);
- return $this->scalars[$this->primaryKey[0]];
- }
- /**
- * Returns the field name(s) in the primary key in an array
- *
- * @return array of field names
- */
- public function getPrimaryKey()
- {
- return $this->primaryKey;
- }
- /**
- * Returns true if this DbObject class is set to use primary keys generated by the database
- *
- * @return boolean True if the database automatically generates primary keys
- */
- public function primaryKeyAssignedByDb()
- {
- return $this->keyAssignedBy == self::keyAssignedBy_db ? true : false;
- }
- /**
- * Returns a DbTable object with scheme information for the associated table (if supported by your database)
- *
- * @return DbTable DbTable object
- */
- static public function _getTableSchema($className)
- {
- $object = new $className();
- return $object->getSchema();
- }
-
- /**
- * Returns a DbTable object with scheme information for the associated table (if supported by your database)
- *
- * @return DbTable DbTable object
- */
- public function getSchema()
- {
- return new DbTable(self::_getConnection(get_class($this)), $this->tableName);
- }
-
- /**
- * Alias for getSchema
- *
- * @return DbTable DbTable object
- */
- public function getTable()
- {
- return $this->getSchema();
- }
- /**
- * Loops through all the fields in the associated table and ensures a default NULL value for each of them in the class
- * This feature only works with DbConnection objects that have full schema support implemented
- */
- public function forceFields()
- {
- if($this->bound)
- $this->loadScalars();
- else
- {
- foreach($this->getSchema()->fields as $thisField)
- $this->scalars[$thisField->name] = NULL;
- }
- }
- /**
- * Serializes all column names and values and returns them in a string of the format "<DbObject class>: <field> => <value> <field> => <value> ..."
- *
- * @return string string
- */
- public function getString()
- {
- $s = '';
- $this->loadScalars();
- foreach($this->scalars as $field => $value)
- $s .= " $field => $value";
- return get_class($this) . ':' . $s;
- }
- /**
- * Returns the connection associated with the DbObject
- *
- * @return DbConnection DbConnection object
- */
- public function getDb()
- {
- return self::_getConnection(get_class($this));
- }
- //
- // the scalar handlers
- //
- // rewrite them and make them handle primary keys with different names or more than one field
- //
- /**
- * Returns a $field => $value array containing all the fields and their values
- *
- * @return assoc_array assoc_array containing all fields and values
- */
- public function getFields()
- {
- return $this->scalars;
- }
- /**
- * Accepts a $field => $value array containing fields and their values to be set in this DbObject
- *
- * @param assoc_array $data $field => $value associative array with data to be stored in this object
- */
- public function setFields($data)
- {
- $this->assignScalars($data);
- }
- /**
- * Returns the value of the specified field
- *
- * @param string $field Field to retreive
- * @return string String value of the field
- */
- public function getField($field)
- {
- return $this->getScalar($field);
- }
- /**
- * Returns the value of the specified field
- *
- * @param string $field Field to retreive
- * @return string String value of the field
- */
- private function getScalar($field)
- {
- if(!isset($this->scalars[$field]))
- {
- if(!$this->bound)
- {
- /* TODO: Handle "getScalar" calls to unbound DbObject instances
- Different possibilities on how to handle this situation. Maybe we could use some flags.
- 1. check the metadata. (alwaysCheckMeta)
- 1. if its there then (useDummyDefaults requires alwaysCheckMeta)
- 1. return the default value
- 2. return NULL
- 2. if its not there
- 1. throw and error
- 2. dont check the metadata (useDummyNulls requires !alwaysCheckMeta)
- 1. return null
- 2. throw an error
- trigger_error("the field: $field is not present in memory and this object is not yet bound to a database row");
- */
- return NULL;
- }
- $this->loadScalars();
- }
-
- if(!array_key_exists($field, $this->scalars))
- trigger_error("the field $field is present neither in memory nor in the cooresponding database table");
- return $this->scalars[$field];
- }
- /**
- * Assigns a value to the specified field
- *
- * @param string $field Field to change the value of
- * @param mixed $value Value to assign to the specified field
- */
- private function setScalar($field, $value)
- {
- $data[$field] = $value;
- $this->assignScalars($data);
- }
- /*
- private function setScalars($data)
- {
- foreach($data as $field => $value)
- {
- $this->scalars[$field] = $value;
- }
- }
- */
- /**
- * Accepts a $field => $value array containing fields and their values to be set in this DbObject
- *
- * @param assoc_array $data $field => $value associative array with data to be stored in this object
- */
- private function assignScalars($data)
- {
- foreach($data as $member => $value)
- {
- if(!isset($this->scalars[$member]) && in_array($member, $this->primaryKey))
- {
- $this->missingKeyFields--;
- if($this->missingKeyFields == 0)
- $this->bound = 1;
- }
- $this->scalars[$member] = $value;
- }
- }
- /**
- * Loads values into the fields of the DbObject
- *
- */
- private function loadScalars()
- {
- assert($this->bound);
- $row = $this->fetchPersisted();
- $this->assignPersisted($row);
- }
- private function assignPersisted($row)
- {
- // if they manually set a field don't write over it just because they loaded one scalar
- foreach($row as $field => $value)
- {
- if(!isset($this->scalars[$field]))
- $this->scalars[$field] = $value;
- }
- }
- /**
- * Retrieves field values from the database using primary key as lookup fields
- *
- * @return unknown
- */
- private function fetchPersisted()
- {
- $wheres = array();
- $whereValues = array();
- foreach($this->primaryKey as $keyField)
- {
- $wheres[] = ":fld_$keyField:identifier = :$keyField";
- $whereValues["fld_$keyField"] = $keyField;
- $whereValues[$keyField] = $this->scalars[$keyField];
- }
- $whereClause = implode(' and ', $wheres);
- $row = self::_getConnection(get_class($this))->fetchRow("select * from $this->tableName where $whereClause", $whereValues);
- if($row)
- $this->persisted = true;
- else
- $this->persisted = false;
- return $row;
- }
- /**
- * Returns true if this DbObject is (and can be) saved in the database
- *
- * @return boolean True if the DbObject is/can be saved in the DB
- */
- private function _persisted()
- {
- if(!$this->bound)
- return false;
- if($this->keyAssignedBy == self::keyAssignedBy_db)
- return true;
- else
- {
- $row = $this->fetchPersisted();
- if($row)
- {
- // we might as well save the results
- $this->assignPersisted($row);
- return true;
- }
- return false;
- }
- }
- /**
- * Returns true if this DbObject is (and can be) saved in the database
- *
- * @return boolean True if the DbObject is/can be saved in the DB
- */
- public function persisted()
- {
- if($this->persisted !== NULL)
- return $this->persisted;
- else
- return $this->persisted = $this->_persisted();
- }
- /**
- * Saves the record in memory
- *
- */
- public function save()
- {
- if(!$this->bound)
- {
- if($this->keyAssignedBy == self::keyAssignedBy_db)
- $this->setScalar($this->primaryKey[0], self::_getConnection(get_class($this))->insertArray($this->tableName, $this->scalars));
- else
- trigger_error("you must define all foreign key fields in order by save this object");
- }
- else
- {
- if($this->keyAssignedBy == self::keyAssignedBy_db)
- {
- $updateInfo = DbConnection::generateUpdateInfo($this->tableName, $this->getKeyConditions(), $this->scalars);
- self::_getConnection(get_class($this))->updateRow($updateInfo['sql'], $updateInfo['params']);
- }
- else
- {
- if(!$this->persisted())
- self::_getConnection(get_class($this))->insertArray($this->tableName, $this->scalars, false);
- else
- {
- $updateInfo = DbConnection::generateUpdateInfo($this->tableName, $this->getKeyConditions(), $this->scalars);
- self::_getConnection(get_class($this))->updateRow($updateInfo['sql'], $updateInfo['params']);
- }
- }
- }
- }
- /**
- * Returns an array containing all primary key fields that have a value assigned to them in this DbObject instance
- *
- * @return array of fields
- */
- private function getKeyConditions()
- {
- assert($this->bound);
- return array_intersect_key($this->scalars, array_flip($this->primaryKey));
- }
- /**
- * Deletes the record from the database, deletes all fields and values from memory, and unbinds the DbObject
- *
- */
- public function destroy()
- {
- // have a way to destroy any existing vector fields or refuse to continue (destroy_r)
- $deleteInfo = DbConnection::generateDeleteInfo($this->tableName, $this->getKeyConditions());
- self::_getConnection(get_class($this))->deleteRow($deleteInfo['sql'], $deleteInfo['params']);
- $this->bound = false;
- $this->scalars = array();
- $this->persisted = false;
- }
- //
- // end of scalar handlers
- //
- //
- // vector handlers
- //
- private function addRelationship($name, $relationship)
- {
- $this->relationships[$name] = $relationship;
- }
- private function hasRelationship($name)
- {
- return isset($this->relationships[$name]) ? true : false;
- }
- private function getRelationshipInfo($name)
- {
- return $this->relationships[$name]->getInfo();
- }
- protected function hasMany($name, $params = array())
- {
- if(isset($params['through']) && $params['through'])
- $this->addRelationship($name, new DbRelationshipHasManyThrough($name, $params, $this));
- else
- $this->addRelationship($name, new DbRelationshipHasMany($name, $params, $this));
- }
- protected function hasOne($name, $params = array())
- {
- $this->addRelationship($name, new DbRelationshipHasOne($name, $params, $this));
- }
- protected function belongsTo($name, $params = array())
- {
- $this->addRelationship($name, new DbRelationshipBelongsTo($name, $params, $this));
- }
- protected function fieldOptions($name, $params = array())
- {
- $this->addRelationship($name, new DbRelationshipOptions($name, $params, $this));
- }
-
- public function getFieldOptions($field)
- {
- foreach($this->relationships as $thisRelationship)
- if($thisRelationship instanceof DbRelationshipOptions && $thisRelationship->isTiedToField($field))
- return $thisRelationship;
-
- return false;
- }
- //
- // end vector handlers
- //
- //
- // begin magic functions
- //
- /**
- * Automatic getter: maps unknown variables to database fields
- *
- * @param string $varname Name of the database field to get the value of
- * @return mixed Value of the given database field
- */
- public function __get($varname)
- {
- // check on inherited getters, setters, and mixins from Object
- if(parent::__isset($varname))
- return parent::__get($varname);
-
- if($this->hasRelationship($varname))
- return $this->getRelationshipInfo($varname);
- return $this->getScalar($varname);
- }
- /**
- * Automatic setter: maps unknown variables to database fields
- *
- * @param string $varname Name of the database field to set the value of
- * @param mixed $value New value for the given database field
- */
- function __set($varname, $value)
- {
- $this->setScalar($varname, $value);
- }
- //
- // end magic functions
- //
- //
- // begin iterator functions
- //
- /**
- * Resets the internal pointer to the first column
- *
- */
- public function rewind()
- {
- reset($this->scalars);
- }
- /**
- * Returns the value of the column that the internal pointer is at
- *
- * @return mixed
- */
- public function current()
- {
- $var = current($this->scalars);
- return $var;
- }
- /**
- * returns the name of the column the internal pointer is at
- *
- * @return string Column Name
- */
- public function key()
- {
- $var = key($this->scalars);
- return $var;
- }
- /**
- * Moves the internal pointer to the next column and returns the value of that column
- *
- * @return mixed Value of the next column
- */
- public function next()
- {
- $var = next($this->scalars);
- return $var;
- }
- /**
- * Returns true if this DbObject is successfully bound to a row in the database
- *
- * @return boolean True if the object is bound to a row in the database
- */
- public function valid()
- {
- $var = $this->current() !== false;
- return $var;
- }
- //
- // end iterator functions
- //
- //
- // static methods
- //
- /**
- * Returns the name of the default connection to be used with this DbObject (override in child class to default to a different connection)
- *
- * @param string $className Name of the DbObject to get the default connection name for
- * @return string Name of the default database connection for this object
- */
- static private function _getConnectionName($className)
- {
- return 'default';
- }
- /**
- * Static method to return the database connection associated with a given DbObject
- *
- * @param string $className Name of the DbObject to retreive the default connection of
- * @return DbConnection object
- */
- static private function _getConnection($className)
- {
- return DbModule::getConnection(call_user_func(array($className, '_getConnectionName'), $className));
- }
- /**
- * Returns the name of the SQL table based on the name of the DbObject class
- *
- * @param string $className
- * @return string Name of the SQL table to link to
- */
- static public function _getTableName($className)
- {
- // work around lack of "late static binding"
- $dummy = new $className();
- return $dummy->getTableName();
- }
- /**
- * Static method creates a new DbObject by name. There must be a class that inherits from DbObject of the name "className" for this to work.
- *
- * @param string $className Name of the DbObject class to use
- * @param array $values Associative array of $fieldName => $value to store in the table
- * @return DbObject
- */
- static public function _create($className, $values)
- {
- $object = new $className($values);
- $object->save();
- return $object;
- }
- /**
- * Inserts a row into the table associated with the given DbObject class
- *
- * @param string $className Name of the DbObject class
- * @param assoc_array $values $field => $value array to be inserted into the database (must contain all required fields or a SQL error will be generated)
- */
- static public function _insert($className, $values)
- {
- self::_getConnection($className)->insertArray(self::_getTableName($className), $values, false);
- }
- /**
- * Returns an array of DbObjects each representing a row in the database returned by the given SQL statement
- *
- * @param string $className Name of the DbObject class to retreive
- * @param string $sql SQL select statement to use to search
- * @param assoc_array $params $param => $value array with parameters to be substituted into the SQL statement
- * @return array of DbObject
- */
- static public function _findBySql($className, $sql, $params)
- {
- $res = self::_getConnection($className)->query($sql, $params);
- if(!$res->valid())
- return array();
- $objects = array();
- for($row = $res->current(); $res->valid(); $row = $res->next())
- {
- $objects[] = new $className($row);
- }
- return $objects;
- }
- /**
- * Returns an array of DbObjects each representing a row in the database returned by selecting on the DbObject specified with the given WHERE clause
- *
- * @param string $className Name of the DbObject class to retreive
- * @param string $where SQL "where" clause (minus the "where " at the beginning) to select on
- * @param assoc_array $params $param => $value array with parameters to be substituted into the WHERE clause
- * @return array of DbObject
- */
- static public function _findByWhere($className, $where, $params)
- {
- $tableName = DbObject::_getTableName($className);
- return self::_findBySql($className, "select * from $tableName where $where", $params);
- }
- /**
- * Searches the database and returns an array of DbObjects each representing a record in the resultset
- *
- * @param string $className Name of the DbObject class to retreive
- * @param assoc_array $conditions $field => $value array of conditions to search on (currently only "=" operator is supported)
- * @param assoc_array $params $param => $value array of parameters to be passed to generateSelectInfo
- * @return array of DbObject (s)
- */
-
- static public function _find($className, $conditions = NULL, $params = NULL)
- {
- $tableName = DbObject::_getTableName($className);
- $selectInfo = self::_getConnection($className)->generateSelectInfo($tableName, '*', $conditions, $params);
- return self::_findBySql($className, $selectInfo['sql'], $selectInfo['params']);
- }
- /**
- * Retrieve one object from the database and map it to an object. Throws an error if more than one row is returned.
- *
- * @param string $className The name of the class corresponding to the table in the database
- * @param array $conditions Key value pair for the fields you want to look up
- * @return DbObject
- */
- static public function _findOne($className, $conditions = NULL)
- {
- $a = DbObject::_find($className, $conditions);
- if(!$a)
- return false;
- assert(is_array($a));
- assert(count($a) == 1);
- return current($a);
- }
- /**
- * Retrieves a DbObject from the given table with a row in it, creating the row if neccesary
- *
- * @param string $className name of the DbObject class to return an instance of
- * @param assoc_array $conditions $field => $value for generating the where clause to
- * @return DbObject
- */
- static public function _getOne($className, $conditions = NULL, $default = NULL)
- {
- $tableName = DbObject::_getTableName($className);
- $row = self::_getConnection($className)->selsertRow($tableName, "*", $conditions, $default);
- return new $className($row);
- }
- //
- // end static methods
- //
- }