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. 47
      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. 15
      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.
* 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;
}
/**

37
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;
}
}

47
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);
}

101
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 <qiang.xue@gmail.com>
* @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));
}
}

26
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;
}
}

72
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));
}
}

15
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);
}

24
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;
}

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
*
* @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]));
}
/**

4
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));
}
}

2
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';
}

34
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));
}
}

2
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';
}

Loading…
Cancel
Save