diff --git a/include/class.orm.php b/include/class.orm.php index 07f6131041c0f69f715c9b95e24fd6fefc4b8184..ac81b8fd28f124342c1f80e1e3db01ecaa8bb544 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -19,6 +19,93 @@ 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 { + + var $base = array(); + + function __construct($model) { + $meta = $model::$meta; + + 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', 'deferred') as $f) { + if (!isset($meta[$f])) + $meta[$f] = array(); + elseif (!is_array($meta[$f])) + $meta[$f] = array($meta[$f]); + } + + // Break down foreign-key metadata + foreach ($meta['joins'] as $field => &$j) { + if (isset($j['reverse'])) { + list($model, $key) = explode('.', $j['reverse']); + $info = $model::$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] = "$model.$foreign"; + } + $j['constraint'] = $constraint; + if (!isset($j['list'])) + $j['list'] = true; + } + // 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]; + } + $this->base = $meta; + } + + 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; + default: + throw new Exception($what . ': No such meta-data'); + } + } + + function inspectFields() { + return DbEngine::getCompiler()->inspectTable($this['table']); + } +} + class VerySimpleModel { static $meta = array( 'table' => false, @@ -27,23 +114,34 @@ class VerySimpleModel { ); var $ht; - var $dirty; + var $dirty = array(); var $__new__ = false; + var $__deferred__ = array(); function __construct($row) { $this->ht = $row; - $this->__setupForeignLists(); - $this->dirty = array(); } function get($field, $default=false) { if (array_key_exists($field, $this->ht)) return $this->ht[$field]; elseif (isset(static::$meta['joins'][$field])) { - // TODO: Support instrumented lists and such - $j = static::$meta['joins'][$field]; // Make sure joins were inspected - if (isset($j['fkey']) + 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[$name] = 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)) { $v = $this->ht[$field] = $class::lookup( @@ -51,8 +149,21 @@ class VerySimpleModel { 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)); } @@ -115,58 +226,16 @@ class VerySimpleModel { $this->set($field, $value); } - function __setupForeignLists() { - // Construct related lists - if (isset(static::$meta['joins'])) { - foreach (static::$meta['joins'] as $name => $j) { - if (isset($this->ht[$j['local']]) - && isset($j['list']) && $j['list']) { - $fkey = $j['fkey']; - $this->set($name, new InstrumentedList( - // Send Model, Foriegn-Field, Local-Id - array($fkey[0], $fkey[1], $this->get($j['local']))) - ); - } - } - } - } - function __onload() {} static function __oninspect() {} static function _inspect() { - if (!static::$meta['table']) - throw new OrmConfigurationException( - __('Model does not define meta.table'), get_called_class()); + if (!static::$meta instanceof ModelMeta) { + static::$meta = new ModelMeta(get_called_class()); - // Break down foreign-key metadata - foreach (static::$meta['joins'] as $field => &$j) { - if (isset($j['reverse'])) { - list($model, $key) = explode('.', $j['reverse']); - $info = $model::$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] = "$model.$foreign"; - } - $j['constraint'] = $constraint; - if (!isset($j['list'])) - $j['list'] = true; - } - // 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]; + // Let the model participate + static::__oninspect(); } - - // Let the model participate - static::__oninspect(); } static function objects() { @@ -210,14 +279,12 @@ class VerySimpleModel { } $pk = static::$meta['pk']; - if (!is_array($pk)) $pk=array($pk); if ($this->__new__) { if (count($pk) == 1) + // XXX: Ensure AUTO_INCREMENT is set for the field $this->ht[$pk[0]] = $ex->insert_id(); $this->__new__ = false; - // Setup lists again - $this->__setupForeignLists(); Signal::send('model.created', $this); } else { @@ -246,6 +313,13 @@ class VerySimpleModel { $i->set($field, $value); return $i; } + + private function getPk() { + $pk = array(); + foreach ($this::$meta['pk'] as $f) + $pk[$f] = $this->ht[$f]; + return $pk; + } } class SqlFunction { @@ -270,12 +344,12 @@ class QuerySet implements IteratorAggregate, ArrayAccess { var $model; var $constraints = array(); - var $exclusions = array(); var $ordering = array(); var $limit = false; var $offset = 0; var $related = array(); var $values = array(); + var $defer = array(); var $lock = false; const LOCK_EXCLUSIVE = 1; @@ -310,6 +384,12 @@ class QuerySet implements IteratorAggregate, ArrayAccess { return $this; } + function defer() { + foreach (func_get_args() as $f) + $this->defer[$f] = true; + return $this; + } + function order_by() { $this->ordering = array_merge($this->ordering, func_get_args()); return $this; @@ -449,6 +529,7 @@ class ModelInstanceIterator implements Iterator, ArrayAccess { function buildModel($row) { // TODO: Traverse to foreign keys $model = new $this->model($row); # nolint + $model->__deferred__ = $this->queryset->defer; $model->__onload(); return $model; } @@ -807,18 +888,17 @@ class DbEngine { // Gets a compiler compatible with this database engine that can compile // and execute a queryset or DML request. - function getCompiler() { + static function getCompiler() { + $class = static::$compiler; + return new $class(); } static function delete(VerySimpleModel $model) { - $class = static::$compiler; - $compiler = new $class(); - return $compiler->compileDelete($model); + return static::getCompiler()->compileDelete($model); } static function save(VerySimpleModel $model) { - $class = static::$compiler; - $compiler = new $class(); + $compiler = static::getCompiler(); if ($model->__new__) return $compiler->compileInsert($model); else @@ -887,7 +967,7 @@ class MySqlCompiler extends SqlCompiler { function input(&$what) { if ($what instanceof QuerySet) { $q = $what->getQuery(array('nosort'=>true)); - $this->params += $q->params; + $this->params = array_merge($q->params); return (string)$q; } elseif ($what instanceof SqlFunction) { @@ -965,7 +1045,16 @@ class MySqlCompiler extends SqlCompiler { list($fields[]) = $this->getField($v, $model); } } else { - $fields[] = $this->quote($table).'.*'; + if ($queryset->defer) { + foreach ($model::$meta['fields'] as $f) { + if (isset($queryset->defer[$f])) + continue; + $fields[] = $this->quote($f); + } + } + else { + $fields[] = $this->quote($table).'.*'; + } } $joins = $this->getJoins(); @@ -1000,13 +1089,9 @@ class MySqlCompiler extends SqlCompiler { function compileUpdate(VerySimpleModel $model) { $pk = $model::$meta['pk']; - if (!is_array($pk)) $pk=array($pk); - $sql = 'UPDATE '.$model::$meta['table']; + $sql = 'UPDATE '.$this->quote($model::$meta['table']); $sql .= $this->__compileUpdateSet($model, $pk); - $filter = array(); - foreach ($pk as $p) - $filter[$p] = $model->get($p); - $sql .= ' WHERE '.$this->compileQ(new Q($filter), $model); + $sql .= ' WHERE '.$this->compileQ(new Q($model->pk), $model); $sql .= ' LIMIT 1'; return new MySqlExecutor($sql, $this->params); @@ -1014,8 +1099,7 @@ class MySqlCompiler extends SqlCompiler { function compileInsert(VerySimpleModel $model) { $pk = $model::$meta['pk']; - if (!is_array($pk)) $pk=array($pk); - $sql = 'INSERT INTO '.$model::$meta['table']; + $sql = 'INSERT INTO '.$this->quote($model::$meta['table']); $sql .= $this->__compileUpdateSet($model, $pk); return new MySqlExecutor($sql, $this->params); @@ -1024,14 +1108,7 @@ class MySqlCompiler extends SqlCompiler { function compileDelete($model) { $table = $model::$meta['table']; - $filter = array(); - $pk = $model::$meta['pk']; - if (!is_array($pk)) $pk=array($pk); - - foreach ($pk as $p) - $filter[$p] = $model->get($p); - - $where = ' WHERE '.implode(' AND ', $this->compileConstraints($filter)); + $where = ' WHERE '.implode(' AND ', $this->compileConstraints($model->pk)); $sql = 'DELETE FROM '.$this->quote($table).$where.' LIMIT 1'; return new MySqlExecutor($sql, $this->params); } @@ -1061,6 +1138,22 @@ class MySqlCompiler extends SqlCompiler { // Returns meta data about the table used to build queries function inspectTable($table) { + static $cache = array(); + + // XXX: Assuming schema is not changing — add support to track + // current schema + if (isset($cache[$table])) + return $cache[$table]; + + $sql = 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS ' + .'WHERE TABLE_NAME = ? AND TABLE_SCHEMA = DATABASE() ' + .'ORDER BY ORDINAL_POSITION'; + $ex = new MysqlExecutor($sql, array($table)); + $columns = array(); + while (list($column) = $ex->getRow()) { + $columns[] = $column; + } + return $cache[$table] = $columns; } }