Skip to content
Snippets Groups Projects
class.orm.php 117 KiB
Newer Older
  • Learn to ignore specific revisions
  •     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 SqlExpression {
    
        var $level;
    
        function __construct($field, $level=0) {
    
            $this->field = $field;
    
            $this->level = $level;
    
        }
    
        function toSql($compiler, $model=false, $alias=false) {
    
            $L = $this->level;
            while ($L--)
                $compiler = $compiler->getParent();
    
            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.($alias ? ' AS '.$alias : '');
    
    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);
    
            }
            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
    
                    if (count($fpk) == 1) {
    
                        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 {
    
    Jared Hancock's avatar
    Jared Hancock committed
        var $model;
    
        var $constraints = array();
    
        var $path_constraints = array();
    
    Jared Hancock's avatar
    Jared Hancock committed
        var $ordering = array();
        var $limit = false;
        var $offset = 0;
        var $related = array();
        var $values = array();
    
        var $aggregated = false;
    
        var $annotations = array();
    
        var $extra = array();
        var $distinct = array();
    
        var $lock = false;
    
        var $chain = array();
    
    
        const LOCK_EXCLUSIVE = 1;
        const LOCK_SHARED = 2;
    
        const ASC = 'ASC';
        const DESC = 'DESC';
    
    
        const OPT_NOSORT    = 'nosort';
        const OPT_NOCACHE   = 'nocache';
    
        const OPT_MYSQL_FOUND_ROWS = 'found_rows';
    
    
        const ITER_MODELS   = 1;
        const ITER_HASH     = 2;
        const ITER_ROW      = 3;
    
        var $iter = self::ITER_MODELS;
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        var $compiler = 'MySqlCompiler';
    
        var $query;
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        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;
        }
    
    
        /**
         * Add a path constraint for the query. This is different from ::filter
         * in that the constraint is added to a join clause which is normally
         * built from the model meta data. The ::filter() method on the other
         * hand adds the constraint to the where clause. This is generally useful
         * for aggregate queries and left join queries where multiple rows might
         * match a filter in the where clause and would produce incorrect results.
         *
         * Example:
         * Find users with personal email hosted with gmail.
         * >>> $Q = User::objects();
         * >>> $Q->constrain(['user__emails' => new Q(['type' => 'personal']))
         * >>> $Q->filter(['user__emails__address__contains' => '@gmail.com'])
         */
    
        function constrain() {
            foreach (func_get_args() as $I) {
                foreach ($I as $path => $Q) {
    
                    if (!is_array($Q) && !$Q instanceof Q) {
                        // ->constrain(array('field__path__op' => val));
                        $Q = array($path => $Q);
                        list(, $path) = SqlCompiler::splitCriteria($path);
                        $path = implode('__', $path);
                    }
                    $this->path_constraints[$path][] = $Q instanceof Q ? $Q : Q::all($Q);
    
        function defer() {
            foreach (func_get_args() as $f)
                $this->defer[$f] = true;
            return $this;
        }
    
        function order_by($order, $direction=false) {
    
            if ($order === false)
                return $this->options(array('nosort' => true));
    
    
            $args = func_get_args();
            if (in_array($direction, array(self::ASC, self::DESC))) {
                $args = array($args[0]);
            }
            else
                $direction = false;
    
            $new = is_array($order) ?  $order : $args;
            if ($direction) {
                foreach ($new as $i=>$x) {
                    $new[$i] = array($x, $direction);
                }
            }
            $this->ordering = array_merge($this->ordering, $new);
    
    Jared Hancock's avatar
    Jared Hancock committed
            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;
        }
    
    
            return $this->limit || $this->offset || (count($this->values) + count($this->annotations) + @count($this->extra['select'])) > 1;
    
        /**
         * Fetch related fields with the query. This will result in better
         * performance as related items are fetched with the root model with
         * only one trip to the database.
         *
         * Either an array of fields can be sent as one argument, or the list of
         * fields can be sent as the arguments to the function.
         *
         * Example:
         * >>> $q = User::objects()->select_related('role');
         */
    
    Jared Hancock's avatar
    Jared Hancock committed
        function select_related() {
    
            $args = func_get_args();
            if (is_array($args[0]))
                $args = $args[0];
    
            $this->related = array_merge($this->related, $args);
    
    Jared Hancock's avatar
    Jared Hancock committed
            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;
        }
    
    
            $this->iter = self::ITER_MODELS;
    
            $this->values = $this->related = array();
            return $this;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function values() {
    
            foreach (func_get_args() as $A)
                $this->values[$A] = $A;
    
            $this->iter = self::ITER_HASH;
    
            // 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->iter = self::ITER_ROW;
    
            // This disables related models
            $this->related = false;
    
        function copy() {
            return clone $this;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function all() {
    
            return $this->getIterator()->asArray();
    
        function first() {
    
            $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))
                );
    
            return $list[0];
    
    Jared Hancock's avatar
    Jared Hancock committed
        function count() {
    
            // 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);
    
        /**
         * Similar to count, except that the LIMIT and OFFSET parts are not
         * considered in the counts. That is, this will return the count of rows
         * if the query were not windowed with limit() and offset().
         *
         * For MySQL, the query will be submitted and fetched and the
         * SQL_CALC_FOUND_ROWS hint will be sent in the query. Afterwards, the
         * result of FOUND_ROWS() is fetched and is the result of this function.
         *
         * The result of this function is cached. If further changes are made
         * after this is run, the changes should be made in a clone.
         */
        function total() {
    
            if (isset($this->total))
                return $this->total;
    
    
            // Optimize the query with the CALC_FOUND_ROWS if
            // - the compiler supports it
            // - the iterator hasn't yet been built, that is, the query for this
            //   statement has not yet been sent to the database
            $compiler = $this->compiler;
            if ($compiler::supportsOption(self::OPT_MYSQL_FOUND_ROWS)
                && !isset($this->_iterator)
            ) {
                // This optimization requires caching
                $this->options(array(
                    self::OPT_MYSQL_FOUND_ROWS => 1,
                    self::OPT_NOCACHE => null,
                ));
                $this->exists(true);
                $compiler = new $compiler();
                return $this->total = $compiler->getFoundRows();
            }
    
            $query = clone $this;
            $query->limit(false)->offset(false)->order_by(false);
            return $this->total = $query->count();
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function toSql($compiler, $model, $alias=false) {
    
            // FIXME: Force root model of the compiler to $model
    
            $exec = $this->getQuery(array('compiler' => get_class($compiler),
                 'parent' => $compiler, 'subquery' => true));
    
            // Rewrite the parameter numbers so they fit the parameter numbers
            // of the current parameters of the $compiler
            $sql = preg_replace_callback("/:(\d+)/",
            function($m) use ($compiler, $exec) {
                $compiler->params[] = $exec->params[$m[1]-1];
                return ':'.count($compiler->params);
            }, $exec->sql);
            return "({$sql})".($alias ? " AS {$alias}" : '');
    
        /**
         * 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) {
    
    Peter Rotich's avatar
    Peter Rotich committed
                if ($A instanceof SqlAggregate) {
    
                    if (is_int($name))
                        $name = $A->getFieldName();
                    $A->setAlias($name);
                }
    
    Peter Rotich's avatar
    Peter Rotich committed
                $this->annotations[$name] = $A;
    
        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 options($options) {
    
            // Make an array with $options as the only key
            if (!is_array($options))
                $options = array($options => 1);
    
    
            $this->options = array_merge($this->options, $options);
            return $this;
        }
    
    
        function hasOption($option) {
            return isset($this->options[$option]);
        }
    
    
        function countSelectFields() {
            $count = count($this->values) + count($this->annotations);
            if (isset($this->extra['select']))
                foreach (@$this->extra['select'] as $S)
                    $count += count($S);
            return $count;
        }
    
    
        function union(QuerySet $other, $all=true) {
            // Values and values_list _must_ match for this to work
    
            if ($this->countSelectFields() != $other->countSelectFields())
    
                throw new OrmException('Union queries must have matching values counts');
    
            // TODO: Clear OFFSET and LIMIT in the $other query
    
            $this->chain[] = array($other, $all);
            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);
    
            unset($this->count);
    
            unset($this->total);
    
        function __call($name, $args) {
    
            if (!is_callable(array($this->getIterator(), $name)))
                throw new OrmException('Call to undefined method QuerySet::'.$name);
    
            return $args
                ? call_user_func_array(array($this->getIterator(), $name), $args)
                : call_user_func(array($this->getIterator(), $name));
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        // IteratorAggregate interface
    
        function getIterator($iterator=false) {
            if (!isset($this->_iterator)) {
                $class = $iterator ?: $this->getIteratorClass();
                $it = new $class($this);
                if (!$this->hasOption(self::OPT_NOCACHE)) {
                    if ($this->iter == self::ITER_MODELS)
                        // Add findFirst() and such
                        $it = new ModelResultSet($it);
                    else
                        $it = new CachedResultSet($it);
                }
                else {
                    $it = $it->getIterator();
                }
                $this->_iterator = $it;
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
            return $this->_iterator;
        }
    
    
        function getIteratorClass() {
            switch ($this->iter) {
            case self::ITER_MODELS:
                return 'ModelInstanceManager';
            case self::ITER_HASH:
                return 'HashArrayIterator';
            case self::ITER_ROW:
                return 'FlatArrayIterator';
            }
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        // 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;
    
            $meta = $model::getMeta();
    
            $query = clone $this;
    
            $options += $this->options;
    
            if ($options['nosort'])
                $query->ordering = array();
    
            elseif (!$query->ordering && $meta['ordering'])
                $query->ordering = $meta['ordering'];
    
            if (false !== $query->related && !$query->related && !$query->values && $meta['select_related'])
    
                $query->related = $meta['select_related'];
            if (!$query->defer && $meta['defer'])
                $query->defer = $meta['defer'];
    
    
            $class = $options['compiler'] ?: $this->compiler;
    
            $compiler = new $class($options);
    
            $this->query = $compiler->compileSelect($query);
    
    Jared Hancock's avatar
    Jared Hancock committed
    
            return $this->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}";
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if (class_exists($classname))
                return $classname;
    
    
            $class = <<<EOF
    class {$classname} extends VerySimpleModel {
        static \$meta = array(
            'view' => true,
        );
        static \$queryset;
    
        static function getQuery(\$compiler) {
            return ' ('.static::\$queryset->getQuery().') ';
        }
    
    
        static function getSqlAddParams(\$compiler) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            return static::\$queryset->toSql(\$compiler, self::\$queryset->model);
    
    }
    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']);
    
            unset($info['count']);
    
            unset($info['total']);
    
            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 {}
    
    
    extends BaseList
    implements ArrayAccess {
    
        protected $inner;
        protected $eoi = false;
    
        function __construct(IteratorAggregate $iterator) {
            $this->inner = $iterator->getIterator();
    
        function fillTo($level) {
    
            while (!$this->eoi && count($this->storage) < $level) {
    
                if (!$this->inner->valid()) {
                    $this->eoi = true;
                    break;
                }
    
                $this->storage[] = $this->inner->current();
    
        function asArray() {
            $this->fillTo(PHP_INT_MAX);
    
            return $this->getCache();
    
            return $this->storage;
        }
    
        function reset() {
            $this->eoi = false;
            $this->storage = array();
            // XXX: Should the inner be recreated to refetch?
            $this->inner->rewind();
    
    
        function getIterator() {
            $this->asArray();
    
            return new ArrayIterator($this->storage);
    
        }
    
        function offsetExists($offset) {
    
            $this->fillTo($offset+1);
    
            return count($this->storage) > $offset;
    
        }
        function offsetGet($offset) {
    
            $this->fillTo($offset+1);
    
            return $this->storage[$offset];
    
        }
        function offsetUnset($a) {
    
            throw new Exception(__('QuerySet is read-only'));
    
        }
        function offsetSet($a, $b) {
    
            throw new Exception(__('QuerySet is read-only'));
    
            return count($this->storage);
    
    
        /**
         * Sort the instrumented list in place. This would be useful to change the
         * sorting order of the items in the list without fetching the list from
         * the database again.
         *
         * Parameters:
         * $key - (callable|int) A callable function to produce the sort keys
         *      or one of the SORT_ constants used by the array_multisort
         *      function
         * $reverse - (bool) true if the list should be sorted descending
         *
         * Returns:
         * This instrumented list for chaining and inlining.
         */
        function sort($key=false, $reverse=false) {
            // Fetch all records into the cache
            $this->asArray();
    
    Peter Rotich's avatar
    Peter Rotich committed
            parent::sort($key, $reverse);
            return $this;
    
        }
    
        /**
         * Reverse the list item in place. Returns this object for chaining
         */
        function reverse() {
            $this->asArray();
    
            return parent::reverse();
    
    class ModelResultSet
    extends CachedResultSet {
        /**
         * Find the first item in the current set which matches the given criteria.
         * This would be used in favor of ::filter() which might trigger another
         * database query. The criteria is intended to be quite simple and should
         * not traverse relationships which have not already been fetched.
         * Otherwise, the ::filter() or ::window() methods would provide better
         * performance.
         *
         * Example:
         * >>> $a = new User();
         * >>> $a->roles->add(Role::lookup(['name' => 'administator']));
         * >>> $a->roles->findFirst(['roles__name__startswith' => 'admin']);
         * <Role: administrator>
         */
        function findFirst($criteria) {
            $records = $this->findAll($criteria, 1);
    
            return count($records) > 0 ? $records[0] : null;
    
        }
    
        /**
         * Find all the items in the current set which match the given criteria.
         * This would be used in favor of ::filter() which might trigger another
         * database query. The criteria is intended to be quite simple and should
         * not traverse relationships which have not already been fetched.
         * Otherwise, the ::filter() or ::window() methods would provide better
         * performance, as they can provide results with one more trip to the
         * database.
         */
        function findAll($criteria, $limit=false) {
    
            $records = new ListObject();
    
            foreach ($this as $record) {
                $matches = true;
                foreach ($criteria as $field=>$check) {
    
                    if (!SqlCompiler::evaluate($record, $check, $field)) {
    
                        $matches = false;
                        break;
                    }
                }
                if ($matches)
                    $records[] = $record;
                if ($limit && count($records) == $limit)
                    break;
            }
            return $records;
        }
    }
    
    class ModelInstanceManager
    implements IteratorAggregate {
        var $queryset;
    
        var $model;
        var $map;
    
        static $objectCache = array();
    
    
        function __construct(QuerySet $queryset) {
            $this->queryset = $queryset;
            $this->model = $queryset->model;
        }
    
    
        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 flushCache() {
            self::$objectCache = array();
        }
    
    
        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.
         */
    
    Peter Rotich's avatar
    Peter Rotich committed
        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) {
    
    Peter Rotich's avatar
    Peter Rotich committed
                foreach ($annotations as $name=>$A) {
    
                    if (array_key_exists($name, $fields)) {
    
                        $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 = $modelClass::__hydrate($fields);
    
                // XXX: defer may refer to fields not in this model
                $m->__deferred__ = $this->queryset->defer;
                $m->__onload();
                if ($cache)
                    $this->cache($m);
            }
            // Wrap annotations in an AnnotatedModel
            if ($extras) {
    
                $m = AnnotatedModel::wrap($m, $extras, $modelClass);
    
            // TODO: If the model has deferred fields which are in $fields,
            // those can be resolved here
    
            return $m;
        }
    
        /**
         * buildModel
         *
         * This method builds the model including related models from the record
         * received. For related recordsets, a $map should be setup inside this
         * object prior to using this method. The $map is assumed to have this
         * configuration:
         *
    
         * array(array(<fieldNames>, <modelClass>, <relativePath>))
    
         *
         * Where $modelClass is the name of the foreign (with respect to the
    
         * root model ($this->model), $fieldNames is the number and names of
         * fields in the row for this model, $relativePath is the path that
         * describes the relationship between the root model and this model,
         * 'user__account' for instance.
    
        function buildModel($row, $cache=true) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            // TODO: Traverse to foreign keys
    
            if ($this->map) {
                if ($this->model != $this->map[0][1])
                    throw new OrmException('Internal select_related error');
    
                $offset = 0;
                foreach ($this->map as $info) {
    
                    @list($fields, $model_class, $path) = $info;
                    $values = array_slice($row, $offset, count($fields));
                    $record = array_combine($fields, $values);
    
                    if (!$path) {
                        // Build the root model
    
                        $model = $this->getOrBuild($this->model, $record, $cache);
    
                    elseif ($model) {
    
                        $i = 0;
                        // Traverse the declared path and link the related model
    
                        $tail = array_pop($path);
                        $m = $model;
                        foreach ($path as $field) {
    
                            if (!($m = $m->get($field)))
                                break;
    
                        if ($m) {
                            // Only apply cache setting to the root model.
                            // Reference models should use caching
                            $m->set($tail, $this->getOrBuild($model_class, $record, $cache));
                        }
    
                    $offset += count($fields);
    
                $model = $this->getOrBuild($this->model, $row, $cache);
    
            return $model;
    
        function getIterator() {
            $this->resource = $this->queryset->getQuery();
            $this->map = $this->resource->getMap();
            $cache = !$this->queryset->hasOption(QuerySet::OPT_NOCACHE);
            $this->resource->setBuffered($cache);
    
            $func = ($this->map) ? 'getRow' : 'getArray';
    
            $func = array($this->resource, $func);
    
            return new CallbackSimpleIterator(function() use ($func, $cache) {
                global $StopIteration;
    
                if ($row = $func())
                    return $this->buildModel($row, $cache);
    
                $this->resource->close();
                throw $StopIteration;
            });
        }
    }
    
    class CallbackSimpleIterator
    implements Iterator {
        var $current;
        var $eoi;
        var $callback;
        var $key = -1;
    
        function __construct($callback) {
            assert(is_callable($callback));
            $this->callback = $callback;
    
        function rewind() {
            $this->eoi = false;
            $this->next();
        }
    
        function key() {
            return $this->key;
        }
    
        function valid() {
            if (!isset($this->eoi))
                $this->rewind();
            return !$this->eoi;
        }