Newer
Older
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);
/**
* 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);
}
}
return $this;
}
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);
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 || (count($this->values) + count($this->annotations) + @count($this->extra['select'])) > 1;
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->iter = self::ITER_MODELS;
$this->values = $this->related = array();
return $this;
}
foreach (func_get_args() as $A)
$this->values[$A] = $A;
$this->iter = self::ITER_HASH;
// This disables related models
$this->related = false;
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;
}
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))
);
// 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=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}" : '');
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
/**
* 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 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);
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));
}
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;
}
function getIteratorClass() {
switch ($this->iter) {
case self::ITER_MODELS:
return 'ModelInstanceManager';
case self::ITER_HASH:
return 'HashArrayIterator';
case self::ITER_ROW:
return 'FlatArrayIterator';
}
}
// 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;
$meta = $model::getMeta();
$options += $this->options;
if ($options['nosort'])
$query->ordering = array();
elseif (!$query->ordering && $meta['ordering'])
$query->ordering = $meta['ordering'];
if (false !== $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);
/**
* 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}";
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) {
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']);
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 {}
class CachedResultSet
implements IteratorAggregate, Countable, ArrayAccess {
protected $inner;
protected $eoi = false;
protected $cache = array();
function __construct(IteratorAggregate $iterator) {
$this->inner = $iterator->getIterator();
function fillTo($level) {
while (!$this->eoi && count($this->cache) < $level) {
if (!$this->inner->valid()) {
$this->eoi = true;
break;
}
$this->cache[] = $this->inner->current();
$this->inner->next();
}
function reset() {
$this->eoi = false;
$this->cache = array();
// XXX: Should the inner be recreated to refetch?
$this->inner->rewind();
}
function asArray() {
$this->fillTo(PHP_INT_MAX);
function getCache() {
return $this->cache;
function getIterator() {
$this->asArray();
return new ArrayIterator($this->cache);
}
function offsetExists($offset) {
$this->fillTo($offset+1);
return count($this->cache) > $offset;
$this->fillTo($offset+1);
return $this->cache[$offset];
}
function offsetUnset($a) {
throw new Exception(__('QuerySet is read-only'));
throw new Exception(__('QuerySet is read-only'));
$this->asArray();
return count($this->cache);
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
/**
* 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();
if (is_callable($key)) {
array_multisort(
array_map($key, $this->cache),
$reverse ? SORT_DESC : SORT_ASC,
$this->cache);
}
elseif ($key) {
array_multisort($this->cache,
$reverse ? SORT_DESC : SORT_ASC, $key);
}
elseif ($reverse) {
rsort($this->cache);
}
else
sort($this->cache);
return $this;
}
/**
* Reverse the list item in place. Returns this object for chaining
*/
function reverse() {
$this->asArray();
array_reverse($this->cache);
return $this;
}
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
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 @$records[0];
}
/**
* 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 = array();
foreach ($this as $record) {
$matches = true;
foreach ($criteria as $field=>$check) {
if (!SqlCompiler::evaluate($record, $field, $check)) {
$matches = false;
break;
}
}
if ($matches)
$records[] = $record;
if ($limit && count($records) == $limit)
break;
}
return $records;
}
}
class ModelInstanceManager
implements IteratorAggregate {
var $resource;
var $annnotations;
var $defer;
function __construct(QuerySet $queryset) {
$this->model = $queryset->model;
$this->resource = $queryset->getQuery();
$cache = !$queryset->hasOption(QuerySet::OPT_NOCACHE);
$this->resource->setBuffered($cache);
$this->map = $this->resource->getMap();
$this->annotations = $queryset->annotations;
$this->defer = $queryset->defer;
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.
*/
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->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 (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::$meta->newInstance($fields);
// XXX: defer may refer to fields not in this model
$m->__deferred__ = $this->defer;
$m->__onload();
if ($cache)
$this->cache($m);
}
// 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
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) {
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);
$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));
}
$model = $this->getOrBuild($this->model, $row, $cache);
function getIterator() {
$func = ($this->map) ? 'getRow' : 'getArray';
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
$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;
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
function rewind() {
$this->eoi = false;
$this->next();
}
function key() {
return $this->key;
}
function valid() {
if (!isset($this->eoi))
$this->rewind();
return !$this->eoi;
}
function current() {
if ($this->eoi) return false;
return $this->current;
}
function next() {
try {
$cbk = $this->callback;
$this->current = $cbk();
$this->key++;
}
catch (StopIteration $x) {
$this->eoi = true;
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
// Use a global variable, as constructing exceptions is expensive
class StopIteration extends Exception {}
$StopIteration = new StopIteration();
class FlatArrayIterator
implements IteratorAggregate {
var $queryset;
var $resource;
function __construct(QuerySet $queryset) {
$this->queryset = $queryset;
}
function getIterator() {
$this->resource = $this->queryset->getQuery();
return new CallbackSimpleIterator(function() {
global $StopIteration;
if ($row = $this->resource->getRow())
return $row;
$this->resource->close();
throw $StopIteration;
});
class HashArrayIterator
implements IteratorAggregate {
var $queryset;
var $resource;
function __construct(QuerySet $queryset) {
$this->queryset = $queryset;
}
function getIterator() {
$this->resource = $this->queryset->getQuery();
return new CallbackSimpleIterator(function() {
global $StopIteration;
if ($row = $this->resource->getArray())
return $row;
$this->resource->close();
throw $StopIteration;
});
class InstrumentedList
extends ModelResultSet {
var $key;
function __construct($fkey, $queryset=false) {
list($model, $this->key) = $fkey;
$queryset = $model::objects()->filter($this->key);
if ($related = $model::getMeta('select_related'))
$queryset->select_related($related);
}
parent::__construct(new ModelInstanceManager($queryset));
$this->queryset = $queryset;
function add($object, $at=false) {
if (!$object || !$object instanceof $this->model)
throw new Exception(sprintf(
'Attempting to add invalid object to list. Expected <%s>, but got <%s>',
$this->model,
get_class($object)
));
foreach ($this->key as $field=>$value)
$object->set($field, $value);
if (!$object->__new__)
$object->save();
if ($at !== false)
$this->cache[$at] = $object;
else
$this->cache[] = $object;
function remove($object, $delete=true) {
if ($delete)
$object->delete();
else
foreach ($this->key as $field=>$value)
$object->set($field, null);
* Slight edit to the standard iteration method which will skip deleted
* items.
function getIterator() {
return new CallbackFilterIterator(parent::getIterator(),
function($i) { return !$i->__deleted__; }
);
/**
* Reduce the list to a subset using a simply key/value constraint. New
* items added to the subset will have the constraint automatically
* added to all new items.
*/
function window($constraint) {
$model = $this->model;
$fields = $model::getMeta()->getFieldNames();
$key = $this->key;
foreach ($constraint as $field=>$value) {
if (!is_string($field) || false === in_array($field, $fields))
throw new OrmException('InstrumentedList windowing must be performed on local fields only');
$key[$field] = $value;
}
return new static(array($this->model, $key), $this->filter($constraint));
}
// Save all changes made to any list items
function saveAll() {
foreach ($this as $I)
if (!$I->save())
return false;
return true;
}
// QuerySet delegates
function count() {
return $this->objects()->count();
}
function exists() {
return $this->queryset->exists();
}
function expunge() {
if ($this->queryset->delete())
$this->reset();
function update(array $what) {
return $this->queryset->update($what);
}
// Fetch a new QuerySet
function objects() {
return clone $this->queryset;
function offsetUnset($a) {
$this->fillTo($a);
$this->cache[$a]->delete();
}
function offsetSet($a, $b) {
$this->fillTo($a);
$this->cache[$a]->delete();
$this->add($b, $a);
function __call($what, $how) {
return call_user_func_array(array($this->objects(), $what), $how);
var $joins = array();
var $aliases = array();
var $alias_num = 1;