From 44af0aa97a6a963f680a8c2f81830ac736e93177 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 22 Dec 2012 16:25:14 -0500 Subject: [PATCH] AR wip --- framework/base/Model.php | 39 ++++++++------ framework/db/ar/ActiveQuery.php | 37 ++++--------- framework/db/ar/ActiveRecord.php | 47 +++++++---------- framework/db/ar/ActiveRelation.php | 101 ++++++++++++++++++++++++++++++------ framework/db/ar/BaseActiveQuery.php | 26 ++++++++++ framework/db/ar/Relation.php | 72 +++++++++++++++++++++++-- framework/db/dao/QueryBuilder.php | 15 ++---- framework/util/ArrayHelper.php | 24 ++++----- framework/validators/Validator.php | 7 +-- tests/unit/data/ar/Customer.php | 4 +- tests/unit/data/ar/Item.php | 2 +- tests/unit/data/ar/Order.php | 34 +++--------- tests/unit/data/ar/OrderItem.php | 2 +- 13 files changed, 258 insertions(+), 152 deletions(-) diff --git a/framework/base/Model.php b/framework/base/Model.php index 731c4f6..6755e72 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -120,6 +120,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Returns a list of scenarios and the corresponding active attributes. + * An active attribute is one that is subject to validation in the current scenario. * The returned array should be in the following format: * * ~~~ @@ -130,14 +131,27 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * ) * ~~~ * + * By default, an active attribute that is considered safe and can be massively assigned. * If an attribute should NOT be massively assigned (thus considered unsafe), - * please prefix the attribute with an exclamation character (e.g. '!attribute'). + * please prefix the attribute with an exclamation character (e.g. '!rank'). * - * @return array a list of scenarios and the corresponding relevant attributes. + * The default implementation of this method will return a 'default' scenario + * which corresponds to all attributes listed in the validation rules applicable + * to the 'default' scenario. + * + * @return array a list of scenarios and the corresponding active attributes. */ public function scenarios() { - return array(); + $attributes = array(); + foreach ($this->getActiveValidators() as $validator) { + foreach ($validator->attributes as $name) { + $attributes[$name] = true; + } + } + return array( + 'default' => array_keys($attributes), + ); } /** @@ -287,7 +301,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess $scenario = $this->getScenario(); /** @var $validator Validator */ foreach ($this->getValidators() as $validator) { - if ($validator->isActive($scenario, $attribute)) { + if ($validator->isActive($scenario) && ($attribute === null || in_array($attribute, $validator->attributes, true))) { $validators[] = $validator; } } @@ -553,17 +567,15 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess { $scenario = $this->getScenario(); $scenarios = $this->scenarios(); + $attributes = array(); if (isset($scenarios[$scenario])) { - $attributes = array(); foreach ($scenarios[$scenario] as $attribute) { if ($attribute[0] !== '!') { $attributes[] = $attribute; } } - return $attributes; - } else { - return $this->activeAttributes(); } + return $attributes; } /** @@ -575,23 +587,16 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess $scenario = $this->getScenario(); $scenarios = $this->scenarios(); if (isset($scenarios[$scenario])) { - // scenario declared in scenarios() $attributes = $scenarios[$this->getScenario()]; foreach ($attributes as $i => $attribute) { if ($attribute[0] === '!') { $attributes[$i] = substr($attribute, 1); } } + return $attributes; } else { - // use validators to determine active attributes - $attributes = array(); - foreach ($this->attributes() as $attribute) { - if ($this->getActiveValidators($attribute) !== array()) { - $attributes[] = $attribute; - } - } + return array(); } - return $attributes; } /** diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php index 39195a1..7ef68e7 100644 --- a/framework/db/ar/ActiveQuery.php +++ b/framework/db/ar/ActiveQuery.php @@ -183,40 +183,25 @@ class ActiveQuery extends BaseQuery $records = $this->createRecords($rows); if ($records !== array()) { foreach ($this->with as $name => $config) { + /** @var Relation $relation */ $relation = $model->$name(); foreach ($config as $p => $v) { $relation->$p = $v; } - $relation->findWith($records); - } - } - - return $records; - } - - protected function createRecords($rows) - { - $records = array(); - if ($this->asArray) { - if ($this->index === null) { - return $rows; - } - foreach ($rows as $row) { - $records[$row[$this->index]] = $row; - } - } else { - /** @var $class ActiveRecord */ - $class = $this->modelClass; - if ($this->index === null) { - foreach ($rows as $row) { - $records[] = $class::create($row); + if ($relation->asArray === null) { + // inherit asArray from parent query + $relation->asArray = $this->asArray; } - } else { - foreach ($rows as $row) { - $records[$row[$this->index]] = $class::create($row); + $rs = $relation->findWith($records); + /* + foreach ($rs as $r) { + // find the matching parent record(s) + // insert into the parent records(s) } + */ } } + return $records; } } diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 32861fe..2ca8983 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -55,10 +55,6 @@ abstract class ActiveRecord extends Model * @var array old attribute values indexed by attribute names. */ private $_oldAttributes; - /** - * @var array related records indexed by relation names. - */ - private $_related; /** @@ -261,18 +257,18 @@ abstract class ActiveRecord extends Model * You may override this method if the table is not named after this convention. * @return string the table name */ - public function tableName() + public static function tableName() { - return StringHelper::camel2id(basename(get_class($this)), '_'); + return StringHelper::camel2id(basename(get_called_class()), '_'); } /** * Returns the schema information of the DB table associated with this AR class. * @return TableSchema the schema information of the DB table associated with this AR class. */ - public function getTableSchema() + public static function getTableSchema() { - return $this->getDbConnection()->getTableSchema($this->tableName()); + return static::getDbConnection()->getTableSchema(static::tableName()); } /** @@ -284,23 +280,21 @@ abstract class ActiveRecord extends Model * for this AR class. * @return string[] the primary keys of the associated database table. */ - public function primaryKey() + public static function primaryKey() { - return $this->getTableSchema()->primaryKey; + return static::getTableSchema()->primaryKey; } /** * Returns the default named scope that should be implicitly applied to all queries for this model. - * Note, default scope only applies to SELECT queries. It is ignored for INSERT, UPDATE and DELETE queries. + * Note, the default scope only applies to SELECT queries. It is ignored for INSERT, UPDATE and DELETE queries. * The default implementation simply returns an empty array. You may override this method - * if the model needs to be queried with some default criteria (e.g. only active records should be returned). - * @param BaseActiveQuery - * @return BaseActiveQuery the query criteria. This will be used as the parameter to the constructor - * of {@link CDbCriteria}. + * if the model needs to be queried with some default criteria (e.g. only non-deleted users should be returned). + * @param ActiveQuery */ public static function defaultScope($query) { - return $query; + // todo: should we drop this? } /** @@ -312,20 +306,17 @@ abstract class ActiveRecord extends Model */ public function __get($name) { - if (isset($this->_attributes[$name])) { + if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) { return $this->_attributes[$name]; - } - if (isset($this->getTableSchema()->columns[$name])) { + } elseif (isset($this->getTableSchema()->columns[$name])) { return null; } elseif (method_exists($this, $name)) { - if (isset($this->_related[$name]) || $this->_related !== null && array_key_exists($name, $this->_related)) { - return $this->_related[$name]; - } else { - // todo - return $this->_related[$name] = $this->findByRelation($md->relations[$name]); - } + // lazy loading related records + $query = $this->$name(); + return $this->_attributes[$name] = $query->multiple ? $query->all() : $query->one(); + } else { + return parent::__get($name); } - return parent::__get($name); } /** @@ -336,10 +327,8 @@ abstract class ActiveRecord extends Model */ public function __set($name, $value) { - if (isset($this->getTableSchema()->columns[$name])) { + if (isset($this->getTableSchema()->columns[$name]) || method_exists($this, $name)) { $this->_attributes[$name] = $value; - } elseif (method_exists($this, $name)) { - $this->_related[$name] = $value; } else { parent::__set($name, $value); } diff --git a/framework/db/ar/ActiveRelation.php b/framework/db/ar/ActiveRelation.php index fe7975c..e80dfd1 100644 --- a/framework/db/ar/ActiveRelation.php +++ b/framework/db/ar/ActiveRelation.php @@ -11,7 +11,10 @@ namespace yii\db\ar; /** - * ActiveRelation represents the specification of a relation declared in [[ActiveRecord::relations()]]. + * It is used in three scenarios: + * - eager loading: User::find()->with('posts')->all(); + * - lazy loading: $user->posts; + * - lazy loading with query options: $user->posts()->where('status=1')->get(); * * @author Qiang Xue * @since 2.0 @@ -19,35 +22,99 @@ namespace yii\db\ar; class ActiveRelation extends BaseActiveQuery { /** - * @var string the name of this relation + * @var string the class name of the ActiveRecord instances that this relation + * should create and populate query results into */ - public $name; + public $modelClass; /** - * @var string the name of the table + * @var ActiveRecord the primary record that this relation is associated with. + * This is used only in lazy loading with dynamic query options. */ - public $table; + public $primaryModel; /** * @var boolean whether this relation is a one-many relation */ public $hasMany; /** - * @var string the join type (e.g. INNER JOIN, LEFT JOIN). Defaults to 'LEFT JOIN' when - * this relation is used to load related records, and 'INNER JOIN' when this relation is used as a filter. - */ - public $joinType; - /** * @var array the columns of the primary and foreign tables that establish the relation. * The array keys must be columns of the table for this relation, and the array values - * must be the corresponding columns from the primary table. Do not prefix or quote the column names. - * They will be done automatically by Yii. + * must be the corresponding columns from the primary table. + * Do not prefix or quote the column names as they will be done automatically by Yii. */ public $link; /** - * @var string the ON clause of the join query - */ - public $on; - /** - * @var string|array + * @var ActiveRelation */ public $via; + + public function get() + { + + } + + public function findWith($name, &$primaryRecords) + { + if (empty($this->link) || !is_array($this->link)) { + throw new \yii\base\Exception('invalid link'); + } + $this->addLinkCondition($primaryRecords); + $records = $this->find(); + + /** @var array $map mapping key(s) to index of $primaryRecords */ + $index = $this->buildRecordIndex($primaryRecords, array_values($this->link)); + $this->initRecordRelation($primaryRecords, $name); + foreach ($records as $record) { + $key = $this->getRecordKey($record, array_keys($this->link)); + if (isset($index[$key])) { + $primaryRecords[$map[$key]][$name] = $record; + } + } + } + + protected function getRecordKey($record, $attributes) + { + if (isset($attributes[1])) { + $key = array(); + foreach ($attributes as $attribute) { + $key[] = is_array($record) ? $record[$attribute] : $record->$attribute; + } + return serialize($key); + } else { + $attribute = $attributes[0]; + return is_array($record) ? $record[$attribute] : $record->$attribute; + } + } + + protected function buildRecordIndex($records, $attributes) + { + $map = array(); + foreach ($records as $i => $record) { + $map[$this->getRecordKey($record, $attributes)] = $i; + } + return $map; + } + + protected function addLinkCondition($primaryRecords) + { + $attributes = array_keys($this->link); + $values = array(); + if (isset($links[1])) { + // composite keys + foreach ($primaryRecords as $record) { + $v = array(); + foreach ($this->link as $attribute => $link) { + $v[$attribute] = is_array($record) ? $record[$link] : $record->$link; + } + $values[] = $v; + } + } else { + // single key + $attribute = $this->link[$links[0]]; + foreach ($primaryRecords as $record) { + $values[] = is_array($record) ? $record[$attribute] : $record->$attribute; + } + } + $this->andWhere(array('in', $attributes, $values)); + } + } diff --git a/framework/db/ar/BaseActiveQuery.php b/framework/db/ar/BaseActiveQuery.php index 61dc664..5b61644 100644 --- a/framework/db/ar/BaseActiveQuery.php +++ b/framework/db/ar/BaseActiveQuery.php @@ -78,4 +78,30 @@ class BaseActiveQuery extends BaseQuery $this->scopes = $names; return $this; } + + protected function createModels($rows) + { + $models = array(); + if ($this->asArray) { + if ($this->index === null) { + return $rows; + } + foreach ($rows as $row) { + $models[$row[$this->index]] = $row; + } + } else { + /** @var $class ActiveRecord */ + $class = $this->modelClass; + if ($this->index === null) { + foreach ($rows as $row) { + $models[] = $class::create($row); + } + } else { + foreach ($rows as $row) { + $models[$row[$this->index]] = $class::create($row); + } + } + } + return $models; + } } diff --git a/framework/db/ar/Relation.php b/framework/db/ar/Relation.php index 1362216..68565f8 100644 --- a/framework/db/ar/Relation.php +++ b/framework/db/ar/Relation.php @@ -4,15 +4,79 @@ namespace yii\db\ar; class Relation extends ActiveQuery { + protected $multiple = false; public $parentClass; + /** + * @var array + */ + public $link; + /** + * @var ActiveQuery + */ + public $via; - public function findWith(&$parentRecords) + public function findWith($name, &$parentRecords) { - $this->andWhere(array('in', $links, $keys)); + if (empty($this->link) || !is_array($this->link)) { + throw new \yii\base\Exception('invalid link'); + } + $this->addLinkCondition($parentRecords); $records = $this->find(); + + /** @var array $map mapping key(s) to index of $parentRecords */ + $index = $this->buildRecordIndex($parentRecords, array_values($this->link)); + $this->initRecordRelation($parentRecords, $name); foreach ($records as $record) { - // find the matching parent record(s) - // insert into the parent records(s) + $key = $this->getRecordKey($record, array_keys($this->link)); + if (isset($index[$key])) { + $parentRecords[$map[$key]][$name] = $record; + } + } + } + + protected function getRecordKey($record, $attributes) + { + if (isset($attributes[1])) { + $key = array(); + foreach ($attributes as $attribute) { + $key[] = is_array($record) ? $record[$attribute] : $record->$attribute; + } + return serialize($key); + } else { + $attribute = $attributes[0]; + return is_array($record) ? $record[$attribute] : $record->$attribute; + } + } + + protected function buildRecordIndex($records, $attributes) + { + $map = array(); + foreach ($records as $i => $record) { + $map[$this->getRecordKey($record, $attributes)] = $i; + } + return $map; + } + + protected function addLinkCondition($parentRecords) + { + $attributes = array_keys($this->link); + $values = array(); + if (isset($links[1])) { + // composite keys + foreach ($parentRecords as $record) { + $v = array(); + foreach ($this->link as $attribute => $link) { + $v[$attribute] = is_array($record) ? $record[$link] : $record->$link; + } + $values[] = $v; + } + } else { + // single key + $attribute = $this->link[$links[0]]; + foreach ($parentRecords as $record) { + $values[] = is_array($record) ? $record[$attribute] : $record->$attribute; + } } + $this->andWhere(array('in', $attributes, $values)); } } diff --git a/framework/db/dao/QueryBuilder.php b/framework/db/dao/QueryBuilder.php index 447f1c5..fad17fe 100644 --- a/framework/db/dao/QueryBuilder.php +++ b/framework/db/dao/QueryBuilder.php @@ -578,22 +578,17 @@ class QueryBuilder extends \yii\base\Object $column = reset($column); foreach ($values as $i => $value) { if (is_array($value)) { - $values[$i] = isset($value[$column]) ? $value[$column] : null; + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $values[$i] = 'NULL'; } else { - $values[$i] = null; + $values[$i] = is_string($value) ? $this->connection->quoteValue($value) : (string)$value; } } } } - foreach ($values as $i => $value) { - if ($value === null) { - $values[$i] = 'NULL'; - } else { - $values[$i] = is_string($value) ? $this->connection->quoteValue($value) : (string)$value; - } - } - if (strpos($column, '(') === false) { $column = $this->quoteColumnName($column); } diff --git a/framework/util/ArrayHelper.php b/framework/util/ArrayHelper.php index c70f03a..8619f45 100644 --- a/framework/util/ArrayHelper.php +++ b/framework/util/ArrayHelper.php @@ -58,11 +58,11 @@ class ArrayHelper * * ~~~ * // working with array - * $username = \yii\util\ArrayHelper::get($_POST, 'username'); + * $username = \yii\util\ArrayHelper::getValue($_POST, 'username'); * // working with object - * $username = \yii\util\ArrayHelper::get($user, 'username'); + * $username = \yii\util\ArrayHelper::getValue($user, 'username'); * // working with anonymous function - * $fullName = \yii\util\ArrayHelper::get($user, function($user, $defaultValue) { + * $fullName = \yii\util\ArrayHelper::getValue($user, function($user, $defaultValue) { * return $user->firstName . ' ' . $user->lastName; * }); * ~~~ @@ -74,7 +74,7 @@ class ArrayHelper * @param mixed $default the default value to be returned if the specified key does not exist * @return mixed the value of the */ - public static function get($array, $key, $default = null) + public static function getValue($array, $key, $default = null) { if ($key instanceof \Closure) { return $key($array, $default); @@ -122,7 +122,7 @@ class ArrayHelper { $result = array(); foreach ($array as $element) { - $value = static::get($element, $key); + $value = static::getValue($element, $key); $result[$value] = $element; } return $result; @@ -139,11 +139,11 @@ class ArrayHelper * array('id' => '123', 'data' => 'abc'), * array('id' => '345', 'data' => 'def'), * ); - * $result = ArrayHelper::column($array, 'id'); + * $result = ArrayHelper::getColumn($array, 'id'); * // the result is: array( '123', '345') * * // using anonymous function - * $result = ArrayHelper::column($array, function(element) { + * $result = ArrayHelper::getColumn($array, function(element) { * return $element['id']; * }); * ~~~ @@ -152,11 +152,11 @@ class ArrayHelper * @param string|\Closure $key * @return array the list of column values */ - public static function column($array, $key) + public static function getColumn($array, $key) { $result = array(); foreach ($array as $element) { - $result[] = static::get($element, $key); + $result[] = static::getValue($element, $key); } return $result; } @@ -206,10 +206,10 @@ class ArrayHelper { $result = array(); foreach ($array as $element) { - $key = static::get($element, $from); - $value = static::get($element, $to); + $key = static::getValue($element, $from); + $value = static::getValue($element, $to); if ($group !== null) { - $result[static::get($element, $group)][$key] = $value; + $result[static::getValue($element, $group)][$key] = $value; } else { $result[$key] = $value; } diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index 5dd2fdb..c8d773a 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -229,14 +229,11 @@ abstract class Validator extends \yii\base\Component * - the validator's `on` property contains the specified scenario * * @param string $scenario scenario name - * @param string|null $attribute the attribute name to check. If this is not null, - * the method will also check if the attribute appears in [[attributes]]. * @return boolean whether the validator applies to the specified scenario. */ - public function isActive($scenario, $attribute = null) + public function isActive($scenario) { - $applies = !isset($this->except[$scenario]) && (empty($this->on) || isset($this->on[$scenario])); - return $attribute === null ? $applies : $applies && in_array($attribute, $this->attributes, true); + return !isset($this->except[$scenario]) && (empty($this->on) || isset($this->on[$scenario])); } /** diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index 2b6ac21..71dab97 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -8,7 +8,7 @@ class Customer extends ActiveRecord const STATUS_ACTIVE = 1; const STATUS_INACTIVE = 2; - public function tableName() + public static function tableName() { return 'tbl_customer'; } @@ -24,6 +24,6 @@ class Customer extends ActiveRecord */ public function active($query) { - return $query->andWhere('`status` = ' . self::STATUS_ACTIVE); + return $query->andWhere(array('status' => self::STATUS_ACTIVE)); } } \ No newline at end of file diff --git a/tests/unit/data/ar/Item.php b/tests/unit/data/ar/Item.php index 1b45f1e..279893f 100644 --- a/tests/unit/data/ar/Item.php +++ b/tests/unit/data/ar/Item.php @@ -4,7 +4,7 @@ namespace yiiunit\data\ar; class Item extends ActiveRecord { - public function tableName() + public static function tableName() { return 'tbl_item'; } diff --git a/tests/unit/data/ar/Order.php b/tests/unit/data/ar/Order.php index 5ac3348..27df1fc 100644 --- a/tests/unit/data/ar/Order.php +++ b/tests/unit/data/ar/Order.php @@ -4,7 +4,7 @@ namespace yiiunit\data\ar; class Order extends ActiveRecord { - public function tableName() + public static function tableName() { return 'tbl_order'; } @@ -22,36 +22,14 @@ class Order extends ActiveRecord public function items() { return $this->hasMany('Item', array('id' => 'item_id')) - ->via('orderItems')->orderBy('id'); + ->via('orderItems') + ->orderBy('id'); } public function books() { - return $this->manyMany('Item', array('id' => 'item_id'), 'tbl_order_item', array('item_id', 'id')) - ->where('category_id = 1'); - } - - public function customer() - { - return $this->hasOne('Customer', array('id' => 'customer_id')); - } - - public function orderItems() - { - return $this->hasMany('OrderItem', array('order_id' => 'id')); - } - - public function items() - { - return $this->hasMany('Item') - ->via('orderItems', array('item_id' => 'id')) - ->order('@.id'); - } - - public function books() - { - return $this->hasMany('Item') - ->pivot('tbl_order_item', array('order_id' => 'id'), array('item_id' => 'id')) - ->on('@.category_id = 1'); + return $this->hasMany('Item', array('id' => 'item_id')) + ->via('tbl_order_item', array('order_id' => 'id')) + ->where(array('category_id' => 1)); } } \ No newline at end of file diff --git a/tests/unit/data/ar/OrderItem.php b/tests/unit/data/ar/OrderItem.php index 0141f11..27e1c7e 100644 --- a/tests/unit/data/ar/OrderItem.php +++ b/tests/unit/data/ar/OrderItem.php @@ -4,7 +4,7 @@ namespace yiiunit\data\ar; class OrderItem extends ActiveRecord { - public function tableName() + public static function tableName() { return 'tbl_order_item'; }