Browse Source

AR wip

tags/2.0.0-beta
Qiang Xue 12 years ago
parent
commit
44af0aa97a
  1. 39
      framework/base/Model.php
  2. 37
      framework/db/ar/ActiveQuery.php
  3. 45
      framework/db/ar/ActiveRecord.php
  4. 101
      framework/db/ar/ActiveRelation.php
  5. 26
      framework/db/ar/BaseActiveQuery.php
  6. 72
      framework/db/ar/Relation.php
  7. 11
      framework/db/dao/QueryBuilder.php
  8. 24
      framework/util/ArrayHelper.php
  9. 7
      framework/validators/Validator.php
  10. 4
      tests/unit/data/ar/Customer.php
  11. 2
      tests/unit/data/ar/Item.php
  12. 34
      tests/unit/data/ar/Order.php
  13. 2
      tests/unit/data/ar/OrderItem.php

39
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. * 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: * 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), * 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() 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(); $scenario = $this->getScenario();
/** @var $validator Validator */ /** @var $validator Validator */
foreach ($this->getValidators() as $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; $validators[] = $validator;
} }
} }
@ -553,17 +567,15 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
{ {
$scenario = $this->getScenario(); $scenario = $this->getScenario();
$scenarios = $this->scenarios(); $scenarios = $this->scenarios();
if (isset($scenarios[$scenario])) {
$attributes = array(); $attributes = array();
if (isset($scenarios[$scenario])) {
foreach ($scenarios[$scenario] as $attribute) { foreach ($scenarios[$scenario] as $attribute) {
if ($attribute[0] !== '!') { if ($attribute[0] !== '!') {
$attributes[] = $attribute; $attributes[] = $attribute;
} }
} }
return $attributes;
} else {
return $this->activeAttributes();
} }
return $attributes;
} }
/** /**
@ -575,23 +587,16 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
$scenario = $this->getScenario(); $scenario = $this->getScenario();
$scenarios = $this->scenarios(); $scenarios = $this->scenarios();
if (isset($scenarios[$scenario])) { if (isset($scenarios[$scenario])) {
// scenario declared in scenarios()
$attributes = $scenarios[$this->getScenario()]; $attributes = $scenarios[$this->getScenario()];
foreach ($attributes as $i => $attribute) { foreach ($attributes as $i => $attribute) {
if ($attribute[0] === '!') { if ($attribute[0] === '!') {
$attributes[$i] = substr($attribute, 1); $attributes[$i] = substr($attribute, 1);
} }
} }
return $attributes;
} else { } else {
// use validators to determine active attributes return array();
$attributes = array();
foreach ($this->attributes() as $attribute) {
if ($this->getActiveValidators($attribute) !== array()) {
$attributes[] = $attribute;
}
}
} }
return $attributes;
} }
/** /**

37
framework/db/ar/ActiveQuery.php

@ -183,40 +183,25 @@ class ActiveQuery extends BaseQuery
$records = $this->createRecords($rows); $records = $this->createRecords($rows);
if ($records !== array()) { if ($records !== array()) {
foreach ($this->with as $name => $config) { foreach ($this->with as $name => $config) {
/** @var Relation $relation */
$relation = $model->$name(); $relation = $model->$name();
foreach ($config as $p => $v) { foreach ($config as $p => $v) {
$relation->$p = $v; $relation->$p = $v;
} }
$relation->findWith($records); if ($relation->asArray === null) {
// inherit asArray from parent query
$relation->asArray = $this->asArray;
} }
$rs = $relation->findWith($records);
/*
foreach ($rs as $r) {
// find the matching parent record(s)
// insert into the parent records(s)
} }
*/
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);
}
} else {
foreach ($rows as $row) {
$records[$row[$this->index]] = $class::create($row);
}
} }
} }
return $records; return $records;
} }
} }

45
framework/db/ar/ActiveRecord.php

@ -55,10 +55,6 @@ abstract class ActiveRecord extends Model
* @var array old attribute values indexed by attribute names. * @var array old attribute values indexed by attribute names.
*/ */
private $_oldAttributes; 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. * You may override this method if the table is not named after this convention.
* @return string the table name * @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. * 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. * @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. * for this AR class.
* @return string[] the primary keys of the associated database table. * @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. * 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 * 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). * if the model needs to be queried with some default criteria (e.g. only non-deleted users should be returned).
* @param BaseActiveQuery * @param ActiveQuery
* @return BaseActiveQuery the query criteria. This will be used as the parameter to the constructor
* of {@link CDbCriteria}.
*/ */
public static function defaultScope($query) public static function defaultScope($query)
{ {
return $query; // todo: should we drop this?
} }
/** /**
@ -312,21 +306,18 @@ abstract class ActiveRecord extends Model
*/ */
public function __get($name) public function __get($name)
{ {
if (isset($this->_attributes[$name])) { if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
return $this->_attributes[$name]; return $this->_attributes[$name];
} } elseif (isset($this->getTableSchema()->columns[$name])) {
if (isset($this->getTableSchema()->columns[$name])) {
return null; return null;
} elseif (method_exists($this, $name)) { } elseif (method_exists($this, $name)) {
if (isset($this->_related[$name]) || $this->_related !== null && array_key_exists($name, $this->_related)) { // lazy loading related records
return $this->_related[$name]; $query = $this->$name();
return $this->_attributes[$name] = $query->multiple ? $query->all() : $query->one();
} else { } else {
// todo
return $this->_related[$name] = $this->findByRelation($md->relations[$name]);
}
}
return parent::__get($name); return parent::__get($name);
} }
}
/** /**
* PHP setter magic method. * PHP setter magic method.
@ -336,10 +327,8 @@ abstract class ActiveRecord extends Model
*/ */
public function __set($name, $value) 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; $this->_attributes[$name] = $value;
} elseif (method_exists($this, $name)) {
$this->_related[$name] = $value;
} else { } else {
parent::__set($name, $value); parent::__set($name, $value);
} }

101
framework/db/ar/ActiveRelation.php

@ -11,7 +11,10 @@
namespace yii\db\ar; 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 <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -19,35 +22,99 @@ namespace yii\db\ar;
class ActiveRelation extends BaseActiveQuery 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 * @var boolean whether this relation is a one-many relation
*/ */
public $hasMany; 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. * @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 * 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. * must be the corresponding columns from the primary table.
* They will be done automatically by Yii. * Do not prefix or quote the column names as they will be done automatically by Yii.
*/ */
public $link; public $link;
/** /**
* @var string the ON clause of the join query * @var ActiveRelation
*/
public $on;
/**
* @var string|array
*/ */
public $via; 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));
}
} }

26
framework/db/ar/BaseActiveQuery.php

@ -78,4 +78,30 @@ class BaseActiveQuery extends BaseQuery
$this->scopes = $names; $this->scopes = $names;
return $this; 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;
}
} }

72
framework/db/ar/Relation.php

@ -4,15 +4,79 @@ namespace yii\db\ar;
class Relation extends ActiveQuery class Relation extends ActiveQuery
{ {
protected $multiple = false;
public $parentClass; 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(); $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) { foreach ($records as $record) {
// find the matching parent record(s) $key = $this->getRecordKey($record, array_keys($this->link));
// insert into the parent records(s) 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));
} }
} }

11
framework/db/dao/QueryBuilder.php

@ -578,21 +578,16 @@ class QueryBuilder extends \yii\base\Object
$column = reset($column); $column = reset($column);
foreach ($values as $i => $value) { foreach ($values as $i => $value) {
if (is_array($value)) { if (is_array($value)) {
$values[$i] = isset($value[$column]) ? $value[$column] : null; $value = isset($value[$column]) ? $value[$column] : null;
} else {
$values[$i] = null;
}
}
}
} }
foreach ($values as $i => $value) {
if ($value === null) { if ($value === null) {
$values[$i] = 'NULL'; $values[$i] = 'NULL';
} else { } else {
$values[$i] = is_string($value) ? $this->connection->quoteValue($value) : (string)$value; $values[$i] = is_string($value) ? $this->connection->quoteValue($value) : (string)$value;
} }
} }
}
}
if (strpos($column, '(') === false) { if (strpos($column, '(') === false) {
$column = $this->quoteColumnName($column); $column = $this->quoteColumnName($column);

24
framework/util/ArrayHelper.php

@ -58,11 +58,11 @@ class ArrayHelper
* *
* ~~~ * ~~~
* // working with array * // working with array
* $username = \yii\util\ArrayHelper::get($_POST, 'username'); * $username = \yii\util\ArrayHelper::getValue($_POST, 'username');
* // working with object * // working with object
* $username = \yii\util\ArrayHelper::get($user, 'username'); * $username = \yii\util\ArrayHelper::getValue($user, 'username');
* // working with anonymous function * // 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; * 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 * @param mixed $default the default value to be returned if the specified key does not exist
* @return mixed the value of the * @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) { if ($key instanceof \Closure) {
return $key($array, $default); return $key($array, $default);
@ -122,7 +122,7 @@ class ArrayHelper
{ {
$result = array(); $result = array();
foreach ($array as $element) { foreach ($array as $element) {
$value = static::get($element, $key); $value = static::getValue($element, $key);
$result[$value] = $element; $result[$value] = $element;
} }
return $result; return $result;
@ -139,11 +139,11 @@ class ArrayHelper
* array('id' => '123', 'data' => 'abc'), * array('id' => '123', 'data' => 'abc'),
* array('id' => '345', 'data' => 'def'), * array('id' => '345', 'data' => 'def'),
* ); * );
* $result = ArrayHelper::column($array, 'id'); * $result = ArrayHelper::getColumn($array, 'id');
* // the result is: array( '123', '345') * // the result is: array( '123', '345')
* *
* // using anonymous function * // using anonymous function
* $result = ArrayHelper::column($array, function(element) { * $result = ArrayHelper::getColumn($array, function(element) {
* return $element['id']; * return $element['id'];
* }); * });
* ~~~ * ~~~
@ -152,11 +152,11 @@ class ArrayHelper
* @param string|\Closure $key * @param string|\Closure $key
* @return array the list of column values * @return array the list of column values
*/ */
public static function column($array, $key) public static function getColumn($array, $key)
{ {
$result = array(); $result = array();
foreach ($array as $element) { foreach ($array as $element) {
$result[] = static::get($element, $key); $result[] = static::getValue($element, $key);
} }
return $result; return $result;
} }
@ -206,10 +206,10 @@ class ArrayHelper
{ {
$result = array(); $result = array();
foreach ($array as $element) { foreach ($array as $element) {
$key = static::get($element, $from); $key = static::getValue($element, $from);
$value = static::get($element, $to); $value = static::getValue($element, $to);
if ($group !== null) { if ($group !== null) {
$result[static::get($element, $group)][$key] = $value; $result[static::getValue($element, $group)][$key] = $value;
} else { } else {
$result[$key] = $value; $result[$key] = $value;
} }

7
framework/validators/Validator.php

@ -229,14 +229,11 @@ abstract class Validator extends \yii\base\Component
* - the validator's `on` property contains the specified scenario * - the validator's `on` property contains the specified scenario
* *
* @param string $scenario scenario name * @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. * @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 !isset($this->except[$scenario]) && (empty($this->on) || isset($this->on[$scenario]));
return $attribute === null ? $applies : $applies && in_array($attribute, $this->attributes, true);
} }
/** /**

4
tests/unit/data/ar/Customer.php

@ -8,7 +8,7 @@ class Customer extends ActiveRecord
const STATUS_ACTIVE = 1; const STATUS_ACTIVE = 1;
const STATUS_INACTIVE = 2; const STATUS_INACTIVE = 2;
public function tableName() public static function tableName()
{ {
return 'tbl_customer'; return 'tbl_customer';
} }
@ -24,6 +24,6 @@ class Customer extends ActiveRecord
*/ */
public function active($query) public function active($query)
{ {
return $query->andWhere('`status` = ' . self::STATUS_ACTIVE); return $query->andWhere(array('status' => self::STATUS_ACTIVE));
} }
} }

2
tests/unit/data/ar/Item.php

@ -4,7 +4,7 @@ namespace yiiunit\data\ar;
class Item extends ActiveRecord class Item extends ActiveRecord
{ {
public function tableName() public static function tableName()
{ {
return 'tbl_item'; return 'tbl_item';
} }

34
tests/unit/data/ar/Order.php

@ -4,7 +4,7 @@ namespace yiiunit\data\ar;
class Order extends ActiveRecord class Order extends ActiveRecord
{ {
public function tableName() public static function tableName()
{ {
return 'tbl_order'; return 'tbl_order';
} }
@ -22,36 +22,14 @@ class Order extends ActiveRecord
public function items() public function items()
{ {
return $this->hasMany('Item', array('id' => 'item_id')) return $this->hasMany('Item', array('id' => 'item_id'))
->via('orderItems')->orderBy('id'); ->via('orderItems')
->orderBy('id');
} }
public function books() public function books()
{ {
return $this->manyMany('Item', array('id' => 'item_id'), 'tbl_order_item', array('item_id', 'id')) return $this->hasMany('Item', array('id' => 'item_id'))
->where('category_id = 1'); ->via('tbl_order_item', array('order_id' => 'id'))
} ->where(array('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');
} }
} }

2
tests/unit/data/ar/OrderItem.php

@ -4,7 +4,7 @@ namespace yiiunit\data\ar;
class OrderItem extends ActiveRecord class OrderItem extends ActiveRecord
{ {
public function tableName() public static function tableName()
{ {
return 'tbl_order_item'; return 'tbl_order_item';
} }

Loading…
Cancel
Save