Newer
Older
<?php
/*********************************************************************
class.orm.php
Simple ORM (Object Relational Mapper) for PHP5 based on Django's ORM,
except that complex filter operations are not supported. The ORM simply
supports ANDed filter operations without any GROUP BY support.
Jared Hancock <jared@osticket.com>
Copyright (c) 2006-2013 osTicket
http://www.osticket.com
Released under the GNU General Public License WITHOUT ANY WARRANTY.
See LICENSE.TXT for details.
vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
class OrmException extends Exception {}
class OrmConfigurationException extends Exception {}
// Database fields/tables do not match codebase
class InconsistentModelException extends OrmException {
function __construct() {
// Drop the model cache (just incase)
ModelMeta::flushModelCache();
call_user_func_array(array('parent', '__construct'), func_get_args());
}
}
/**
* Meta information about a model including edges (relationships), table
* name, default sorting information, database fields, etc.
*
* This class is constructed and built automatically from the model's
* ::_inspect method using a class's ::$meta array.
*/
class ModelMeta implements ArrayAccess {
static $base = array(
'pk' => false,
'table' => false,
'defer' => array(),
'select_related' => array(),
'joins' => array(),
'foreign_keys' => array(),
function __construct($model) {
// Merge ModelMeta from parent model (if inherited)
$parent = get_parent_class($this->model);
if (is_subclass_of($parent, 'VerySimpleModel')) {
$meta = $parent::getMeta()->extend($model::$meta);
}
else {
$meta = $model::$meta + self::$base;
}
if (!$meta['view']) {
if (!$meta['table'])
throw new OrmConfigurationException(
sprintf(__('%s: Model does not define meta.table'), $this->model));
elseif (!$meta['pk'])
throw new OrmConfigurationException(
sprintf(__('%s: Model does not define meta.pk'), $this->model));
}
// Ensure other supported fields are set and are arrays
foreach (array('pk', 'ordering', 'defer', 'select_related') as $f) {
if (!isset($meta[$f]))
$meta[$f] = array();
elseif (!is_array($meta[$f]))
$meta[$f] = array($meta[$f]);
}
// Break down foreign-key metadata
foreach ($meta['joins'] as $field => &$j) {
if ($j['local'])
$meta['foreign_keys'][$j['local']] = $field;
$this->base = $meta;
}
function extend($meta) {
if ($meta instanceof self)
$meta = $meta->base;
return $meta + $this->base + self::$base;
}
/**
* Adds some more information to a declared relationship. If the
* relationship is a reverse relation, then the information from the
* reverse relation is loaded into the local definition
*
* Compiled-Join-Structure:
* 'constraint' => array(local => array(foreign_field, foreign_class)),
* Constraint used to construct a JOIN in an SQL query
* 'list' => boolean
* TRUE if an InstrumentedList should be employed to fetch a list
* of related items
* 'broker' => Handler for the 'list' property. Usually a subclass of
* 'InstrumentedList'
* 'null' => boolean
* TRUE if relation is nullable
* 'fkey' => array(class, pk)
* Classname and field of the first item in the constraint that
* points to a PK field of a foreign model
* 'local' => string
* The local field corresponding to the 'fkey' property
*/
function processJoin(&$j) {
if (isset($j['reverse'])) {
list($fmodel, $key) = explode('.', $j['reverse']);
// NOTE: It's ok if the forein meta data is not yet inspected.
$info = $fmodel::$meta['joins'][$key];
if (!is_array($info['constraint']))
throw new OrmConfigurationException(sprintf(__(
// `reverse` here is the reverse of an ORM relationship
'%s: Reverse does not specify any constraints'),
$j['reverse']));
foreach ($info['constraint'] as $foreign => $local) {
list($L,$field) = is_array($local) ? $local : explode('.', $local);
$constraint[$field ?: $L] = array($fmodel, $foreign);
}
if (!isset($j['list']))
$j['list'] = true;
if (!isset($j['null']))
// By default, reverse releationships can be empty lists
$j['null'] = true;
else {
foreach ($j['constraint'] as $local => $foreign) {
list($class, $field) = $constraint[$local]
= is_array($foreign) ? $foreign : explode('.', $foreign);
if ($j['list'] && !isset($j['broker'])) {
$j['broker'] = 'InstrumentedList';
}
if ($j['broker'] && !class_exists($j['broker'])) {
throw new OrmException($j['broker'] . ': List broker does not exist');
}
foreach ($constraint as $local => $foreign) {
list($class, $field) = $foreign;
if ($local[0] == "'" || $field[0] == "'" || !class_exists($class))
continue;
function addJoin($name, array $join) {
$this->base['joins'][$name] = $join;
$this->processJoin($this->base['joins'][$name]);
}
function offsetGet($field) {
if (!isset($this->base[$field]))
$this->setupLazy($field);
return $this->base[$field];
}
function offsetSet($field, $what) {
$this->base[$field] = $what;
}
function offsetExists($field) {
return isset($this->base[$field]);
}
function offsetUnset($field) {
throw new Exception('Model MetaData is immutable');
}
function setupLazy($what) {
switch ($what) {
case 'fields':
$this->base['fields'] = self::inspectFields();
break;
case 'newInstance':
$class_repr = sprintf(
'O:%d:"%s":0:{}',
strlen($this->model), $this->model
);
$this->base['newInstance'] = function() use ($class_repr) {
return unserialize($class_repr);
};
break;
default:
throw new Exception($what . ': No such meta-data');
}
}
function inspectFields() {
if (!isset(self::$model_cache))
self::$model_cache = function_exists('apc_fetch');
if (self::$model_cache) {
$key = md5(SECRET_SALT . GIT_VERSION . $this['table']);
if ($fields = apc_fetch($key)) {
return $fields;
}
}
$fields = DbEngine::getCompiler()->inspectTable($this['table']);
apc_store($key, $fields);
}
return $fields;
static function flushModelCache() {
if (self::$model_cache)
@apc_clear_cache('user');
}
class VerySimpleModel {
static $meta = array(
'table' => false,
'ordering' => false,
'pk' => false
);
var $ht;
var $dirty = array();
var $__deferred__ = array();
function __construct($row) {
$this->ht = $row;
}
function get($field, $default=false) {
if (array_key_exists($field, $this->ht))
return $this->ht[$field];
elseif (($joins = static::getMeta('joins')) && isset($joins[$field])) {
$j = $joins[$field];
// Support instrumented lists and such
if (isset($j['list']) && $j['list']) {
$class = $j['fkey'][0];
$fkey = array();
// Localize the foreign key constraint
foreach ($j['constraint'] as $local=>$foreign) {
list($_klas,$F) = $foreign;
$fkey[$F ?: $_klas] = ($local[0] == "'")
? trim($local, "'") : $this->ht[$local];
}
$v = $this->ht[$field] = new $j['broker'](
// Send Model, [Foriegn-Field => Local-Id]
array($class, $fkey)
);
return $v;
}
// Support relationships
elseif (isset($j['fkey'])) {
$criteria = array();
foreach ($j['constraint'] as $local => $foreign) {
if (class_exists($klas))
$class = $klas;
if ($local[0] == "'") {
$criteria[$F] = trim($local,"'");
}
// Does not affect the local model
continue;
}
else {
$criteria[$F] = $this->ht[$local];
}
}
$v = $this->ht[$field] = $class::lookup($criteria);
}
catch (DoesNotExist $e) {
$v = null;
}
elseif (isset($this->__deferred__[$field])) {
// Fetch deferred field
$row = static::objects()->filter($this->getPk())
// XXX: Seems like all the deferred fields should be fetched
->values_flat($field)
->one();
if ($row)
return $this->ht[$field] = $row[0];
}
elseif ($field == 'pk') {
return $this->getPk();
}
if (isset($default))
return $default;
// For new objects, assume the field is NULLable
if ($this->__new__)
return null;
// TODO: Inspect fields from database before throwing this error
throw new OrmException(sprintf(__('%s: %s: Field not defined'),
get_class($this), $field));
}
function __get($field) {
return $this->get($field, null);
}
function __isset($field) {
return array_key_exists($field, $this->ht)
|| isset(static::$meta['joins'][$field]);
function __unset($field) {
if ($this->__isset($field))
unset($this->ht[$field]);
else
unset($this->{$field});
// Update of foreign-key by assignment to model instance
$joins = static::getMeta('joins');
if (isset($joins[$field])) {
$j = $joins[$field];
if ($j['list'] && ($value instanceof InstrumentedList)) {
// Magic list property
$this->ht[$field] = $value;
return;
}
if (in_array($j['local'], static::$meta['pk'])) {
// Reverse relationship — don't null out local PK
return;
}
// Pass. Set local field to NULL in logic below
}
elseif ($value instanceof $j['fkey'][0]) {
// Capture the object under the object's field name
$this->ht[$field] = $value;
if ($value->__new__)
// save() will be performed when saving this object
$value = null;
else
$value = $value->get($j['fkey'][1]);
// Fall through to the standard logic below
}
else
throw new InvalidArgumentException(
sprintf(__('Expecting NULL or instance of %s. Got a %s instead'),
$j['fkey'][0], is_object($value) ? get_class($value) : gettype($value)));
// Capture the foreign key id value
$field = $j['local'];
}
// elseif $field is in a relationship, adjust the relationship
elseif (isset(static::$meta['foreign_keys'][$field])) {
// meta->foreign_keys->{$field} points to the property of the
// foreign object. For instance 'object_id' points to 'object'
$related = static::$meta['foreign_keys'][$field];
}
$old = isset($this->ht[$field]) ? $this->ht[$field] : null;
if ($old != $value) {
// isset should not be used here, because `null` should not be
// replaced in the dirty array
if (!array_key_exists($field, $this->dirty))
$this->dirty[$field] = $old;
if ($related)
// $related points to a foreign object propery. If setting a
// new object_id value, the relationship to object should be
// cleared and rebuilt
unset($this->ht[$related]);
$this->ht[$field] = $value;
}
function __set($field, $value) {
return $this->set($field, $value);
}
function setAll($props) {
foreach ($props as $field=>$value)
$this->set($field, $value);
}
static function __oninspect() {}
static function _inspect() {
static::$meta = new ModelMeta(get_called_class());
// Let the model participate
static::__oninspect();
}
static function getMeta($key=false) {
if (!static::$meta instanceof ModelMeta)
static::_inspect();
$M = static::$meta;
return ($key) ? $M->offsetGet($key) : $M;
/**
* objects
*
* Retrieve a QuerySet for this model class which can be used to fetch
* models from the connected database. Subclasses can override this
* method to apply forced constraints on the QuerySet.
*/
static function objects() {
return new QuerySet(get_called_class());
}
/**
* lookup
*
* Retrieve a record by its primary key. This method may be short
* circuited by model caching if the record has already been loaded by
* the database. In such a case, the database will not be consulted for
* the model's data.
*
* This method can be called with an array of keyword arguments matching
* the PK of the object or the values of the primary key. Both of these
* usages are correct:
*
* >>> User::lookup(1)
* >>> User::lookup(array('id'=>1))
*
* For composite primary keys and the first usage, pass the values in
* the order they are given in the Model's 'pk' declaration in its meta
* data.
*
* Parameters:
* $criteria - (mixed) primary key for the sought model either as
* arguments or key/value array as the function's first argument
*
* Returns:
* (Object<Model>|null) a single instance of the sought model or null if
* no such instance exists.
// Model::lookup(1), where >1< is the pk value
if (!is_array($criteria)) {
$criteria = array();
$pk = static::getMeta('pk');
// Only consult cache for PK lookup, which is assumed if the
// values are passed as args rather than an array
if ($cached = ModelInstanceManager::checkCache(get_called_class(),
$criteria))
return $cached;
try {
return static::objects()->filter($criteria)->one();
}
catch (DoesNotExist $e) {
return null;
}
$ex = DbEngine::delete($this);
try {
$ex->execute();
if ($ex->affected_rows() != 1)
return false;
Signal::send('model.deleted', $this);
}
catch (OrmException $e) {
return false;
}
if ($this->__deleted__)
throw new OrmException('Trying to update a deleted object');
$pk = static::getMeta('pk');
$wasnew = $this->__new__;
// First, if any foreign properties of this object are connected to
// another *new* object, then save those objects first and set the
// local foreign key field values
foreach (static::getMeta('joins') as $prop => $j) {
if (isset($this->ht[$prop])
&& ($foreign = $this->ht[$prop])
&& $foreign instanceof VerySimpleModel
&& !in_array($j['local'], $pk)
&& null === $this->get($j['local'])
) {
if ($foreign->__new__ && !$foreign->save())
return false;
$this->set($j['local'], $foreign->get($j['fkey'][1]));
}
}
// If there's nothing in the model to be saved, then we're done
$ex = DbEngine::save($this);
try {
$ex->execute();
if ($ex->affected_rows() != 1) {
// This doesn't really signify an error. It just means that
// the database believes that the row did not change. For
// inserts though, it's a deal breaker
if ($this->__new__)
return false;
else
// No need to reload the record if requested — the
// database didn't update anything
$refetch = false;
}
catch (OrmException $e) {
return false;
// XXX: Ensure AUTO_INCREMENT is set for the field
$this->ht[$pk[0]] = $ex->insert_id();
Signal::send('model.created', $this);
}
else {
$data = array('dirty' => $this->dirty);
Signal::send('model.updated', $this, $data);
}
# Refetch row from database
if ($refetch) {
// Preserve non database information such as list relationships
// across the refetch
static::objects()->filter($this->getPk())->values()->one()
+ $this->ht;
}
if ($wasnew) {
// Attempt to update foreign, unsaved objects with the PK of
// this newly created object
foreach (static::getMeta('joins') as $prop => $j) {
if (isset($this->ht[$prop])
&& ($foreign = $this->ht[$prop])
&& in_array($j['local'], $pk)
) {
if ($foreign instanceof VerySimpleModel
&& null === $foreign->get($j['fkey'][1])
) {
$foreign->set($j['fkey'][1], $this->get($j['local']));
}
elseif ($foreign instanceof InstrumentedList) {
foreach ($foreign as $item) {
if (null === $item->get($j['fkey'][1]))
$item->set($j['fkey'][1], $this->get($j['local']));
}
}
}
}
}
static function create($ht=false) {
if (!$ht) $ht=array();
$class = get_called_class();
$i = new $class(array());
$i->__new__ = true;
foreach ($ht as $field=>$value)
if (!is_array($value))
$i->set($field, $value);
return $i;
}
private function getPk() {
$pk = array();
foreach ($this::getMeta('pk') as $f)
$pk[$f] = $this->ht[$f];
return $pk;
}
/**
* AnnotatedModel
*
* Simple wrapper class which allows wrapping and write-protecting of
* annotated fields retrieved from the database. Instances of this class
* will delegate most all of the heavy lifting to the wrapped Model instance.
*/
class AnnotatedModel {
var $model;
var $annotations;
function __construct($model, $annotations) {
$this->model = $model;
$this->annotations = $annotations;
}
function __get($what) {
return $this->get($what);
}
function get($what) {
if (isset($this->annotations[$what]))
return $this->annotations[$what];
return $this->model->get($what, null);
}
function __set($what, $to) {
return $this->set($what, $to);
}
function set($what, $to) {
if (isset($this->annotations[$what]))
throw new OrmException('Annotated fields are read-only');
return $this->model->set($what, $to);
}
function __isset($what) {
return isset($this->annotations[$what]) || $this->model->__isset($what);
}
// Delegate everything else to the model
function __call($what, $how) {
return call_user_func_array(array($this->model, $what), $how);
}
}
function SqlFunction($name) {
$this->func = $name;
$this->args = array_slice(func_get_args(), 1);
}
function input($what, $compiler, $model) {
if ($what instanceof SqlFunction)
$A = $what->toSql($compiler, $model);
elseif ($what instanceof Q)
$A = $compiler->compileQ($what, $model);
else
$A = $compiler->input($what);
return $A;
}
function toSql($compiler, $model=false, $alias=false) {
$args = array();
foreach ($this->args as $A) {
$args[] = $this->input($A, $compiler, $model);
}
return sprintf('%s(%s)%s', $this->func, implode(',', $args),
$alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
}
function getAlias() {
return $this->alias;
}
function setAlias($alias) {
$this->alias = $alias;
static function __callStatic($func, $args) {
$I = new static($func);
$I->args = $args;
return $I;
}
class SqlCase extends SqlFunction {
var $cases = array();
var $else = false;
static function N() {
return new static('CASE');
}
function when($expr, $result) {
if (is_array($expr))
$expr = new Q($expr);
$this->cases[] = array($expr, $result);
return $this;
}
function otherwise($result) {
$this->else = $result;
return $this;
}
function toSql($compiler, $model=false, $alias=false) {
$cases = array();
foreach ($this->cases as $A) {
list($expr, $result) = $A;
$expr = $this->input($expr, $compiler, $model);
$result = $this->input($result, $compiler, $model);
$cases[] = "WHEN {$expr} THEN {$result}";
}
if ($this->else) {
$else = $this->input($this->else, $compiler, $model);
$cases[] = "ELSE {$else}";
}
return sprintf('CASE %s END%s', implode(' ', $cases),
$alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
}
}
class SqlExpr extends SqlFunction {
function __construct($args) {
$this->args = $args;
}
function toSql($compiler, $model=false, $alias=false) {
$O = array();
foreach ($this->args as $field=>$value) {
list($field, $op) = $compiler->getField($field, $model);
if (is_callable($op))
$O[] = call_user_func($op, $field, $value, $model);
else
$O[] = sprintf($op, $field, $compiler->input($value));
}
return implode(' ', $O) . ($alias ? ' AS ' . $alias : '');
}
}
class SqlExpression extends SqlFunction {
var $operator;
var $operands;
function toSql($compiler, $model=false, $alias=false) {
$O = array();
foreach ($this->args as $operand) {
if ($operand instanceof SqlFunction)
$O[] = $operand->toSql($compiler, $model);
else
$O[] = $compiler->input($operand);
}
return implode(' '.$this->func.' ', $O)
. ($alias ? ' AS '.$compiler->quote($alias) : '');
}
static function __callStatic($operator, $operands) {
switch ($operator) {
case 'minus':
$operator = '-'; break;
case 'plus':
$operator = '+'; break;
case 'times':
$operator = '*'; break;
case 'bitand':
$operator = '&'; break;
case 'bitor':
$operator = '|'; break;
default:
throw new InvalidArgumentException('Invalid operator specified');
}
return parent::__callStatic($operator, $operands);
}
}
class SqlInterval extends SqlFunction {
var $type;
function toSql($compiler, $model=false, $alias=false) {
$A = $this->args[0];
if ($A instanceof SqlFunction)
$A = $A->toSql($compiler, $model);
else
$A = $compiler->input($A);
return sprintf('INTERVAL %s %s',
$A,
$this->func)
. ($alias ? ' AS '.$compiler->quote($alias) : '');
}
static function __callStatic($interval, $args) {
if (count($args) != 1) {
throw new InvalidArgumentException("Interval expects a single interval value");
}
return parent::__callStatic($interval, $args);
}
}
class SqlField extends SqlFunction {
function __construct($field) {
$this->field = $field;
}
function toSql($compiler, $model=false, $alias=false) {
list($field) = $compiler->getField($this->field, $model);
return $field;
}
}
class SqlCode extends SqlFunction {
function __construct($code) {
$this->code = $code;
}
function toSql($compiler, $model=false, $alias=false) {
return $this->code;
}
}
class SqlAggregate extends SqlFunction {
var $func;
var $expr;
var $distinct=false;
var $constraint=false;
function __construct($func, $expr, $distinct=false, $constraint=false) {
$this->func = $func;
$this->expr = $expr;
$this->distinct = $distinct;
if ($constraint instanceof Q)
$this->constraint = $constraint;
elseif ($constraint)
$this->constraint = new Q($constraint);
}
static function __callStatic($func, $args) {
$distinct = @$args[1] ?: false;
$constraint = @$args[2] ?: false;
return new static($func, $args[0], $distinct, $constraint);
}
function toSql($compiler, $model=false, $alias=false) {
$options = array('constraint' => $this->constraint, 'model' => true);
// For DISTINCT, require a field specification — not a relationship
// specification.
$E = $this->expr;
if ($E instanceof SqlFunction) {
$field = $E->toSql($compiler, $model, $alias);
}
else {
list($field, $rmodel) = $compiler->getField($E, $model, $options);
if ($this->distinct) {
$pk = false;
$fpk = $rmodel::getMeta('pk');
foreach ($fpk as $f) {
$pk |= false !== strpos($field, $f);
}
if (!$pk) {
// Try and use the foriegn primary key
list($field) = $compiler->getField(
$this->expr . '__' . $fpk[0],
$model, $options);
}
else {
throw new OrmException(
sprintf('%s :: %s', $rmodel, $field) .
': DISTINCT aggregate expressions require specification of a single primary key field of the remote model'
);
}
}
}
return sprintf('%s(%s%s)%s', $this->func,
$this->distinct ? 'DISTINCT ' : '', $field,
$alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
}
function getFieldName() {
return strtolower(sprintf('%s__%s', $this->args[0], $this->func));
}
}
class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countable {
var $model;
var $constraints = array();
var $ordering = array();
var $limit = false;
var $offset = 0;
var $related = array();
var $values = array();
var $defer = array();
var $extra = array();
var $distinct = array();
var $lock = false;
const LOCK_EXCLUSIVE = 1;
const LOCK_SHARED = 2;
function __construct($model) {
$this->model = $model;
}
function filter() {
// Multiple arrays passes means OR
foreach (func_get_args() as $Q) {
$this->constraints[] = $Q instanceof Q ? $Q : new Q($Q);
return $this;
}
function exclude() {
foreach (func_get_args() as $Q) {
$this->constraints[] = $Q instanceof Q ? $Q->negate() : Q::not($Q);
function defer() {
foreach (func_get_args() as $f)
$this->defer[$f] = true;
return $this;
}
function order_by($order) {
$this->ordering = array_merge($this->ordering,
is_array($order) ? $order : func_get_args());
function getSortFields() {
$ordering = $this->ordering;
if ($this->extra['order_by'])
$ordering = array_merge($ordering, $this->extra['order_by']);
return $ordering;
}
function lock($how=false) {
$this->lock = $how ?: self::LOCK_EXCLUSIVE;
return $this;
}
function limit($count) {
$this->limit = $count;
return $this;
}
function offset($at) {
$this->offset = $at;
return $this;
}
function isWindowed() {
return $this->limit || $this->offset;
}
function select_related() {
$this->related = array_merge($this->related, func_get_args());
return $this;
}
function extra(array $extra) {
foreach ($extra as $section=>$info) {
$this->extra[$section] = array_merge($this->extra[$section] ?: array(), $info);
}
return $this;
}
function distinct() {
foreach (func_get_args() as $D)
$this->distinct[] = $D;
return $this;
}
function models() {
$this->iterator = 'ModelInstanceManager';
$this->values = $this->related = array();
return $this;
}
foreach (func_get_args() as $A)
$this->values[$A] = $A;
// This disables related models
$this->related = false;
function values_flat() {
$this->values = func_get_args();
$this->iterator = 'FlatArrayIterator';
// This disables related models
$this->related = false;
function all() {
return $this->getIterator()->asArray();
}
$list = $this->limit(1)->all();
/**
* one
*
* Finds and returns a single model instance based on the criteria in
* this QuerySet instance.
*
* Throws:
* DoesNotExist - if no such model exists with the given criteria
* ObjectNotUnique - if more than one model matches the given criteria
*
* Returns:
* (Object<Model>) a single instance of the sought model is guarenteed.
* If no such model or multiple models exist, an exception is thrown.
*/
function one() {
$list = $this->all();
if (count($list) == 0)
throw new DoesNotExist();
elseif (count($list) > 1)
throw new ObjectNotUnique('One object was expected; however '
.'multiple objects in the database matched the query. '
.sprintf('In fact, there are %d matching objects.', count($list))
);
// TODO: Throw error if more than one result from database
// Defer to the iterator if fetching already started
if (isset($this->_iterator)) {
return $this->_iterator->count();
}
elseif (isset($this->_count)) {
return $this->_count;
}
$class = $this->compiler;
$compiler = new $class();
return $this->_count = $compiler->compileCount($this);
function toSql($compiler, $model, $alias) {
// FIXME: Force root model of the compiler to $model
$exec = $this->getQuery(array('compiler' => get_class($compiler)));
foreach ($exec->params as $P)
$compiler->params[] = $P;
return "({$exec})".($alias ? " AS {$alias}" : '');
}
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
/**
* exists
*
* Determines if there are any rows in this QuerySet. This can be
* achieved either by evaluating a SELECT COUNT(*) query or by
* attempting to fetch the first row from the recordset and return
* boolean success.
*
* Parameters:
* $fetch - (bool) TRUE if a compile and fetch should be attempted
* instead of a SELECT COUNT(*). This would be recommended if an
* accurate count is not required and the records would be fetched
* if this method returns TRUE.
*
* Returns:
* (bool) TRUE if there would be at least one record in this QuerySet
*/
function exists($fetch=false) {
if ($fetch) {
return (bool) $this[0];
}
return $this->count() > 0;
}
function annotate($annotations) {
if (!is_array($annotations))
$annotations = func_get_args();
foreach ($annotations as $name=>$A) {
if (is_int($name))
$name = $A->getFieldName();
$A->setAlias($name);
}
function aggregate($annotations) {
// Aggregate works like annotate, except that it sets up values
// fetching which will disable model creation
$this->annotate($annotations);
// Disable other fields from being fetched
$this->aggregated = true;
$this->related = false;
return $this;
}
function delete() {
$class = $this->compiler;
$compiler = new $class();
// XXX: Mark all in-memory cached objects as deleted
$ex = $compiler->compileBulkDelete($this);
$ex->execute();
return $ex->affected_rows();
}
function update(array $what) {
$class = $this->compiler;
$compiler = new $class;
$ex = $compiler->compileBulkUpdate($this, $what);
$ex->execute();
return $ex->affected_rows();
}
function __clone() {
unset($this->_iterator);
unset($this->query);
}
// IteratorAggregate interface
function getIterator() {
$class = $this->iterator;
$this->_iterator = new $class($this);
return $this->_iterator;
}
// ArrayAccess interface
function offsetExists($offset) {
return $this->getIterator()->offsetExists($offset);
}
function offsetGet($offset) {
return $this->getIterator()->offsetGet($offset);
}
function offsetUnset($a) {
throw new Exception(__('QuerySet is read-only'));
throw new Exception(__('QuerySet is read-only'));
return (string) $this->getQuery();
function getQuery($options=array()) {
if (isset($this->query))
return $this->query;
// Load defaults from model
$model = $this->model;
if (!$options['nosort'] && !$query->ordering && $model::getMeta('ordering'))
$query->ordering = $model::getMeta('ordering');
if (false !== $query->related && !$query->values && $model::getMeta('select_related'))
$query->related = $model::getMeta('select_related');
if (!$query->defer && $model::getMeta('defer'))
$query->defer = $model::getMeta('defer');
$class = $options['compiler'] ?: $this->compiler;
$compiler = new $class($options);
$this->query = $compiler->compileSelect($query);
/**
* Fetch a model class which can be used to render the QuerySet as a
* subquery to be used as a JOIN.
*/
function asView() {
$unique = spl_object_hash($this);
$classname = "QueryView{$unique}";
$class = <<<EOF
class {$classname} extends VerySimpleModel {
static \$meta = array(
'view' => true,
);
static \$queryset;
static function getQuery(\$compiler) {
return ' ('.static::\$queryset->getQuery().') ';
}
}
EOF;
eval($class); // Ugh
$classname::$queryset = $this;
return $classname;
}
function serialize() {
$info = get_object_vars($this);
unset($info['query']);
unset($info['limit']);
unset($info['offset']);
unset($info['_iterator']);
return serialize($info);
}
function unserialize($data) {
$data = unserialize($data);
foreach ($data as $name => $val) {
$this->{$name} = $val;
}
}
class DoesNotExist extends Exception {}
class ObjectNotUnique extends Exception {}
abstract class ResultSet implements Iterator, ArrayAccess, Countable {
var $resource;
var $position = 0;
var $queryset;
function __construct($queryset=false) {
$this->queryset = $queryset;
if ($queryset) {
$this->model = $queryset->model;
function prime() {
if (!isset($this->resource) && $this->queryset)
$this->resource = $this->queryset->getQuery();
}
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
abstract function fillTo($index);
function asArray() {
$this->fillTo(PHP_INT_MAX);
return $this->cache;
}
// Iterator interface
function rewind() {
$this->position = 0;
}
function current() {
$this->fillTo($this->position);
return $this->cache[$this->position];
}
function key() {
return $this->position;
}
function next() {
$this->position++;
}
function valid() {
$this->fillTo($this->position);
return count($this->cache) > $this->position;
}
// ArrayAccess interface
function offsetExists($offset) {
$this->fillTo($offset);
return $this->position >= $offset;
}
function offsetGet($offset) {
$this->fillTo($offset);
return $this->cache[$offset];
}
function offsetUnset($a) {
throw new Exception(sprintf(__('%s is read-only'), get_class($this)));
}
function offsetSet($a, $b) {
throw new Exception(sprintf(__('%s is read-only'), get_class($this)));
}
// Countable interface
function count() {
return count($this->asArray());
}
}
class ModelInstanceManager extends ResultSet {
var $model;
var $map;
static $objectCache = array();
function cache($model) {
$key = sprintf('%s.%s',
$model::$meta->model, implode('.', $model->get('pk')));
self::$objectCache[$key] = $model;
}
/**
* uncache
*
* Drop the cached reference to the model. If the model is deleted
* database-side. Lookups for the same model should not be short
* circuited to retrieve the cached reference.
*/
static function uncache($model) {
$key = sprintf('%s.%s',
$model::$meta->model, implode('.', $model->pk));
unset(self::$objectCache[$key]);
}
static function checkCache($modelClass, $fields) {
$key = $modelClass::$meta->model;
foreach ($modelClass::getMeta('pk') as $f)
$key .= '.'.$fields[$f];
return @self::$objectCache[$key];
}
/**
* getOrBuild
*
* Builds a new model from the received fields or returns the model
* already stashed in the model cache. Caching helps to ensure that
* multiple lookups for the same model identified by primary key will
* fetch the exact same model. Therefore, changes made to the model
* anywhere in the project will be reflected everywhere.
*
* For annotated models (models build from querysets with annotations),
* the built or cached model is wrapped in an AnnotatedModel instance.
* The annotated fields are in the AnnotatedModel instance and the
* database-backed fields are managed by the Model instance.
*/
function getOrBuild($modelClass, $fields, $cache=true) {
// Check for NULL primary key, used with related model fetching. If
// the PK is NULL, then consider the object to also be NULL
foreach ($modelClass::getMeta('pk') as $pkf) {
if (!isset($fields[$pkf])) {
return null;
}
}
$annotations = $this->queryset->annotations;
$extras = array();
// For annotations, drop them from the $fields list and add them to
// an $extras list. The fields passed to the root model should only
// be the root model's fields. The annotated fields will be wrapped
// using an AnnotatedModel instance.
if ($annotations && $modelClass == $this->model) {
if (isset($fields[$name])) {
$extras[$name] = $fields[$name];
unset($fields[$name]);
}
}
}
// Check the cache for the model instance first
if (!($m = self::checkCache($modelClass, $fields))) {
// Construct and cache the object
$m = new $modelClass($fields);
// XXX: defer may refer to fields not in this model
$m->__deferred__ = $this->queryset->defer;
$m->__onload();
if ($cache)
$this->cache($m);
}
elseif (get_class($m) != $modelClass) {
// Change the class of the object to be returned to match what
// was expected
// TODO: Emit a warning?
$m = new $modelClass($m->ht);
}
// Wrap annotations in an AnnotatedModel
if ($extras) {
$m = new AnnotatedModel($m, $extras);
// TODO: If the model has deferred fields which are in $fields,
// those can be resolved here
Loading
Loading full blame...