Sources for file plugins/class/activerecord.php in version 4.0 Beta 1
Click on a comment to hide it. Click here to show all comments.
/**
* Project: Xnyo 4: Bubbles
* File: plugins/class/activerecord.php
*
* Version: 4.0-dev
* SVN Id: $Id: activerecord.php 10 2007-08-01 01:02:01Z bok $
* SVN URL: $HeadURL:
http://svn.syd.wholesalebroadband.com.au/xnyo/trunk/plugins/class/activerecord.php $
* Authors: Robert Amos <bok[at]odynia.org>
*
* Copyright (c) 2001-2007 Robert Amos <bok[at]odynia.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
**/
/**
* This class implements activerecord support in Xnyo
**/
class XnyoActiveRecord implements IteratorAggregate
{
// this contains the current PDOStatement object.
protected $statement;
// this contains the current XnyoDatabasePlugin object
protected $resource;
// this contains the last executed SQL statement
protected $sql;
// this is an array containing the results of the current view
protected $results = array();
// contains the number of elements available
protected $count = 0;
// contains the current element number
protected $key = 0;
// the parent table, if there is one
protected $uptable;
// all of the columns
public $columns = array();
// xnyo
private $xnyo;
/**
* Get Iterator
*
* This returns a XnyoActiveRecordIterator object with the current results.
* See further down in this file
**/
public function getIterator ()
{
return new XnyoActiveRecordIterator($this->results);
}
/**
* Constructor
**/
public function __construct ($uptable=null)
{
global $db;
$this->xnyo = $GLOBALS['xnyo_parent'];
// check to see if we have a valid database specified.
if (!isset($this->database) || empty($this->database))
throw new XnyoError('No database specified for dbspec plugin <b>%s</b>', __CLASS__);
// we want to announce ourselves to the XnyoDB collector so it can establish a connection
if (XNYO_DEBUG) $this->xnyo->d('Announcing <b>%s</b> to the XnyoDB collector object.',
get_class($this));
$this->resource = $db->announce($this);
// we're being created as a reference inside another table, store that
if (!is_null($uptable))
$this->uptable = $uptable;
}
/**
* Populate me with results of a basic query
**/
public function populate ($sql=null)
{
// pass the query through to the PDO object
if (!is_null($sql))
{
$this->statement = $this->resource->query($sql);
// store the SQL for later debug usage
$this->sql = $sql;
}
elseif (!is_object($this->statement))
throw new XnyoError('Unable to populate with results of existing query because no statement
object found.');
// empty the current results
$this->results = array();
// no results?
if ($this->statement->rowCount() == 0)
return false;
// for all the matching rows
// get our child class name
if (class_exists(get_class($this).'Child'))
{
$c = new get_class($this).'Child';
if ($c instanceof XnyoActiveRecordChild)
$class = get_class($this).'Child';
else
throw new XnyoError('Attempted to use <b>%s</b> as the child row record for <b>%s</b> however it
does not extend <b>XnyoActiveRecordChild</b>.', get_class($this).'Child', get_class($this));
} else
$class = 'XnyoActiveRecordChild';
for ($i = 0, $c = $this->statement->rowCount(); $i < $c; $i++)
{
// fetch a XnyoActiveRecordChild object with the result for this row
$this->results[] = $this->statement->fetchObject($class, array($this, $this->xnyo));
}
// total of all the results
$this->count = count($this->results);
return $this->count;
}
/**
* Load
*
* Loads the specified id into .. me
**/
public function load ($id)
{
return $this->find(array('id' => $id));
}
/**
* Find
*
* Magic find method, lets you find based on anything.
**/
public function find ($params=null, $order=null)
{
// check
if (is_null($params))
$params = array();
else
$params = (array)$params;
// ordering on multiple fields?
if (is_null($order))
$order = array('id');
elseif (strpos($order, ',') !== false)
$order = explode(',', $order);
else
$order = array($order);
// build the sql
$sql = 'SELECT * FROM '.$this->table;
// loop over the params
if (count($params))
{
$sql .= ' WHERE ';
foreach ($params as $name => $value)
{
if (!isset($this->columns[$name]))
throw new XnyoError('Unable to find based on invalid column <b>%s</b> in table <b>%s</b>.',
$name, $this->table);
if (is_array($value) && count($value) == 2)
$sql .= $name.' '.$value[0].' :'.$name.' AND ';
else
$sql .= $name.' = :'.$name.' AND ';
}
$sql = substr($sql, 0, -4);
// field to order by
foreach ($order as $o)
{
$o = explode(' ', $o);
if (!isset($this->columns[$o[0]]))
throw new XnyoError('Unable to order find results by invalid column <b>%s</b> in table
<b>%s</b>.', $o[0], $this->table);
}
$sql .= ' ORDER BY '.join(',', $order);
// prepare the statement
$this->statement = $this->resource->prepare($sql);
// loop the parameters.. again
foreach ($params as $name => $value)
{
// cater for null values and columns that are not allowed to be null
if (is_array($value))
$value = $value[1];
if (is_null($value) || $value === '')
{
// not allowed to be null!
if ($this->columns[$name]['notnull'])
throw new XnyoWarning('Unable to find rows in table <b>%s</b> where column <b>%s</b> is null
because column <b>%s</b> cannot be null.', $this->__objs['table'], $name, $name);
// bind a null value
$this->statement->bindValue(':'.$name, null, PDO::PARAM_NULL);
continue;
}
// bind the parameter to the sql query
$this->statement->bindValue(':'.$name, $value,
XnyoDB::xnyoToPDOType($this->columns[$name]['type']));
}
} else
{
// field to order by
foreach ($order as $o)
{
$o = explode(' ', $o);
if (!isset($this->columns[$o[0]]))
throw new XnyoError('Unable to order find results by invalid column <b>%s</b> in table
<b>%s</b>.', $o[0], $this->table);
}
$sql .= ' ORDER BY '.join(',', $order);
// prepare to select all!
if (XNYO_DEBUG) $this->xnyo->d('Executing query for find: <b>%s</b>', $sql);
$this->statement = $this->resource->prepare($sql);
}
// store the SQL for later debug usage
if (XNYO_DEBUG) $this->xnyo->d('Executing query for find: <b>%s</b>', $sql);
$this->sql = $sql;
// execute the statement and populate us with the results
$this->statement->execute();
return $this->populate();
}
/**
* Returns the current PDO object.
*
* This is used by the XnyoActiveRecordChild objects to make a connection
* back to the database.
**/
public function get_resource ()
{
return $this->resource;
}
/**
* Retrieves the last executed SQL statement (for debugging)
**/
public function get_sql ()
{
return $this->sql;
}
/**
* Add a empty placeholder row
**/
public function add ()
{
// add an empty XnyoActiveRecordChild object to the results array
$this->results[] = new XnyoActiveRecordChild($this, $this->xnyo);
// point our current row at the new one
$this->key = $this->count;
// update the count
$this->count++;
// is there a parent table we need to reference?
if (is_object($this->uptable))
{
$c = strtolower(get_class($this->uptable));
$this->results[$this->key]->$c = $this->uptable->id;
}
}
/**
* Clear all rows!
**/
public function reset ()
{
unset($this->results);
$this->results = array();
$this->rewind();
$this->count = 0;
}
/**
* Next
*
* Advance us to the next row if we're not already on the last row
**/
public function next ()
{
if (($this->key + 1) < $this->count)
$this->key++;
}
/**
* Prev
*
* Rewinds to the previous row if we're not already on the first row
**/
public function prev ()
{
if ($this->key > 0)
$this->key--;
}
/**
* Rewind
*
* Rewinds to the first row
**/
public function rewind ()
{
// take us back to the start
$this->key = 0;
}
/**
* End
*
* Advances to the last row
**/
public function end ()
{
if ($this->count != 0)
$this->key = $this->count - 1;
}
/**
* Count
*
* Returns the number of result rows
**/
public function count ()
{
return $this->count;
}
/**
* Return the row instance
**/
public function current()
{
return $this->results[$this->key];
}
/**
* Accessing data at the current element
**/
public function __get($var)
{
if (!isset($this->results[$this->key]) || !isset($this->results[$this->key]->$var))
return null;
return $this->results[$this->key]->$var;
}
/**
* Setting data at the current element
**/
public function __set($var, $value)
{
if (!isset($this->results[$this->key]))
return false;
return $this->results[$this->key]->$var = $value;
}
/**
* Does the element exist?
**/
public function __isset($var)
{
if (!isset($this->results[$this->key]))
return false;
return isset($this->results[$this->key]->$var);
}
/**
* Unset the element
**/
public function __unset($var)
{
if (isset($this->results[$this->key]))
unset($this->results[$this->key]->$var);
}
/**
* Export all rows
**/
public function export ()
{
$r = array();
foreach ($this->results as $var)
$r[] = $var->export();
return $r;
}
/**
* Remove the column from this reference
**/
public function removeColumn ($col)
{
if (!isset($this->columns) || empty($this->columns) || !isset($this->columns[$col]))
return false;
unset($this->columns[$col]);
// and from any results for security reasons
foreach ($this->results as $r)
$r->removeColumn($col);
}
/**
* Save Current Element
**/
public function save ()
{
if (!isset($this->results[$this->key]))
return false;
return $this->results[$this->key]->save();
}
/**
* Save all
**/
public function saveAll ()
{
foreach ($this->results as $v)
$v->save();
}
/**
* Any other method call
**/
public function __call ($func, $args)
{
$func = strtolower($func);
// check to see if the method they're calling is set_<column>
foreach ($this->columns as $name => $value)
if ($func == 'set'.$name || $func == 'set_'.$name)
$this->results[$this->key]->$name = $args[0];
}
}
/**
* XnyoActiveRecordIterator
*
* Allows you to use foreach() on the XnyoActiveRecord-based object. Each XnyoActiveRecordChild
* object is independant of the parent one, see XnyoActiveRecordChild below.
**/
class XnyoActiveRecordIterator implements Iterator
{
private $results = array();
private $key = 0;
private $count = 0;
/**
* Store the results in an internal array
**/
public function __construct ($results)
{
$this->results = $results;
$this->count = count($this->results);
}
/**
* Rewind
*
* Rewinds us to the first row
**/
public function rewind ()
{
$this->key = 0;
}
/**
* Valid
*
* Returns whether the current row exists
**/
public function valid ()
{
return $this->key < $this->count;
}
/**
* Key
*
* Returns the current row number
**/
public function key ()
{
return $this->key;
}
/**
* Current
*
* Returns the contents of the current row
**/
public function current ()
{
return $this->results[$this->key];
}
/**
* Next
*
* Advances to the next row
**/
public function next ()
{
$this->key++;
}
}
/**
* XnyoActiveRecordChild
*
* Our self-contained object representing a row
**/
class XnyoActiveRecordChild
{
// store data outside of the table results
private $__objs = array();
public function __construct ($parent, $xnyo)
{
/**
* Store references to the parent object so we can use
* it to communicate with the database
**/
$this->__objs['parent'] = $parent;
$this->__objs['xnyo'] = $xnyo;
$this->__objs['table'] = $parent->table;
$this->__objs['database'] = $parent->database;
// initialise the array, if its not already been done
if (!isset($this->__objs['result']) || !is_array($this->__objs['result']))
$this->__objs['result'] = array();
// Relationships with other tables
if (isset($this->__objs['parent']->has_many))
{
// strtolower() all references, for ease of comparison later
if (is_array($this->__objs['parent']->has_many))
foreach ($this->__objs['parent']->has_many as $k => $v)
$this->__objs['parent']->has_many[$k] = strtolower($v);
else
$this->__objs['parent']->has_many = strtolower($this->__objs['parent']->has_many);
// create the blank array
$this->__objs['has_many'] = array();
}
// Has One
if (isset($this->__objs['parent']->has_one))
{
// strtolower() all references, for ease of comparison later
if (is_array($this->__objs['parent']->has_one))
foreach ($this->__objs['parent']->has_one as $k => $v)
$this->__objs['parent']->has_one[$k] = strtolower($v);
else
$this->__objs['parent']->has_one = strtolower($this->__objs['parent']->has_one);
// create the blank array
$this->__objs['has_one'] = array();
}
// Belongs To
if (isset($this->__objs['parent']->belongs_to))
{
// strtolower() all references, for ease of comparison later
if (is_array($this->__objs['parent']->belongs_to))
foreach ($this->__objs['parent']->belongs_to as $k => $v)
$this->__objs['parent']->belongs_to[$k] = strtolower($v);
else
$this->__objs['parent']->belongs_to = strtolower($this->__objs['parent']->belongs_to);
// create the blank array
$this->__objs['belongs_to'] = array();
}
// Keep a copy of the original result for comparison
$this->__objs['orig'] = array();
// populate the original result
$this->copy_orig();
}
/**
* Copy Orig
*
* Take a snapshot of what our row looks like so that we can
* compare results later on to see if it has been modified.
**/
private function copy_orig()
{
foreach ($this->__objs['result'] as $name => $value)
$this->__objs['orig'][$name] = $value;
}
/**
* Destructor
*
* When the object is being destroyed, check to see if we have been
* changed. If we have, update the database before we die.
*
* We don't insert new rows on destruction though, they need to be
* saved implicitely via save()
**/
public function __destruct ()
{
$this->save(true);
}
/**
* Save
*
* Saves the row (only if it has been changed). Will auto-detect
* whether or not a row is new and insert it if appropriate.
**/
public function save ($destruct=false)
{
// are we a new row?
if (count($this->__objs['orig']) == 0)
{
// we dont insert during destruct, it needs to be implicetly saved
if (!$destruct)
$this->insert();
return true;
}
// Lets checks to see if any of our properties have been changed.
$changed = array();
foreach ($this->__objs['parent']->columns as $name => $value)
{
// we can't change the id or the objs array, obviously
if ($name == 'id' || $name == 'modified' || $name == 'created')
continue;
// check changed first..
if ($this->__objs['result'][$name] != $this->__objs['orig'][$name])
{
// is this one null?
if ((!isset($this->__objs['result'][$name]) || $this->__objs['result'][$name] === null ||
$this->__objs['result'][$name] === '') && $this->__objs['parent']->columns[$name]['notnull'])
{
// Can't throw errors in destructors
if (!$destruct)
throw new XnyoError('Unable to update row <b>%s</b> in table <b>%s</b> because column
<b>%s</b> cannot be null.', $this->id, $this->__objs['table'], $name);
$this->__objs['xnyo']->storage->error->raise(new XnyoError('Unable to update row <b>%s</b> in
table <b>%s</b> because column <b>%s</b> cannot be null.', $this->id, $this->__objs['table'],
$name));
} elseif (!isset($this->__objs['result'][$name]) || $this->__objs['result'][$name] === null ||
$this->__objs['result'][$name] === '')
$changed[$name] = null;
else
$changed[$name] = $this->__objs['result'][$name];
}
}
// has anything changed?
if (count($changed) > 0)
$this->update($changed);
// save all our related rows too
if (isset($this->__objs['has_many']) && is_array($this->__objs['has_many']))
foreach ($this->__objs['has_many'] as $r)
$r->save();
if (isset($this->__objs['has_one']) && is_array($this->__objs['has_one']))
foreach ($this->__objs['has_one'] as $r)
$r->save();
if (isset($this->__objs['belongs_to']) && is_array($this->__objs['belongs_to']))
foreach ($this->__objs['belongs_to'] as $r)
$r->save();
}
/**
* Remove a column from reference
**/
public function removeColumn ($col)
{
if (isset($this->__objs['result'][$col]))
unset($this->__objs['result'][$col]);
if (isset($this->__objs['orig'][$col]))
unset($this->__objs['orig'][$col]);
}
/**
* Export the row using a stdClass
**/
public function export ()
{
$r = new stdClass;
foreach ($this->__objs['parent']->columns as $c => $v)
if (!isset($this->__objs['result'][$c]))
$r->$c = null;
else
$r->$c = $this->__objs['result'][$c];
return $r;
}
/**
* Update a row
**/
protected function update ($values)
{
// Build up the SQL query
$sql = 'UPDATE '.$this->__objs['table'].' SET ';
// show which ones have changed
foreach ($values as $name => $value)
$sql .= $name . ' = :'.$name.', ';
// and add the modified field
if (isset($this->__objs['parent']->columns['modified']))
$sql .= 'modified = '.time().', ';
// trim off any extra ", "'s
$sql = substr($sql, 0, -2);
// and append our WHERE clause.
$sql .= ' WHERE id = '.$this->id;
// get a reference to the PDO object so we can talk to the database
$res = $this->__objs['parent']->get_resource();
// PDO::prepare() the query
$statement = $res->prepare($sql);
// bind our parameters
foreach ($values as $name => $value)
{
if ($value === null || $value === '')
{
$statement->bindValue(':'.$name, null, PDO::PARAM_NULL);
continue;
}
$statement->bindValue(':'.$name, $value,
XnyoDB::xnyoToPDOType($this->__objs['parent']->columns[$name]['type']));
}
// execute the query
if ($statement->execute())
// make a snapshot of the current state of the row
$this->copy_orig();
}
/**
* Insert a new row
**/
protected function insert ()
{
$sql = 'INSERT INTO '.$this->__objs['table'].' (';
foreach ($this->__objs['parent']->columns as $name => $value)
{
if ($name == 'id')
continue;
$names[] = $name;
$values[] = ':'.$name;
}
$sql .= join(', ', $names).') VALUES ('.join(', ', $values).')';
// get a reference to the PDO object so we can talk to the database
$res = $this->__objs['parent']->get_resource();
// PDO::prepare() the query
$statement = $res->prepare($sql);
// bind our parameters
foreach ($this->__objs['parent']->columns as $name => $value)
{
if ($name == 'id')
continue;
// cater for created/modified fields
if (($name == 'created' || $name == 'modified') &&
$this->__objs['parent']->columns[$name]['type'] == XnyoDB::INT)
{
$statement->bindValue(':'.$name, time(),
XnyoDB::xnyoToPDOType($this->__objs['parent']->columns[$name]['type']));
continue;
}
// cater for null values and columns that are not allowed to be null
if (!isset($this->__objs['result'][$name]) || $this->__objs['result'][$name] === null ||
$this->__objs['result'][$name] == '')
{
// not allowed to be null!
if ($this->__objs['parent']->columns[$name]['notnull'])
throw new XnyoWarning('Unable to insert row in table <b>%s</b> because column <b>%s</b> cannot
be null.', $this->__objs['table'], $name);
// bind a null value
$statement->bindValue(':'.$name, null, PDO::PARAM_NULL);
continue;
}
// bind the parameter to the sql query
$statement->bindValue(':'.$name, $this->__objs['result'][$name],
XnyoDB::xnyoToPDOType($this->__objs['parent']->columns[$name]['type']));
}
// execute the query
if ($statement->execute())
{
if (isset($this->__objs['parent']->columns['id']))
$this->id = $res->lastInsertId($this->__objs['table']);
$this->copy_orig();
}
}
/**
* Get a column from the row
**/
public function __get ($var)
{
// relations - has_many
if (isset($this->__objs['parent']->has_many))
{
// is this a from a table we've loaded before?
if (isset($this->__objs['has_many'][$var]))
return $this->__objs['has_many'][$var];
// is it a valid has_many?
if ((is_array($this->__objs['parent']->has_many) && in_array(strtolower($var),
$this->__objs['parent']->has_many)) || $this->__objs['parent']->has_many === $var)
{
// yep, load up a new class for that table
$this->__objs['has_many'][$var] = new $var($this->__objs['parent']);
// populate it with all rows that have their class column as my id
// eg. if im a 'person' object ill find all rows with person = my_id
$this->__objs['has_many'][$var]->find(array(strtolower(get_class($this->__objs['parent'])) =>
$this->id));
// return resulting object
return $this->__objs['has_many'][$var];
}
}
// relations - has_one
if (isset($this->__objs['parent']->has_one))
{
// is this from a table we've loaded before?
if (isset($this->__objs['has_one'][$var]))
return $this->__objs['has_one'][$var];
// is it a valid has_one?
if ((is_array($this->__objs['parent']->has_one) && in_array(strtolower($var),
$this->__objs['parent']->has_one)) || $this->__objs['parent']->has_one === $var)
{
// yep, load up a new class for that table.
$this->__objs['has_one'][$var] = new $var($this->__objs['parent']);
// populate it with all the rows that reference me
$this->__objs['has_one'][$var]->find(array(strtolower(get_class($this->__objs['parent'])) =>
$this->id), null, 1);
// return the object
return $this->__objs['has_one'][$var];
}
}
// relations - belongs_to
if (isset($this->__objs['parent']->belongs_to))
{
// is this from a table we've loaded before?
if (isset($this->__objs['belongs_to'][$var]))
return $this->__objs['belongs_to'][$var];
// is it a valid belongs_to?
if ((is_array($this->__objs['parent']->belongs_to) && in_array(strtolower($var),
$this->__objs['parent']->belongs_to)) || $this->__objs['parent']->belongs_to === $var)
{
// yep, load up a new class for that table.
$this->__objs['belongs_to'][$var] = new $var($this->__objs['parent']);
// populate it with the row i have listed for that table
$this->__objs['belongs_to'][$var]->load($this->__objs['result'][$var]);
// return the object
return $this->__objs['belongs_to'][$var];
}
}
// check to see if this column exists in this table
if (!isset($this->__objs['parent']->columns[$var]['type']) ||
!isset($this->__objs['result'][$var]))
return null;
// return the column data
return $this->__objs['result'][$var];
}
/**
* Set a column in a row
**/
public function __set ($var, $value)
{
// the constructor hasn't fired yet, meaning that this is a PDO::fetchObject filling contents
if (!isset($this->__objs['parent']))
return $this->__objs['result'][$var] = $value;
// does this column exist?
if (!isset($this->__objs['parent']->columns[$var]))
throw new XnyoError('Unable to set column <b>%s</b> of row in table <b>%s</b> as column does not
exist.', $var, $this->__objs['table']);
// filter it
$type = $this->__objs['parent']->columns[$var]['type'];
if (!in_array($type, get_class_methods('XnyoInput')))
throw new XnyoError('Unable to set column <b>%s</b> of row in table <b>%s</b> because specified
column type <b>%s</b> cannot be filtered.', $var, $this->__objs['table'], $type);
return $this->__objs['result'][$var] = XnyoInput::$type($value);
}
/**
* Does the column exist?
**/
public function __isset ($var)
{
return isset($this->__objs['parent']->columns[$var]) || $this->is_relation($var);
}
/**
* Is the variable being accessed a relationship?
**/
private function is_relation ($var)
{
// for niceness
$var = strtolower($var);
// is it a belongs_to reference?
if (isset($this->__objs['parent']->belongs_to))
{
if (is_array($this->__objs['parent']->belongs_to) && in_array($var,
$this->__objs['parent']->belongs_to))
return true;
if (!is_array($this->__objs['parent']->belongs_to) && $this->__objs['parent']->belongs_to ==
$var)
return true;
}
// what about a has_many reference?
if (isset($this->__objs['parent']->has_many))
{
if (is_array($this->__objs['parent']->has_many) && in_array($var,
$this->__objs['parent']->has_many))
return true;
if (!is_array($this->__objs['parent']->has_many) && $this->__objs['parent']->has_many == $var)
return true;
}
// or a has_one reference?
if (isset($this->has_one))
{
if (is_array($this->__objs['parent']->has_one) && in_array($var,
$this->__objs['parent']->has_one))
return true;
if (!is_array($this->__objs['parent']->has_one) && $this->__objs['parent']->has_one == $var)
return true;
}
return false;
}
/**
* Set the column to null
**/
public function __unset ($var)
{
$this->__objs['result'][$var] = null;
}
/**
* Any other method call
**/
public function __call ($func, $args)
{
$func = strtolower($func);
// check to see if the method they're calling is set_<column>
foreach ($this->__objs['parent']->columns as $name => $value)
if ($func == 'set'.$name || $func == 'set_'.$name)
$this->$name = $args[0];
}
}
