Skip to content
Snippets Groups Projects
class.orm.php 71.5 KiB
Newer Older
Jared Hancock's avatar
Jared Hancock committed
<?php
/*********************************************************************
    class.orm.php

    Simple ORM (Object Relational Mapper) for PHP5 based on Django's ORM,
Jared Hancock's avatar
Jared Hancock committed
    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 {}
/**
 * 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(),
        'view' => false,
    var $model;

    function __construct($model) {
        $this->model = $model;
        $meta = $model::$meta + self::$base;
        // TODO: Merge ModelMeta from parent model (if inherited)

        if (!$meta['table'])
            throw new OrmConfigurationException(
                __('Model does not define meta.table'), $model);
        elseif (!$meta['pk'])
            throw new OrmConfigurationException(
                __('Model does not define meta.pk'), $model);

        // Ensure other supported fields are set and are arrays
        foreach (array('pk', 'ordering', 'defer') as $f) {
            if (!isset($meta[$f]))
                $meta[$f] = array();
            elseif (!is_array($meta[$f]))
                $meta[$f] = array($meta[$f]);
        }

        // Break down foreign-key metadata
        if (!isset($meta['joins']))
            $meta['joins'] = array();
        foreach ($meta['joins'] as $field => &$j) {
            $this->processJoin($j);
        unset($j);
    function processJoin(&$j) {
        if (isset($j['reverse'])) {
            list($fmodel, $key) = explode('.', $j['reverse']);
            $info = $fmodel::$meta['joins'][$key];
            $constraint = array();
            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(,$field) = explode('.', $local);
                $constraint[$field] = "$fmodel.$foreign";
            }
            $j['constraint'] = $constraint;
            if (!isset($j['list']))
                $j['list'] = true;
            if (!isset($j['null']))
                $j['null'] = $info['null'] ?: false;
        }
        // XXX: Make this better (ie. composite keys)
        $keys = array_keys($j['constraint']);
        $foreign = $j['constraint'][$keys[0]];
        $j['fkey'] = explode('.', $foreign);
        $j['local'] = $keys[0];
    }

    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() {
        return DbEngine::getCompiler()->inspectTable($this['table']);
    }
}

Jared Hancock's avatar
Jared Hancock committed
class VerySimpleModel {
    static $meta = array(
        'table' => false,
        'ordering' => false,
        'pk' => false
    );

    var $ht;
Jared Hancock's avatar
Jared Hancock committed
    var $__new__ = false;
    var $__deleted__ = false;
    var $__deferred__ = array();
Jared Hancock's avatar
Jared Hancock committed

    function __construct($row) {
        $this->ht = $row;
    }

    function get($field, $default=false) {
Jared Hancock's avatar
Jared Hancock committed
        if (array_key_exists($field, $this->ht))
            return $this->ht[$field];
        elseif (isset(static::$meta['joins'][$field])) {
            // Make sure joins were inspected
            if (!static::$meta instanceof ModelMeta)
                static::_inspect();
            $j = static::$meta['joins'][$field];
            // Support instrumented lists and such
            if (isset($this->ht[$j['local']])
                    && isset($j['list']) && $j['list']) {
                $fkey = $j['fkey'];
                $v = $this->ht[$field] = new InstrumentedList(
                    // Send Model, Foriegn-Field, Local-Id
                    array($fkey[0], $fkey[1], $this->get($j['local']))
                );
                return $v;
            }
            // Support relationships
            elseif (isset($j['fkey'])
                    && ($class = $j['fkey'][0])
                    && class_exists($class)) {
                try {
                    $v = $this->ht[$field] = $class::lookup(
                        array($j['fkey'][1] => $this->ht[$j['local']]));
                }
                catch (DoesNotExist $e) {
                    $v = null;
                }
                return $v;
            }
        elseif (isset($this->__deferred__[$field])) {
            // Fetch deferred field
            $row = static::objects()->filter($this->getPk())
                ->values_flat($field)
                ->one();
            if ($row)
                return $this->ht[$field] = $row[0];
        }
        elseif ($field == 'pk') {
            return $this->getPk();
        }

        if (isset($default))
            return $default;
        // 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) {
        unset($this->ht[$field]);
    }
Jared Hancock's avatar
Jared Hancock committed

    function set($field, $value) {
        // Update of foreign-key by assignment to model instance
        if (isset(static::$meta['joins'][$field])) {
            static::_inspect();
            $j = static::$meta['joins'][$field];
            if ($j['list'] && ($value instanceof InstrumentedList)) {
                // Magic list property
                $this->ht[$field] = $value;
                return;
            }
            if ($value === null) {
                if (in_array($j['local'], static::$meta['pk'])) {
                    // Reverse relationship — don't null out local PK
                    $this->ht[$field] = $value;
                    return;
                }
                // Pass. Set local field to NULL in logic below
            }
            elseif ($value instanceof $j['fkey'][0]) {
                if ($value->__new__)
                    $value->save();
                // Capture the object under the object's field name
                $this->ht[$field] = $value;
                $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], get_class($value)));
            // Capture the foreign key id value
            $field = $j['local'];
        }
Jared Hancock's avatar
Jared Hancock committed
        $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;
Jared Hancock's avatar
Jared Hancock committed
            $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);
    }

    function __onload() {}
    static function __oninspect() {}
    static function _inspect() {
        if (!static::$meta instanceof ModelMeta) {
            static::$meta = new ModelMeta(get_called_class());
            // Let the model participate
            static::__oninspect();
    /**
     * 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.
     */
Jared Hancock's avatar
Jared Hancock committed
    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.
Jared Hancock's avatar
Jared Hancock committed
    static function lookup($criteria) {
        // Model::lookup(1), where >1< is the pk value
        if (!is_array($criteria)) {
            $criteria = array();
            foreach (func_get_args() as $i=>$f)
                $criteria[static::$meta['pk'][$i]] = $f;
        }
        if ($cached = ModelInstanceManager::checkCache(get_called_class(),
                $criteria))
            return $cached;
        try {
            return static::objects()->filter($criteria)->one();
        }
        catch (DoesNotExist $e) {
            return null;
        }
Jared Hancock's avatar
Jared Hancock committed
    }

    function delete($pk=false) {
        $ex = DbEngine::delete($this);
        try {
            $ex->execute();
            if ($ex->affected_rows() != 1)
                return false;
            $this->__deleted__ = true;
            Signal::send('model.deleted', $this);
        }
        catch (OrmException $e) {
            return false;
        }
Jared Hancock's avatar
Jared Hancock committed
    }

    function save($refetch=false) {
        if (count($this->dirty) === 0)
            return true;
        elseif ($this->__deleted__)
            throw new OrmException('Trying to update a deleted object');

        $ex = DbEngine::save($this);
        try {
            $ex->execute();
            if ($ex->affected_rows() != 1)
                return false;
        catch (OrmException $e) {
            return false;

        $pk = static::$meta['pk'];
        $wasnew = $this->__new__;
Jared Hancock's avatar
Jared Hancock committed
        if ($this->__new__) {
            if (count($pk) == 1)
                // XXX: Ensure AUTO_INCREMENT is set for the field
                $this->ht[$pk[0]] = $ex->insert_id();
Jared Hancock's avatar
Jared Hancock committed
            $this->__new__ = false;
            Signal::send('model.created', $this);
        }
        else {
            $data = array('dirty' => $this->dirty);
            Signal::send('model.updated', $this, $data);
Jared Hancock's avatar
Jared Hancock committed
        }
        # Refetch row from database
        # XXX: Too much voodoo
        if ($refetch) {
            // Uncache so that the lookup will not be short-cirtuited to
            // return this object
            ModelInstanceManager::uncache($this);
            $self = static::lookup($this->get('pk'));
Jared Hancock's avatar
Jared Hancock committed
            $this->ht = $self->ht;
        }
        if ($wasnew)
            $this->__onload();
Jared Hancock's avatar
Jared Hancock committed
        $this->dirty = array();
        return $this->get($pk[0]);
    }

    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::$meta['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);
    }

    // Delegate everything else to the model
    function __call($what, $how) {
        return call_user_func_array(array($this->model, $what), $how);
    }
    static function __callStatic($what, $how) {
        return call_user_func_array(array($this->model, $what), $how);
    }
}

Jared Hancock's avatar
Jared Hancock committed
class SqlFunction {
Jared Hancock's avatar
Jared Hancock committed
    function SqlFunction($name) {
        $this->func = $name;
        $this->args = array_slice(func_get_args(), 1);
    }

    function toSql($compiler, $model=false, $alias=false) {
        $args = array();
        foreach ($this->args as $A) {
            if ($A instanceof SqlFunction)
                $A = $A->toSql($compiler, $model);
            else
                $A = $compiler->input($A);
            $args[] = $A;
        }
        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 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);
        list($field) = $compiler->getField($this->expr, $model, $options);
        return sprintf('%s(%s)%s', $this->func, $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 {
Jared Hancock's avatar
Jared Hancock committed
    var $model;

    var $constraints = array();
    var $ordering = array();
    var $limit = false;
    var $offset = 0;
    var $related = array();
    var $values = array();
    var $annotations = array();
    var $extra = array();
    var $distinct = array();
    var $lock = false;

    const LOCK_EXCLUSIVE = 1;
    const LOCK_SHARED = 2;
Jared Hancock's avatar
Jared Hancock committed

    var $compiler = 'MySqlCompiler';
    var $iterator = 'ModelInstanceManager';
Jared Hancock's avatar
Jared Hancock committed

    var $params;
    var $query;

    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);
Jared Hancock's avatar
Jared Hancock committed
        return $this;
    }

    function exclude() {
        foreach (func_get_args() as $Q) {
            $this->constraints[] = $Q instanceof Q ? $Q->negate() : Q::not($Q);
Jared Hancock's avatar
Jared Hancock committed
        return $this;
    }

    function defer() {
        foreach (func_get_args() as $f)
            $this->defer[$f] = true;
        return $this;
    }

Jared Hancock's avatar
Jared Hancock committed
    function order_by() {
        $this->ordering = array_merge($this->ordering, func_get_args());
        return $this;
    }
    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;
    }

Jared Hancock's avatar
Jared Hancock committed
    function limit($count) {
        $this->limit = $count;
        return $this;
    }

    function offset($at) {
        $this->offset = $at;
        return $this;
    }

    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;
    }

Jared Hancock's avatar
Jared Hancock committed
    function values() {
        foreach (func_get_args() as $A)
            $this->values[$A] = $A;
Jared Hancock's avatar
Jared Hancock committed
        $this->iterator = 'HashArrayIterator';
        // This disables related models
        $this->related = false;
Jared Hancock's avatar
Jared Hancock committed
        return $this;
    }

    function values_flat() {
        $this->values = func_get_args();
        $this->iterator = 'FlatArrayIterator';
        // This disables related models
        $this->related = false;
Jared Hancock's avatar
Jared Hancock committed
    function all() {
        return $this->getIterator()->asArray();
    }

    function first() {
        $list = $this->limit(1)->all();
        return $list[0];
    }

    /**
     * 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
        return $list[0];
Jared Hancock's avatar
Jared Hancock committed
    function count() {
        $class = $this->compiler;
        $compiler = new $class();
Jared Hancock's avatar
Jared Hancock committed
        return $compiler->compileCount($this);
    }

    /**
     * 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 ($A instanceof Aggregate) {
                if (is_int($name))
                    $name = $A->getFieldName();
                $A->setAlias($name);
                $this->annotations[$name] = $A;
            }
        }
        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);
    }

Jared Hancock's avatar
Jared Hancock committed
    // IteratorAggregate interface
    function getIterator() {
        $class = $this->iterator;
Jared Hancock's avatar
Jared Hancock committed
        if (!isset($this->_iterator))
            $this->_iterator = new $class($this);
Jared Hancock's avatar
Jared Hancock committed
        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'));
Jared Hancock's avatar
Jared Hancock committed
    }
    function offsetSet($a, $b) {
        throw new Exception(__('QuerySet is read-only'));
Jared Hancock's avatar
Jared Hancock committed
    }

    function __toString() {
        return (string) $this->getQuery();
    function getQuery($options=array()) {
Jared Hancock's avatar
Jared Hancock committed
        if (isset($this->query))
            return $this->query;

        // Load defaults from model
        $model = $this->model;
        $query = clone $this;
        if (!$query->ordering && isset($model::$meta['ordering']))
            $query->ordering = $model::$meta['ordering'];
        if (!$query->related && $model::$meta['select_related'])
            $query->related = $model::$meta['select_related'];
        if (!$query->defer && $model::$meta['defer'])
            $query->defer = $model::$meta['defer'];
        $class = $this->compiler;
        $compiler = new $class($options);
        $this->query = $compiler->compileSelect($query);
Jared Hancock's avatar
Jared Hancock committed

        return $this->query;
    }

    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 {
Jared Hancock's avatar
Jared Hancock committed
    var $resource;
    var $position = 0;
    var $queryset;
    var $cache = array();
    function __construct($queryset=false) {
        if ($queryset) {
            $this->model = $queryset->model;
            $this->resource = $queryset->getQuery();
        }
    }

    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)));
    }
}

class ModelInstanceManager extends ResultSet {
    var $model;
    var $map;

    static $objectCache = array();

    function __construct($queryset=false) {
        parent::__construct($queryset);
        if ($queryset) {
            $this->map = $this->resource->getMap();
    function cache($model) {
        $model::_inspect();
        $key = sprintf('%s.%s',
            $model::$meta->model, implode('.', $model->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::$meta['pk'] as $f)
            $key .= '.'.$fields[$f];
        return @self::$objectCache[$key];
    }