From fbcf677697f09d944f28aad978f9e099b6e9ac66 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 4 Oct 2012 20:16:18 +0400 Subject: [PATCH 1/6] documented model validation and scenarios --- docs/model.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 docs/model.md diff --git a/docs/model.md b/docs/model.md new file mode 100644 index 0000000..ca823ff --- /dev/null +++ b/docs/model.md @@ -0,0 +1,36 @@ +Model +===== + +Validation rules and mass assignment +------------------------------------ + +In Yii2 unlike Yii 1.x validation rules are separated from mass assignment. Validation +rules are described in `rules()` method of the model while what's safe for mass +assignment is described in `scenarios` method: + +```php + +function rules() { + return array( + // rule applied when corresponding field is "safe" + array('username', 'length', 'min' => 2), + array('first_name', 'length', 'min' => 2), + array('password', 'required'), + + // rule applied when scenario is "signup" no matter if field is "safe" or not + array('hashcode', 'check', 'on' => 'signup'), + ); +} + +function scenarios() { + return array( + // on signup allow mass assignment of username + 'signup' => array('username', 'password'), + 'update' => array('username', 'first_name'), + ); +} + +``` + +Note that everything is unsafe by default and you can't make field "safe" +without specifying scenario. \ No newline at end of file From da786f657a541927324d009a4a2b6f3429625e20 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 9 Nov 2012 17:47:50 -0800 Subject: [PATCH 2/6] Implemented new rules and safe attributes --- framework/base/Model.php | 199 ++++++++++++++++++++----------------- framework/db/ar/ActiveRecord.php | 8 +- framework/validators/Validator.php | 24 ++--- framework/web/Sort.php | 2 +- 4 files changed, 121 insertions(+), 112 deletions(-) diff --git a/framework/base/Model.php b/framework/base/Model.php index b2da4e2..a7c3433 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -10,6 +10,8 @@ namespace yii\base; use yii\util\StringHelper; +use yii\validators\Validator; +use yii\validators\RequiredValidator; /** * Model is the base class for data models. @@ -35,7 +37,6 @@ use yii\util\StringHelper; * @property array $errors Errors for all attributes or the specified attribute. Empty array is returned if no error. * @property array $attributes Attribute values (name=>value). * @property string $scenario The scenario that this model is in. - * @property array $safeAttributeNames Safe attribute names in the current [[scenario]]. * * @event ModelEvent beforeValidate an event raised at the beginning of [[validate()]]. You may set * [[ModelEvent::isValid]] to be false to stop the validation. @@ -48,43 +49,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess { private static $_attributes = array(); // class name => array of attribute names private $_errors; // attribute name => array of errors - private $_validators; // validators - private $_scenario; // scenario - - /** - * Constructor. - * @param string|null $scenario name of the [[scenario]] that this model is used in. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($scenario = null, $config = array()) - { - $this->_scenario = $scenario; - parent::__construct($config); - } - - /** - * Returns the list of attribute names. - * By default, this method returns all public non-static properties of the class. - * You may override this method to change the default behavior. - * @return array list of attribute names. - */ - public function attributeNames() - { - $className = get_class($this); - if (isset(self::$_attributes[$className])) { - return self::$_attributes[$className]; - } - - $class = new \ReflectionClass($this); - $names = array(); - foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { - $name = $property->getName(); - if (!$property->isStatic()) { - $names[] = $name; - } - } - return self::$_attributes[$className] = $names; - } + private $_validators; // Vector of validators + private $_scenario = 'default'; /** * Returns the validation rules for attributes. @@ -107,7 +73,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * * - attribute list: required, specifies the attributes (separated by commas) to be validated; * - validator type: required, specifies the validator to be used. It can be the name of a model - * class method, the name of a built-in validator, or a validator class (or its path alias). + * class method, the name of a built-in validator, or a validator class name (or its path alias). * - on: optional, specifies the [[scenario|scenarios]] (separated by commas) when the validation * rule can be applied. If this option is not set, the rule will apply to all scenarios. * - additional name-value pairs can be specified to initialize the corresponding validator properties. @@ -145,6 +111,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * merge the parent rules with child rules using functions such as `array_merge()`. * * @return array validation rules + * @see scenarios */ public function rules() { @@ -152,6 +119,56 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Returns a list of scenarios and the corresponding relevant attributes. + * The returned array should be in the following format: + * + * ~~~ + * array( + * 'scenario1' => array('attribute11', 'attribute12', ...), + * 'scenario2' => array('attribute21', 'attribute22', ...), + * ... + * ) + * ~~~ + * + * Attributes relevant to the current scenario are considered safe and can be + * massively assigned. When [[validate()]] is invoked, these attributes will + * be validated using the rules declared in [[rules()]]. + * + * If an attribute should NOT be massively assigned (thus considered unsafe), + * please prefix the attribute with an exclamation character (e.g. '!attribute'). + * + * @return array a list of scenarios and the corresponding relevant attributes. + */ + public function scenarios() + { + return array(); + } + + /** + * Returns the list of attribute names. + * By default, this method returns all public non-static properties of the class. + * You may override this method to change the default behavior. + * @return array list of attribute names. + */ + public function attributes() + { + $className = get_class($this); + if (isset(self::$_attributes[$className])) { + return self::$_attributes[$className]; + } + + $class = new \ReflectionClass($this); + $names = array(); + foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $name = $property->getName(); + if (!$property->isStatic()) { + $names[] = $name; + } + } + return self::$_attributes[$className] = $names; + } + + /** * Returns the attribute labels. * * Attribute labels are mainly used for display purpose. For example, given an attribute @@ -175,30 +192,33 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Performs the data validation. * - * This method executes the validation rules as declared in [[rules()]]. - * Only the rules applicable to the current [[scenario]] will be executed. - * A rule is considered applicable to a scenario if its `on` option is not set - * or contains the scenario. + * This method executes the validation rules applicable to the current [[scenario]]. + * The following criteria are used to determine whether a rule is currently applicable: + * + * - the rule must be associated with the attributes relevant to the current scenario; + * - the rules must be effective for the current scenario. * * This method will call [[beforeValidate()]] and [[afterValidate()]] before and - * after actual validation, respectively. If [[beforeValidate()]] returns false, - * the validation and [[afterValidate()]] will be cancelled. + * after the actual validation, respectively. If [[beforeValidate()]] returns false, + * the validation will be cancelled and [[afterValidate()]] will not be called. * - * Errors found during the validation can be retrieved via [[getErrors()]]. + * Errors found during the validation can be retrieved via [[getErrors()]] + * and [[getError()]]. * * @param array $attributes list of attributes that should be validated. * If this parameter is empty, it means any attribute listed in the applicable * validation rules should be validated. * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation * @return boolean whether the validation is successful without any error. - * @see beforeValidate() - * @see afterValidate() */ public function validate($attributes = null, $clearErrors = true) { if ($clearErrors) { $this->clearErrors(); } + if ($attributes === null) { + $attributes = $this->activeAttributes(); + } if ($this->beforeValidate()) { foreach ($this->getActiveValidators() as $validator) { $validator->validate($this, $attributes); @@ -214,7 +234,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * The default implementation raises a `beforeValidate` event. * You may override this method to do preliminary checks before validation. * Make sure the parent implementation is invoked so that the event can be raised. - * @return boolean whether validation should be executed. Defaults to true. + * @return boolean whether the validation should be executed. Defaults to true. * If false is returned, the validation will stop and the model is considered invalid. */ public function beforeValidate() @@ -269,8 +289,9 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess { $validators = array(); $scenario = $this->getScenario(); + /** @var $validator Validator */ foreach ($this->getValidators() as $validator) { - if ($validator->applyTo($scenario, $attribute)) { + if ($validator->isActive($scenario, $attribute)) { $validators[] = $validator; } } @@ -287,8 +308,10 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess { $validators = new Vector; foreach ($this->rules() as $rule) { - if (isset($rule[0], $rule[1])) { // attributes, validator type - $validator = \yii\validators\Validator::createValidator($rule[1], $this, $rule[0], array_slice($rule, 2)); + if ($rule instanceof Validator) { + $validators->add($rule); + } elseif (isset($rule[0], $rule[1])) { // attributes, validator type + $validator = Validator::createValidator($rule[1], $this, $rule[0], array_slice($rule, 2)); $validators->add($validator); } else { throw new BadConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); @@ -308,7 +331,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess public function isAttributeRequired($attribute) { foreach ($this->getActiveValidators($attribute) as $validator) { - if ($validator instanceof \yii\validators\RequiredValidator) { + if ($validator instanceof RequiredValidator) { return true; } } @@ -322,13 +345,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function isAttributeSafe($attribute) { - $validators = $this->getActiveValidators($attribute); - foreach ($validators as $validator) { - if (!$validator->safe) { - return false; - } - } - return $validators !== array(); + $scenarios = $this->scenarios(); + return in_array($attribute, $scenarios[$this->getScenario()], true); } /** @@ -346,7 +364,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Returns a value indicating whether there is any validation error. - * @param string $attribute attribute name. Use null to check all attributes. + * @param string|null $attribute attribute name. Use null to check all attributes. * @return boolean whether there is any error. */ public function hasErrors($attribute = null) @@ -452,7 +470,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Returns attribute values. * @param array $names list of attributes whose value needs to be returned. - * Defaults to null, meaning all attributes listed in [[attributeNames()]] will be returned. + * Defaults to null, meaning all attributes listed in [[attributes()]] will be returned. * If it is an array, only the attributes in the array will be returned. * @return array attribute values (name=>value). */ @@ -461,13 +479,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess $values = array(); if (is_array($names)) { - foreach ($this->attributeNames() as $name) { + foreach ($this->attributes() as $name) { if (in_array($name, $names, true)) { $values[$name] = $this->$name; } } } else { - foreach ($this->attributeNames() as $name) { + foreach ($this->attributes() as $name) { $values[$name] = $this->$name; } } @@ -480,13 +498,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * @param array $values attribute values (name=>value) to be assigned to the model. * @param boolean $safeOnly whether the assignments should only be done to the safe attributes. * A safe attribute is one that is associated with a validation rule in the current [[scenario]]. - * @see getSafeAttributeNames - * @see attributeNames + * @see safeAttributes() + * @see attributes() */ public function setAttributes($values, $safeOnly = true) { if (is_array($values)) { - $attributes = array_flip($safeOnly ? $this->getSafeAttributeNames() : $this->attributeNames()); + $attributes = array_flip($safeOnly ? $this->safeAttributes() : $this->attributes()); foreach ($values as $name => $value) { if (isset($attributes[$name])) { $this->$name = $value; @@ -517,15 +535,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * Scenario affects how validation is performed and which attributes can * be massively assigned. * - * A validation rule will be performed when calling [[validate()]] - * if its 'on' option is not set or contains the current scenario value. - * - * And an attribute can be massively assigned if it is associated with - * a validation rule for the current scenario. An exception is - * the [[\yii\validators\UnsafeValidator|unsafe]] validator which marks - * the associated attributes as unsafe and not allowed to be massively assigned. - * - * @return string the scenario that this model is in. + * @return string the scenario that this model is in. Defaults to 'default'. */ public function getScenario() { @@ -543,30 +553,35 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** - * Returns the attribute names that are safe to be massively assigned. - * A safe attribute is one that is associated with a validation rule in the current [[scenario]]. + * Returns the attribute names that are safe to be massively assigned in the current scenario. * @return array safe attribute names */ - public function getSafeAttributeNames() + public function safeAttributes() { + $scenarios = $this->scenarios(); $attributes = array(); - $unsafe = array(); - foreach ($this->getActiveValidators() as $validator) { - if (!$validator->safe) { - foreach ($validator->attributes as $name) { - $unsafe[] = $name; - } - } else { - foreach ($validator->attributes as $name) { - $attributes[$name] = true; - } + foreach ($scenarios[$this->getScenario()] as $attribute) { + if ($attribute[0] !== '!') { + $attributes[] = $attribute; } } + return $attributes; + } - foreach ($unsafe as $name) { - unset($attributes[$name]); + /** + * Returns the attribute names that are subject to validation in the current scenario. + * @return array safe attribute names + */ + public function activeAttributes() + { + $scenarios = $this->scenarios(); + $attributes = $scenarios[$this->getScenario()]; + foreach ($attributes as $i => $attribute) { + if ($attribute[0] === '!') { + $attributes[$i] = substr($attribute, 1); + } } - return array_keys($attributes); + return $attributes; } /** diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 88c0e5a..2cf76b5 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -588,7 +588,7 @@ abstract class ActiveRecord extends Model * This would return all column names of the table associated with this AR class. * @return array list of attribute names. */ - public function attributeNames() + public function attributes() { return array_keys($this->getMetaData()->table->columns); } @@ -633,7 +633,7 @@ abstract class ActiveRecord extends Model public function getAttributes($names = null) { if ($names === null) { - $names = $this->attributeNames(); + $names = $this->attributes(); } $values = array(); foreach ($names as $name) { @@ -645,7 +645,7 @@ abstract class ActiveRecord extends Model public function getChangedAttributes($names = null) { if ($names === null) { - $names = $this->attributeNames(); + $names = $this->attributes(); } $names = array_flip($names); $attributes = array(); @@ -931,7 +931,7 @@ abstract class ActiveRecord extends Model return false; } if ($attributes === null) { - foreach ($this->attributeNames() as $name) { + foreach ($this->attributes() as $name) { $this->_attributes[$name] = $record->_attributes[$name]; } $this->_oldAttributes = $this->_attributes; diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index ed8d5a4..5dd2fdb 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -42,8 +42,6 @@ namespace yii\validators; * - `captcha`: [[CaptchaValidator]] * - `default`: [[DefaultValueValidator]] * - `exist`: [[ExistValidator]] - * - `safe`: [[SafeValidator]] - * - `unsafe`: [[UnsafeValidator]] * * @author Qiang Xue * @since 2.0 @@ -58,8 +56,6 @@ abstract class Validator extends \yii\base\Component 'match' => '\yii\validators\RegularExpressionValidator', 'email' => '\yii\validators\EmailValidator', 'url' => '\yii\validators\UrlValidator', - 'safe' => '\yii\validators\SafeValidator', - 'unsafe' => '\yii\validators\UnsafeValidator', 'filter' => '\yii\validators\FilterValidator', 'captcha' => '\yii\validators\CaptchaValidator', 'default' => '\yii\validators\DefaultValueValidator', @@ -103,11 +99,6 @@ abstract class Validator extends \yii\base\Component */ public $skipOnError = true; /** - * @var boolean whether attributes listed with this validator should be considered safe for - * massive assignment. Defaults to true. - */ - public $safe = true; - /** * @var boolean whether to enable client-side validation. Defaults to true. * Please refer to [[\yii\web\ActiveForm::enableClientValidation]] for more details about * client-side validation. @@ -187,8 +178,10 @@ abstract class Validator extends \yii\base\Component /** * Validates the specified object. * @param \yii\base\Model $object the data object being validated - * @param array $attributes the list of attributes to be validated. Defaults to null, - * meaning every attribute listed in [[attributes]] will be validated. + * @param array|null $attributes the list of attributes to be validated. + * Note that if an attribute is not associated with the validator, + * it will be ignored. + * If this parameter is null, every attribute listed in [[attributes]] will be validated. */ public function validate($object, $attributes = null) { @@ -228,10 +221,11 @@ abstract class Validator extends \yii\base\Component } /** - * Returns a value indicating whether the validator applies to the specified scenario. - * A validator applies to a scenario as long as any of the following conditions is met: + * Returns a value indicating whether the validator is active for the given scenario and attribute. + * + * A validator is active if * - * - the validator's `on` property is empty + * - the validator's `on` property is empty, or * - the validator's `on` property contains the specified scenario * * @param string $scenario scenario name @@ -239,7 +233,7 @@ abstract class Validator extends \yii\base\Component * the method will also check if the attribute appears in [[attributes]]. * @return boolean whether the validator applies to the specified scenario. */ - public function applyTo($scenario, $attribute = null) + public function isActive($scenario, $attribute = null) { $applies = !isset($this->except[$scenario]) && (empty($this->on) || isset($this->on[$scenario])); return $attribute === null ? $applies : $applies && in_array($attribute, $this->attributes, true); diff --git a/framework/web/Sort.php b/framework/web/Sort.php index 1b6ba63..12e16a5 100644 --- a/framework/web/Sort.php +++ b/framework/web/Sort.php @@ -429,7 +429,7 @@ class CSort extends CComponent $attributes = $this->attributes; } else { if ($this->modelClass !== null) { - $attributes = CActiveRecord::model($this->modelClass)->attributeNames(); + $attributes = CActiveRecord::model($this->modelClass)->attributes(); } else { return false; } From fee2c998f80a8fc32db50d92335c3d7fd6257722 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 10 Nov 2012 07:17:25 -0800 Subject: [PATCH 3/6] updated the default value of scenarios() --- framework/base/Model.php | 11 ++++++++++- framework/db/ar/ActiveRecord.php | 12 ++++++++++++ framework/db/dao/BaseQuery.php | 2 +- framework/db/dao/DataReader.php | 8 ++++---- framework/db/dao/QueryBuilder.php | 6 +++--- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/framework/base/Model.php b/framework/base/Model.php index a7c3433..1571ae4 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -137,11 +137,20 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * If an attribute should NOT be massively assigned (thus considered unsafe), * please prefix the attribute with an exclamation character (e.g. '!attribute'). * + * WARNING: The default implementation returns the 'default' scenario and the result of + * [[attributes()]]. This means if the model is in 'default' scenario, all + * public member variables can be massively assigned and will be validated when + * calling [[validate()]]. Make sure you override this method if you do not want + * this behavior (e.g. you only want some of the attributes to be massively assigned + * and validated.) + * * @return array a list of scenarios and the corresponding relevant attributes. */ public function scenarios() { - return array(); + return array( + 'default' => $this->attributes(), + ); } /** diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 2cf76b5..3691571 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -594,6 +594,18 @@ abstract class ActiveRecord extends Model } /** + * Returns a list of scenarios and the corresponding relevant attributes. + * Please refer to [[\yii\base\Model::scenarios()]] for more details. + * The implementation here simply returns an empty array. You may override + * this method to return the scenarios that you want to use with this AR class. + * @return array a list of scenarios and the corresponding relevant attributes. + */ + public function scenarios() + { + return array(); + } + + /** * Returns the named attribute value. * If this is a new record and the attribute is not set before, * the default column value will be returned. diff --git a/framework/db/dao/BaseQuery.php b/framework/db/dao/BaseQuery.php index c2d5a2e..fdbfc58 100644 --- a/framework/db/dao/BaseQuery.php +++ b/framework/db/dao/BaseQuery.php @@ -67,7 +67,7 @@ class BaseQuery extends \yii\base\Component public $group; /** * @var string|array how to join with other tables. This refers to the JOIN clause in a SQL statement. - * It can either a string (e.g. `'LEFT JOIN tbl_user ON tbl_user.id=author_id'`) or an array (e.g. + * It can be either a string (e.g. `'LEFT JOIN tbl_user ON tbl_user.id=author_id'`) or an array (e.g. * `array('LEFT JOIN tbl_user ON tbl_user.id=author_id', 'LEFT JOIN tbl_team ON tbl_team.id=team_id')`). * @see join() */ diff --git a/framework/db/dao/DataReader.php b/framework/db/dao/DataReader.php index 91b2ce6..3d744f4 100644 --- a/framework/db/dao/DataReader.php +++ b/framework/db/dao/DataReader.php @@ -94,7 +94,7 @@ class DataReader extends \yii\base\Object implements \Iterator, \Countable /** * Advances the reader to the next row in a result set. - * @return array|false the current row, false if no more row available + * @return array the current row, false if no more row available */ public function read() { @@ -104,7 +104,7 @@ class DataReader extends \yii\base\Object implements \Iterator, \Countable /** * Returns a single column from the next row of a result set. * @param integer $columnIndex zero-based column index - * @return mixed|false the column of the current row, false if no more row available + * @return mixed the column of the current row, false if no more row available */ public function readColumn($columnIndex) { @@ -115,7 +115,7 @@ class DataReader extends \yii\base\Object implements \Iterator, \Countable * Returns an object populated with the next row of data. * @param string $className class name of the object to be created and populated * @param array $fields Elements of this array are passed to the constructor - * @return mixed|false the populated object, false if no more row of data available + * @return mixed the populated object, false if no more row of data available */ public function readObject($className, $fields) { @@ -149,7 +149,7 @@ class DataReader extends \yii\base\Object implements \Iterator, \Countable /** * Closes the reader. * This frees up the resources allocated for executing this SQL statement. - * Read attemps after this method call are unpredictable. + * Read attempts after this method call are unpredictable. */ public function close() { diff --git a/framework/db/dao/QueryBuilder.php b/framework/db/dao/QueryBuilder.php index a2b685f..407179e 100644 --- a/framework/db/dao/QueryBuilder.php +++ b/framework/db/dao/QueryBuilder.php @@ -92,7 +92,7 @@ class QueryBuilder extends \yii\base\Object * * @param string $table the table that new rows will be inserted into. * @param array $columns the column data (name=>value) to be inserted into the table. - * @return integer number of rows affected by the execution. + * @return string the INSERT SQL */ public function insert($table, $columns) { @@ -139,7 +139,7 @@ class QueryBuilder extends \yii\base\Object * @param mixed $condition the condition that will be put in the WHERE part. Please * refer to [[Query::where()]] on how to specify condition. * @param array $params the parameters to be bound to the query. - * @return integer number of rows affected by the execution. + * @return string the UPDATE SQL */ public function update($table, $columns, $condition = '', $params = array()) { @@ -180,7 +180,7 @@ class QueryBuilder extends \yii\base\Object * @param mixed $condition the condition that will be put in the WHERE part. Please * refer to [[Query::where()]] on how to specify condition. * @param array $params the parameters to be bound to the query. - * @return integer number of rows affected by the execution. + * @return string the DELETE SQL */ public function delete($table, $condition = '', $params = array()) { From e828cc6654bedcf702ba502642a404ae46950ede Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 12 Nov 2012 03:17:32 -0800 Subject: [PATCH 4/6] changed the default behavior of scenarios() --- docs/internals/ar.md | 0 docs/internals/database.md | 0 framework/base/Model.php | 77 ++++++++++++++++++++-------------------- framework/db/ar/ActiveRecord.php | 52 +++++++++++++-------------- framework/db/dao/Query.php | 1 + 5 files changed, 65 insertions(+), 65 deletions(-) create mode 100644 docs/internals/ar.md create mode 100644 docs/internals/database.md diff --git a/docs/internals/ar.md b/docs/internals/ar.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/internals/database.md b/docs/internals/database.md new file mode 100644 index 0000000..e69de29 diff --git a/framework/base/Model.php b/framework/base/Model.php index 1571ae4..8756b5c 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -119,7 +119,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** - * Returns a list of scenarios and the corresponding relevant attributes. + * Returns a list of scenarios and the corresponding active attributes. * The returned array should be in the following format: * * ~~~ @@ -130,27 +130,14 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * ) * ~~~ * - * Attributes relevant to the current scenario are considered safe and can be - * massively assigned. When [[validate()]] is invoked, these attributes will - * be validated using the rules declared in [[rules()]]. - * * If an attribute should NOT be massively assigned (thus considered unsafe), * please prefix the attribute with an exclamation character (e.g. '!attribute'). * - * WARNING: The default implementation returns the 'default' scenario and the result of - * [[attributes()]]. This means if the model is in 'default' scenario, all - * public member variables can be massively assigned and will be validated when - * calling [[validate()]]. Make sure you override this method if you do not want - * this behavior (e.g. you only want some of the attributes to be massively assigned - * and validated.) - * * @return array a list of scenarios and the corresponding relevant attributes. */ public function scenarios() { - return array( - 'default' => $this->attributes(), - ); + return array(); } /** @@ -354,8 +341,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function isAttributeSafe($attribute) { - $scenarios = $this->scenarios(); - return in_array($attribute, $scenarios[$this->getScenario()], true); + return in_array($attribute, $this->safeAttributes(), true); } /** @@ -481,22 +467,20 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * @param array $names list of attributes whose value needs to be returned. * Defaults to null, meaning all attributes listed in [[attributes()]] will be returned. * If it is an array, only the attributes in the array will be returned. + * @param array $except list of attributes whose value should NOT be returned. * @return array attribute values (name=>value). */ - public function getAttributes($names = null) + public function getAttributes($names = null, $except = array()) { $values = array(); - - if (is_array($names)) { - foreach ($this->attributes() as $name) { - if (in_array($name, $names, true)) { - $values[$name] = $this->$name; - } - } - } else { - foreach ($this->attributes() as $name) { - $values[$name] = $this->$name; - } + if ($names === null) { + $names = $this->attributes(); + } + foreach ($names as $name) { + $values[$name] = $this->$name; + } + foreach ($except as $name) { + unset($values[$name]); } return $values; @@ -567,14 +551,19 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function safeAttributes() { + $scenario = $this->getScenario(); $scenarios = $this->scenarios(); - $attributes = array(); - foreach ($scenarios[$this->getScenario()] as $attribute) { - if ($attribute[0] !== '!') { - $attributes[] = $attribute; + if (isset($scenarios[$scenario])) { + $attributes = array(); + foreach ($scenarios[$scenario] as $attribute) { + if ($attribute[0] !== '!') { + $attributes[] = $attribute; + } } + return $attributes; + } else { + return $this->activeAttributes(); } - return $attributes; } /** @@ -583,11 +572,23 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function activeAttributes() { + $scenario = $this->getScenario(); $scenarios = $this->scenarios(); - $attributes = $scenarios[$this->getScenario()]; - foreach ($attributes as $i => $attribute) { - if ($attribute[0] === '!') { - $attributes[$i] = substr($attribute, 1); + 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); + } + } + } else { + // use validators to determine active attributes + $attributes = array(); + foreach ($this->attributes() as $attribute) { + if ($this->getActiveValidators($attribue) !== array()) { + $attributes[] = $attribute; + } } } return $attributes; diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 3691571..b3b69e4 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -44,6 +44,10 @@ use yii\util\StringHelper; abstract class ActiveRecord extends Model { /** + * @var ActiveRecord[] global model instances indexed by model class names + */ + private static $_models = array(); + /** * @var array attribute values indexed by attribute names */ private $_attributes = array(); @@ -56,6 +60,21 @@ abstract class ActiveRecord extends Model */ private $_related; + + /** + * Returns a model instance to support accessing non-static methods such as [[table()]], [[primaryKey()]]. + * @return ActiveRecord + */ + public static function model() + { + $className = get_called_class(); + if (isset(self::$_models[$className])) { + return self::$_models[$className]; + } else { + return self::$_models[$className] = new static; + } + } + /** * Returns the metadata for this AR class. * @param boolean $refresh whether to rebuild the metadata. @@ -194,7 +213,7 @@ abstract class ActiveRecord extends Model public static function updateAll($attributes, $condition = '', $params = array()) { $query = new Query; - $query->update(static::tableName(), $attributes, $condition, $params); + $query->update(static::model()->tableName(), $attributes, $condition, $params); return $query->createCommand(static::getDbConnection())->execute(); } @@ -215,7 +234,7 @@ abstract class ActiveRecord extends Model $counters[$name] = new Expression($value >= 0 ? "$quotedName+$value" : "$quotedName$value"); } $query = new Query; - $query->update(static::tableName(), $counters, $condition, $params); + $query->update(static::model()->tableName(), $counters, $condition, $params); return $query->createCommand($db)->execute(); } @@ -229,7 +248,7 @@ abstract class ActiveRecord extends Model public static function deleteAll($condition = '', $params = array()) { $query = new Query; - $query->delete(static::tableName(), $condition, $params); + $query->delete(static::model()->tableName(), $condition, $params); return $query->createCommand(static::getDbConnection())->execute(); } @@ -250,9 +269,9 @@ 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 static function tableName() + public function tableName() { - return StringHelper::camel2id(basename(get_called_class()), '_'); + return StringHelper::camel2id(basename(get_class($this)), '_'); } /** @@ -266,7 +285,7 @@ abstract class ActiveRecord extends Model * If the key is a composite one consisting of several columns, it should * return the array of the key column names. */ - public static function primaryKey() + public function primaryKey() { } @@ -633,27 +652,6 @@ abstract class ActiveRecord extends Model $this->_attributes[$name] = $value; } - /** - * Returns all column attribute values. - * Note, related objects are not returned. - * @param null|array $names names of attributes whose value needs to be returned. - * If this is true (default), then all attribute values will be returned, including - * those that are not loaded from DB (null will be returned for those attributes). - * If this is null, all attributes except those that are not loaded from DB will be returned. - * @return array attribute values indexed by attribute names. - */ - public function getAttributes($names = null) - { - if ($names === null) { - $names = $this->attributes(); - } - $values = array(); - foreach ($names as $name) { - $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; - } - return $values; - } - public function getChangedAttributes($names = null) { if ($names === null) { diff --git a/framework/db/dao/Query.php b/framework/db/dao/Query.php index 862154c..0769f92 100644 --- a/framework/db/dao/Query.php +++ b/framework/db/dao/Query.php @@ -75,6 +75,7 @@ class Query extends BaseQuery $qb->query = $this; return call_user_func_array(array($qb, $method), $params); } else { + /** @var $qb QueryBuilder */ return $qb->build($this); } } From 794df81628617c27679d0d62cd1070d0ffd01038 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 12 Nov 2012 03:47:24 -0800 Subject: [PATCH 5/6] renamed order and group to orderBy and groupBy --- framework/db/ar/ActiveFinder.php | 20 +++++++++--------- framework/db/ar/ActiveRecord.php | 2 +- framework/db/dao/BaseQuery.php | 44 +++++++++++++++++++-------------------- framework/db/dao/QueryBuilder.php | 4 ++-- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/framework/db/ar/ActiveFinder.php b/framework/db/ar/ActiveFinder.php index e1be9bb..3334d79 100644 --- a/framework/db/ar/ActiveFinder.php +++ b/framework/db/ar/ActiveFinder.php @@ -467,21 +467,21 @@ class ActiveFinder extends \yii\base\Object } } - if ($element->query->order !== null) { - if (!is_array($element->query->order)) { - $element->query->order = preg_split('/\s*,\s*/', trim($element->query->order), -1, PREG_SPLIT_NO_EMPTY); + if ($element->query->orderBy !== null) { + if (!is_array($element->query->orderBy)) { + $element->query->orderBy = preg_split('/\s*,\s*/', trim($element->query->orderBy), -1, PREG_SPLIT_NO_EMPTY); } - foreach ($element->query->order as $order) { - $query->order[] = strtr($order, $prefixes); + foreach ($element->query->orderBy as $order) { + $query->orderBy[] = strtr($order, $prefixes); } } - if ($element->query->group !== null) { - if (!is_array($element->query->group)) { - $element->query->group = preg_split('/\s*,\s*/', trim($element->query->group), -1, PREG_SPLIT_NO_EMPTY); + if ($element->query->groupBy !== null) { + if (!is_array($element->query->groupBy)) { + $element->query->groupBy = preg_split('/\s*,\s*/', trim($element->query->groupBy), -1, PREG_SPLIT_NO_EMPTY); } - foreach ($element->query->group as $group) { - $query->group[] = strtr($group, $prefixes); + foreach ($element->query->groupBy as $group) { + $query->groupBy[] = strtr($group, $prefixes); } } diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index b3b69e4..f8370d0 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -71,7 +71,7 @@ abstract class ActiveRecord extends Model if (isset(self::$_models[$className])) { return self::$_models[$className]; } else { - return self::$_models[$className] = new static; + return self::$_models[$className] = new static; } } diff --git a/framework/db/dao/BaseQuery.php b/framework/db/dao/BaseQuery.php index fdbfc58..a9a8896 100644 --- a/framework/db/dao/BaseQuery.php +++ b/framework/db/dao/BaseQuery.php @@ -59,12 +59,12 @@ class BaseQuery extends \yii\base\Component * @var string|array how to sort the query results. This refers to the ORDER BY clause in a SQL statement. * It can be either a string (e.g. `'id ASC, name DESC'`) or an array (e.g. `array('id ASC', 'name DESC')`). */ - public $order; + public $orderBy; /** * @var string|array how to group the query results. This refers to the GROUP BY clause in a SQL statement. * It can be either a string (e.g. `'company, department'`) or an array (e.g. `array('company', 'department')`). */ - public $group; + public $groupBy; /** * @var string|array how to join with other tables. This refers to the JOIN clause in a SQL statement. * It can be either a string (e.g. `'LEFT JOIN tbl_user ON tbl_user.id=author_id'`) or an array (e.g. @@ -330,9 +330,9 @@ class BaseQuery extends \yii\base\Component * @return BaseQuery the query object itself * @see addGroup() */ - public function group($columns) + public function groupBy($columns) { - $this->group = $columns; + $this->groupBy = $columns; return $this; } @@ -347,16 +347,16 @@ class BaseQuery extends \yii\base\Component */ public function addGroup($columns) { - if (empty($this->group)) { - $this->group = $columns; + if (empty($this->groupBy)) { + $this->groupBy = $columns; } else { - if (!is_array($this->group)) { - $this->group = preg_split('/\s*,\s*/', trim($this->group), -1, PREG_SPLIT_NO_EMPTY); + if (!is_array($this->groupBy)) { + $this->groupBy = preg_split('/\s*,\s*/', trim($this->groupBy), -1, PREG_SPLIT_NO_EMPTY); } if (!is_array($columns)) { $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); } - $this->group = array_merge($this->group, $columns); + $this->groupBy = array_merge($this->groupBy, $columns); } return $this; } @@ -428,9 +428,9 @@ class BaseQuery extends \yii\base\Component * @return BaseQuery the query object itself * @see addOrder() */ - public function order($columns) + public function orderBy($columns) { - $this->order = $columns; + $this->orderBy = $columns; return $this; } @@ -443,18 +443,18 @@ class BaseQuery extends \yii\base\Component * @return BaseQuery the query object itself * @see order() */ - public function addOrder($columns) + public function addOrderBy($columns) { - if (empty($this->order)) { - $this->order = $columns; + if (empty($this->orderBy)) { + $this->orderBy = $columns; } else { - if (!is_array($this->order)) { - $this->order = preg_split('/\s*,\s*/', trim($this->order), -1, PREG_SPLIT_NO_EMPTY); + if (!is_array($this->orderBy)) { + $this->orderBy = preg_split('/\s*,\s*/', trim($this->orderBy), -1, PREG_SPLIT_NO_EMPTY); } if (!is_array($columns)) { $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); } - $this->order = array_merge($this->order, $columns); + $this->orderBy = array_merge($this->orderBy, $columns); } return $this; } @@ -540,7 +540,7 @@ class BaseQuery extends \yii\base\Component * takes precedence over this query. * - [[where]], [[having]]: the new query's corresponding property value * will be 'AND' together with the existing one. - * - [[params]], [[order]], [[group]], [[join]], [[union]]: the new query's + * - [[params]], [[orderBy]], [[groupBy]], [[join]], [[union]]: the new query's * corresponding property value will be appended to the existing one. * * In general, the merging makes the resulting query more restrictive and specific. @@ -591,12 +591,12 @@ class BaseQuery extends \yii\base\Component $this->addParams($query->params); } - if ($query->order !== null) { - $this->addOrder($query->order); + if ($query->orderBy !== null) { + $this->addOrderBy($query->orderBy); } - if ($query->group !== null) { - $this->addGroup($query->group); + if ($query->groupBy !== null) { + $this->addGroup($query->groupBy); } if ($query->join !== null) { diff --git a/framework/db/dao/QueryBuilder.php b/framework/db/dao/QueryBuilder.php index 407179e..bc5845a 100644 --- a/framework/db/dao/QueryBuilder.php +++ b/framework/db/dao/QueryBuilder.php @@ -69,10 +69,10 @@ class QueryBuilder extends \yii\base\Object $this->buildFrom($query->from), $this->buildJoin($query->join), $this->buildWhere($query->where), - $this->buildGroup($query->group), + $this->buildGroup($query->groupBy), $this->buildHaving($query->having), $this->buildUnion($query->union), - $this->buildOrder($query->order), + $this->buildOrder($query->orderBy), $this->buildLimit($query->limit, $query->offset), ); return implode($this->separator, array_filter($clauses)); From 377181a0082d4f378bc49b17638dd71185cf9556 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 12 Nov 2012 20:31:23 -0500 Subject: [PATCH 6/6] new AR WIP --- docs/internals/ar.md | 15 + docs/internals/database.md | 27 ++ docs/model.md | 178 +++++++ framework/base/Model.php | 4 +- framework/db/ar/ActiveMetaData.php | 19 +- framework/db/ar/ActiveQuery.php | 336 +++++++++++++- framework/db/ar/ActiveRecord.php | 264 ++--------- framework/db/dao/Connection.php | 11 + tests/unit/data/ar/Customer.php | 17 +- tests/unit/data/ar/Item.php | 8 +- tests/unit/data/ar/Order.php | 50 +- tests/unit/data/ar/OrderItem.php | 18 +- tests/unit/framework/db/ar/ActiveRecordTest.php | 594 +++++++++++++----------- tests/unit/framework/db/dao/QueryTest.php | 20 +- 14 files changed, 1003 insertions(+), 558 deletions(-) diff --git a/docs/internals/ar.md b/docs/internals/ar.md index e69de29..c493269 100644 --- a/docs/internals/ar.md +++ b/docs/internals/ar.md @@ -0,0 +1,15 @@ +ActiveRecord +============ + +Query +----- + +### Basic Queries + + + +### Relational Queries + +### Scopes + + diff --git a/docs/internals/database.md b/docs/internals/database.md index e69de29..c137035 100644 --- a/docs/internals/database.md +++ b/docs/internals/database.md @@ -0,0 +1,27 @@ +Working with Database +===================== + +Architecture +------------ + +### Data Access Object (DAO) + +* Connection +* Command +* DataReader +* Transaction + +### Schema + +* TableSchema +* ColumnSchema + +### Query Builder + +* Query +* QueryBuilder + +### ActiveRecord + +* ActiveRecord +* ActiveQuery \ No newline at end of file diff --git a/docs/model.md b/docs/model.md index ca823ff..e9e0d62 100644 --- a/docs/model.md +++ b/docs/model.md @@ -1,6 +1,184 @@ Model ===== +Attributes +---------- + +Attributes store the actual data represented by a model and can +be accessed like object member variables. For example, a `Post` model +may contain a `title` attribute and a `content` attribute which may be +accessed as follows, + +~~~php +$post->title = 'Hello, world'; +$post->content = 'Something interesting is happening'; +echo $post->title; +echo $post->content; +~~~ + +A model should list all its available attributes in the `attributes()` method. + +Attributes may be implemented in various ways. The [[\yii\base\Model]] class +implements attributes as public member variables of the class, while the +[[\yii\db\ar\ActiveRecord]] class implements them as DB table columns. For example, + +~~~php +// LoginForm has two attributes: username and password +class LoginForm extends \yii\base\Model +{ + public $username; + public $password; +} + +// Post is associated with the tbl_post DB table. +// Its attributes correspond to the columns in tbl_post +class Post extends \yii\db\ar\ActiveRecord +{ + public function table() + { + return 'tbl_post'; + } +} +~~~ + + +### Attribute Labels + + +Scenarios +--------- + +A model may be used in different scenarios. For example, a `User` model may be +used to collect user login inputs, and it may also be used for user registration +purpose. For this reason, each model has a property named `scenario` which stores +the name of the scenario that the model is currently being used. As we will explain +in the next few sections, the concept of scenario is mainly used in validation and +massive attribute assignment. + +Associated with each scenario is a list of attributes that are *active* in that +particular scenario. For example, in the `login` scenario, only the `username` +and `password` attributes are active; while in the `register` scenario, +additional attributes such as `email` are *active*. + +Possible scenarios should be listed in the `scenarios()` method which returns an array +whose keys are the scenario names and whose values are the corresponding +active attribute lists. Below is an example: + +~~~php +class User extends \yii\db\ar\ActiveRecord +{ + public function table() + { + return 'tbl_user'; + } + + public function scenarios() + { + return array( + 'login' => array('username', 'password'), + 'register' => array('username', 'email', 'password'), + ); + } +} +~~~ + +Sometimes, we want to mark that an attribute is not safe for massive assignment +(but we still want it to be validated). We may do so by prefixing an exclamation +character to the attribute name when declaring it in `scenarios()`. For example, + +~~~php +array('username', 'password', '!secret') +~~~ + + +Validation +---------- + +When a model is used to collect user input data via its attributes, +it usually needs to validate the affected attributes to make sure they +satisfy certain requirements, such as an attribute cannot be empty, +an attribute must contain letters only, etc. If errors are found in +validation, they may be presented to the user to help him fix the errors. +The following example shows how the validation is performed: + +~~~php +$model = new LoginForm; +$model->username = $_POST['username']; +$model->password = $_POST['password']; +if ($model->validate()) { + // ...login the user... +} else { + $errors = $model->getErrors(); + // ...display the errors to the end user... +} +~~~ + +The possible validation rules for a model should be listed in its +`rules()` method. Each validation rule applies to one or several attributes +and is effective in one or several scenarios. A rule can be specified +using a validator object - an instance of a [[\yii\validators\Validator]] +child class, or an array with the following format: + +~~~php +array( + 'attribute1, attribute2, ...', + 'validator class or alias', + // specifies in which scenario(s) this rule is active. + // if not given, it means it is active in all scenarios + 'on' => 'scenario1, scenario2, ...', + // the following name-value pairs will be used + // to initialize the validator properties... + 'name1' => 'value1', + 'name2' => 'value2', + .... +) +~~~ + +When `validate()` is called, the actual validation rules executed are +determined using both of the following criteria: + +* the rules must be associated with at least one active attribute; +* the rules must be active for the current scenario. + + +### Active Attributes + +An attribute is *active* if it is subject to some validations in the current scenario. + + +### Safe Attributes + +An attribute is *safe* if it can be massively assigned in the current scenario. + + +Massive Access of Attributes +---------------------------- + + +Massive Attribute Retrieval +--------------------------- + +Attributes can be massively retrieved via the `attributes` property. +The following code will return *all* attributes in the `$post` model +as an array of name-value pairs. + +~~~php +$attributes = $post->attributes; +var_dump($attributes); +~~~ + + +Massive Attribute Assignment +---------------------------- + + + + +Safe Attributes +--------------- + +Safe attributes are those that can be massively assigned. For example, + Validation rules and mass assignment ------------------------------------ diff --git a/framework/base/Model.php b/framework/base/Model.php index 8756b5c..731c4f6 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -586,7 +586,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess // use validators to determine active attributes $attributes = array(); foreach ($this->attributes() as $attribute) { - if ($this->getActiveValidators($attribue) !== array()) { + if ($this->getActiveValidators($attribute) !== array()) { $attributes[] = $attribute; } } @@ -614,7 +614,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function offsetExists($offset) { - return property_exists($this, $offset) && $this->$offset !== null; + return $this->$offset !== null; } /** diff --git a/framework/db/ar/ActiveMetaData.php b/framework/db/ar/ActiveMetaData.php index 74ce6cb..0ff2e41 100644 --- a/framework/db/ar/ActiveMetaData.php +++ b/framework/db/ar/ActiveMetaData.php @@ -26,9 +26,9 @@ class ActiveMetaData */ public $modelClass; /** - * @var array list of relations + * @var ActiveRecord the model instance that can be used to access non-static methods */ - public $relations = array(); + public $model; /** * Returns an instance of ActiveMetaData for the specified model class. @@ -55,21 +55,18 @@ class ActiveMetaData public function __construct($modelClass) { $this->modelClass = $modelClass; - $tableName = $modelClass::tableName(); - $this->table = $modelClass::getDbConnection()->getDriver()->getTableSchema($tableName); + $this->model = new $modelClass; + $tableName = $this->model->tableName(); + $this->table = $this->model->getDbConnection()->getDriver()->getTableSchema($tableName); if ($this->table === null) { throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'."); } - $primaryKey = $modelClass::primaryKey(); - if ($primaryKey !== null) { + $primaryKey = $this->model->primaryKey(); + if ($primaryKey !== $this->table->primaryKey) { $this->table->fixPrimaryKey($primaryKey); - } elseif ($this->table->primaryKey === null) { + } elseif ($primaryKey === null) { throw new Exception("The table '$tableName' for ActiveRecord class '$modelClass' does not have a primary key."); } - - foreach ($modelClass::relations() as $name => $config) { - $this->addRelation($name, $config); - } } /** diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php index ce7291b..01c7037 100644 --- a/framework/db/ar/ActiveQuery.php +++ b/framework/db/ar/ActiveQuery.php @@ -10,10 +10,310 @@ namespace yii\db\ar; +use yii\db\dao\BaseQuery; use yii\base\VectorIterator; use yii\db\dao\Expression; use yii\db\Exception; +class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, \Countable +{ + /** + * @var string the name of the ActiveRecord class. + */ + public $modelClass; + /** + * @var array list of relations that this query should be performed with + */ + public $with; + /** + * @var string the name of the column that the result should be indexed by. + * This is only useful when the query result is returned as an array. + */ + public $index; + /** + * @var boolean whether to return each record as an array. If false (default), an object + * of [[modelClass]] will be created to represent each record. + */ + public $asArray; + /** + * @var array list of scopes that should be applied to this query + */ + public $scopes; + /** + * @var string the SQL statement to be executed for retrieving AR records. + * This is set by [[ActiveRecord::findBySql()]]. + */ + public $sql; + /** + * @var array list of query results. Depending on [[asArray]], this can be either + * an array of AR objects (when [[asArray]] is false) or an array of array + * (when [[asArray]] is true). + */ + public $records; + + /** + * @param string $modelClass the name of the ActiveRecord class. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($modelClass, $config = array()) + { + $this->modelClass = $modelClass; + parent::__construct($config); + } + + public function __call($name, $params) + { + if (method_exists($this->modelClass, $name)) { + $this->scopes[$name] = $params; + return $this; + } else { + return parent::__call($name, $params); + } + } + + /** + * Executes query and returns all results as an array. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all() + { + return $this->findRecords(); + } + + /** + * Executes query and returns a single row of result. + * @return null|array|ActiveRecord the single row of query result. Depending on the setting of [[asArray]], + * the query result may be either an array or an ActiveRecord object. Null will be returned + * if the query results in nothing. + */ + public function one() + { + $this->limit = 1; + $records = $this->findRecords(); + return isset($records[0]) ? $records[0] : null; + } + + /** + * Returns a scalar value for this query. + * The value returned will be the first column in the first row of the query results. + * @return string|boolean the value of the first column in the first row of the query result. + * False is returned if there is no value. + */ + public function value() + { + return $this->createFinder()->find($this, true); + } + + /** + * Executes query and returns if matching row exists in the table. + * @return bool if row exists in the table. + */ + public function exists() + { + return $this->select(array(new Expression('1')))->value() !== false; + } + + /** + * Returns the database connection used by this query. + * This method returns the connection used by the [[modelClass]]. + * @return \yii\db\dao\Connection the database connection used by this query + */ + public function getDbConnection() + { + $class = $this->modelClass; + return $class::getDbConnection(); + } + + /** + * Returns the number of items in the vector. + * @return integer the number of items in the vector + */ + public function getCount() + { + return $this->count(); + } + + /** + * Sets the parameters about query caching. + * This is a shortcut method to {@link CDbConnection::cache()}. + * It changes the query caching parameter of the {@link dbConnection} instance. + * @param integer $duration the number of seconds that query results may remain valid in cache. + * If this is 0, the caching will be disabled. + * @param \yii\caching\Dependency $dependency the dependency that will be used when saving the query results into cache. + * @param integer $queryCount number of SQL queries that need to be cached after calling this method. Defaults to 1, + * meaning that the next SQL query will be cached. + * @return ActiveRecord the active record instance itself. + */ + public function cache($duration, $dependency = null, $queryCount = 1) + { + $this->getDbConnection()->cache($duration, $dependency, $queryCount); + return $this; + } + + /** + * Returns an iterator for traversing the items in the vector. + * This method is required by the SPL interface `IteratorAggregate`. + * It will be implicitly called when you use `foreach` to traverse the vector. + * @return VectorIterator an iterator for traversing the items in the vector. + */ + public function getIterator() + { + if ($this->records === null) { + $this->records = $this->findRecords(); + } + return new VectorIterator($this->records); + } + + /** + * Returns the number of items in the vector. + * This method is required by the SPL `Countable` interface. + * It will be implicitly called when you use `count($vector)`. + * @return integer number of items in the vector. + */ + public function count() + { + if ($this->records === null) { + $this->records = $this->findRecords(); + } + return count($this->records); + } + + /** + * Returns a value indicating whether there is an item at the specified offset. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `isset($vector[$offset])`. + * @param integer $offset the offset to be checked + * @return boolean whether there is an item at the specified offset. + */ + public function offsetExists($offset) + { + if ($this->records === null) { + $this->records = $this->findRecords(); + } + return isset($this->records[$offset]); + } + + /** + * Returns the item at the specified offset. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$value = $vector[$offset];`. + * This is equivalent to [[itemAt]]. + * @param integer $offset the offset to retrieve item. + * @return ActiveRecord the item at the offset + * @throws Exception if the offset is out of range + */ + public function offsetGet($offset) + { + if ($this->records === null) { + $this->records = $this->findRecords(); + } + return isset($this->records[$offset]) ? $this->records[$offset] : null; + } + + /** + * Sets the item at the specified offset. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `$vector[$offset] = $item;`. + * If the offset is null or equal to the number of the existing items, + * the new item will be appended to the vector. + * Otherwise, the existing item at the offset will be replaced with the new item. + * @param integer $offset the offset to set item + * @param ActiveRecord $item the item value + * @throws Exception if the offset is out of range, or the vector is read only. + */ + public function offsetSet($offset, $item) + { + if ($this->records === null) { + $this->records = $this->findRecords(); + } + $this->records[$offset] = $item; + } + + /** + * Unsets the item at the specified offset. + * This method is required by the SPL interface `ArrayAccess`. + * It is implicitly called when you use something like `unset($vector[$offset])`. + * This is equivalent to [[removeAt]]. + * @param integer $offset the offset to unset item + * @throws Exception if the offset is out of range, or the vector is read only. + */ + public function offsetUnset($offset) + { + if ($this->records === null) { + $this->records = $this->findRecords(); + } + unset($this->records[$offset]); + } + + public function find() + { + /** + * find the primary ARs + * for each child relation + * find the records filtered by the PK constraints + * populate primary ARs with the related records + * recursively call this metod again + */ + } + + protected function findByParent($parent) + { + + } + + protected function findRecords() + { + if (!empty($this->with)) { + return $this->findWithRelations(); + } + + if ($this->sql === null) { + if ($this->from === null) { + $modelClass = $this->modelClass; + $tableName = $modelClass::model()->getTableSchema()->name; + $this->from = array($tableName); + } + $this->sql = $this->connection->getQueryBuilder()->build($this); + } + $command = $this->connection->createCommand($this->sql, $this->params); + $rows = $command->queryAll(); + return $this->createRecords($rows); + } + + protected function findWithRelations() + { + $records = $this->findRecords(); + } + + 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; + } +} + + + /** * 1. eager loading, base limited and has has_many relations * 2. @@ -24,7 +324,7 @@ use yii\db\Exception; * @author Qiang Xue * @since 2.0 */ -class ActiveQuery extends BaseActiveQuery implements \IteratorAggregate, \ArrayAccess, \Countable +class ActiveQuery2 extends BaseActiveQuery implements \IteratorAggregate, \ArrayAccess, \Countable { /** * @var string the SQL statement to be executed to retrieve primary records. @@ -244,4 +544,38 @@ class ActiveQuery extends BaseActiveQuery implements \IteratorAggregate, \ArrayA { return new ActiveFinder($this->getDbConnection()); } + + public function asArray($value = true) + { + $this->asArray = $value; + return $this; + } + + public function with() + { + $this->with = func_get_args(); + if (isset($this->with[0]) && is_array($this->with[0])) { + // the parameter is given as an array + $this->with = $this->with[0]; + } + return $this; + } + + public function index($column) + { + $this->index = $column; + return $this; + } + + public function tableAlias($value) + { + $this->tableAlias = $value; + return $this; + } + + public function scopes($names) + { + $this->scopes = $names; + return $this; + } } diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index f8370d0..5162c41 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -76,16 +76,6 @@ abstract class ActiveRecord extends Model } /** - * Returns the metadata for this AR class. - * @param boolean $refresh whether to rebuild the metadata. - * @return ActiveMetaData the meta for this AR class. - */ - public static function getMetaData($refresh = false) - { - return ActiveMetaData::getInstance(get_called_class(), $refresh); - } - - /** * Returns the database connection used by this AR class. * By default, the "db" application component is used as the database connection. * You may override this method if you want to use a different database connection. @@ -114,12 +104,12 @@ abstract class ActiveRecord extends Model * // find all active customers and order them by their age: * $customers = Customer::find() * ->where(array('status' => 1)) - * ->order('age') + * ->orderBy('age') * ->all(); * // or alternatively: * $customers = Customer::find(array( * 'where' => array('status' => 1), - * 'order' => 'age', + * 'orderBy' => 'age', * ))->all(); * ~~~ * @@ -141,14 +131,14 @@ abstract class ActiveRecord extends Model } } elseif ($q !== null) { // query by primary key - $primaryKey = static::getMetaData()->table->primaryKey; + $primaryKey = static::model()->primaryKey(); return $query->where(array($primaryKey[0] => $q))->one(); } return $query; } /** - * Creates an [[ActiveQuery]] instance and query by a given SQL statement. + * Creates an [[ActiveQuery]] instance and queries by a given SQL statement. * Note that because the SQL statement is already specified, calling further * query methods (such as `where()`, `order()`) on [[ActiveQuery]] will have no effect. * Methods such as `with()`, `asArray()` can still be called though. @@ -183,10 +173,12 @@ abstract class ActiveRecord extends Model * echo Customer::count('COUNT(DISTINCT age)')->value(); * ~~~ * - * @param array $q the query configuration. This should be an array of name-value pairs. - * It will be used to configure the [[ActiveQuery]] object for query purpose. + * @param array|string $q the query option. This can be one of the followings: * - * @return integer the counting result + * - an array of name-value pairs: it will be used to configure the [[ActiveQuery]] object. + * - a string: the count expression, e.g. 'COUNT(DISTINCT age)'. + * + * @return ActiveQuery the [[ActiveQuery]] instance */ public static function count($q = null) { @@ -195,11 +187,12 @@ abstract class ActiveRecord extends Model foreach ($q as $name => $value) { $query->$name = $value; } - } - if ($query->select === null) { + } elseif ($q !== null) { + $query->select = array($q); + } elseif ($query->select === null) { $query->select = array('COUNT(*)'); } - return $query->value(); + return $query; } /** @@ -275,122 +268,26 @@ abstract class ActiveRecord extends Model } /** - * Declares the primary key name for this AR class. - * This method is meant to be overridden in case when the table has no primary key defined - * (for some legacy database). If the table already has a primary key, - * you do not need to override this method. The default implementation simply returns null, - * meaning using the primary key defined in the database table. - * @return string|array the primary key of the associated database table. - * If the key is a single column, it should return the column name; - * If the key is a composite one consisting of several columns, it should - * return the array of the key column names. + * 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 primaryKey() + public function getTableSchema() { + return $this->getDbConnection()->getTableSchema($this->tableName()); } /** - * Declares the relations for this AR class. - * - * Child classes may override this method to specify their relations. - * - * The following code shows how to declare relations for a `Programmer` AR class: - * - * ~~~ - * return array( - * 'manager:Manager' => '@.id = ?.manager_id', - * 'assignments:Assignment[]' => array( - * 'on' => '@.owner_id = ?.id AND @.status = 1', - * 'order' => '@.create_time DESC', - * ), - * 'projects:Project[]' => array( - * 'via' => 'assignments', - * 'on' => '@.id = ?.project_id', - * ), - * ); - * ~~~ - * - * This method should be overridden to declare related objects. - * - * There are four types of relations that may exist between two active record objects: - *
    - *
  • BELONGS_TO: e.g. a member belongs to a team;
  • - *
  • HAS_ONE: e.g. a member has at most one profile;
  • - *
  • HAS_MANY: e.g. a team has many members;
  • - *
  • MANY_MANY: e.g. a member has many skills and a skill belongs to a member.
  • - *
- * - * Besides the above relation types, a special relation called STAT is also supported - * that can be used to perform statistical query (or aggregational query). - * It retrieves the aggregational information about the related objects, such as the number - * of comments for each post, the average rating for each product, etc. - * - * Each kind of related objects is defined in this method as an array with the following elements: - *
-	 * 'varName'=>array('relationType', 'className', 'foreign_key', ...additional options)
-	 * 
- * where 'varName' refers to the name of the variable/property that the related object(s) can - * be accessed through; 'relationType' refers to the type of the relation, which can be one of the - * following four constants: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY and self::MANY_MANY; - * 'className' refers to the name of the active record class that the related object(s) is of; - * and 'foreign_key' states the foreign key that relates the two kinds of active record. - * Note, for composite foreign keys, they must be listed together, separated by commas; - * and for foreign keys used in MANY_MANY relation, the joining table must be declared as well - * (e.g. 'join_table(fk1, fk2)'). - * - * Additional options may be specified as name-value pairs in the rest array elements: - *
    - *
  • 'select': string|array, a list of columns to be selected. Defaults to '*', meaning all columns. - * Column names should be disambiguated if they appear in an expression (e.g. COUNT(relationName.name) AS name_count).
  • - *
  • 'condition': string, the WHERE clause. Defaults to empty. Note, column references need to - * be disambiguated with prefix 'relationName.' (e.g. relationName.age>20)
  • - *
  • 'order': string, the ORDER BY clause. Defaults to empty. Note, column references need to - * be disambiguated with prefix 'relationName.' (e.g. relationName.age DESC)
  • - *
  • 'with': string|array, a list of child related objects that should be loaded together with this object. - * Note, this is only honored by lazy loading, not eager loading.
  • - *
  • 'joinType': type of join. Defaults to 'LEFT OUTER JOIN'.
  • - *
  • 'alias': the alias for the table associated with this relationship. - * This option has been available since version 1.0.1. It defaults to null, - * meaning the table alias is the same as the relation name.
  • - *
  • 'params': the parameters to be bound to the generated SQL statement. - * This should be given as an array of name-value pairs. This option has been - * available since version 1.0.3.
  • - *
  • 'on': the ON clause. The condition specified here will be appended - * to the joining condition using the AND operator. This option has been - * available since version 1.0.2.
  • - *
  • 'index': the name of the column whose values should be used as keys - * of the array that stores related objects. This option is only available to - * HAS_MANY and MANY_MANY relations. This option has been available since version 1.0.7.
  • - *
  • 'scopes': scopes to apply. In case of a single scope can be used like 'scopes'=>'scopeName', - * in case of multiple scopes can be used like 'scopes'=>array('scopeName1','scopeName2'). - * This option has been available since version 1.1.9.
  • - *
- * - * The following options are available for certain relations when lazy loading: - *
    - *
  • 'group': string, the GROUP BY clause. Defaults to empty. Note, column references need to - * be disambiguated with prefix 'relationName.' (e.g. relationName.age). This option only applies to HAS_MANY and MANY_MANY relations.
  • - *
  • 'having': string, the HAVING clause. Defaults to empty. Note, column references need to - * be disambiguated with prefix 'relationName.' (e.g. relationName.age). This option only applies to HAS_MANY and MANY_MANY relations.
  • - *
  • 'limit': limit of the rows to be selected. This option does not apply to BELONGS_TO relation.
  • - *
  • 'offset': offset of the rows to be selected. This option does not apply to BELONGS_TO relation.
  • - *
  • 'through': name of the model's relation that will be used as a bridge when getting related data. Can be set only for HAS_ONE and HAS_MANY. This option has been available since version 1.1.7.
  • - *
- * - * Below is an example declaring related objects for 'Post' active record class: - *
-	 * return array(
-	 *	 'author'=>array(self::BELONGS_TO, 'User', 'author_id'),
-	 *	 'comments'=>array(self::HAS_MANY, 'Comment', 'post_id', 'with'=>'author', 'order'=>'create_time DESC'),
-	 *	 'tags'=>array(self::MANY_MANY, 'Tag', 'post_tag(post_id, tag_id)', 'order'=>'name'),
-	 * );
-	 * 
- * - * @return array list of related object declarations. Defaults to empty array. + * Returns the primary keys for this AR class. + * The default implementation will return the primary keys as declared + * in the DB table that is associated with this AR class. + * If the DB table does not declare any primary key, you should override + * this method to return the attributes that you want to use as primary keys + * for this AR class. + * @return string[] the primary keys of the associated database table. */ - public static function relations() + public function primaryKey() { - return array(); + return $this->getTableSchema()->primaryKey; } /** @@ -408,42 +305,6 @@ abstract class ActiveRecord extends Model } /** - * Returns the declaration of named scopes. - * A named scope represents a query criteria that can be chained together with - * other named scopes and applied to a query. This method should be overridden - * by child classes to declare named scopes for the particular AR classes. - * For example, the following code declares two named scopes: 'recently' and - * 'published'. - *
-	 * return array(
-	 *	 'published'=>array(
-	 *		   'condition'=>'status=1',
-	 *	 ),
-	 *	 'recently'=>array(
-	 *		   'order'=>'create_time DESC',
-	 *		   'limit'=>5,
-	 *	 ),
-	 * );
-	 * 
- * If the above scopes are declared in a 'Post' model, we can perform the following - * queries: - *
-	 * $posts=Post::model()->published()->findAll();
-	 * $posts=Post::model()->published()->recently()->findAll();
-	 * $posts=Post::model()->published()->with('comments')->findAll();
-	 * 
- * Note that the last query is a relational query. - * - * @return array the scope definition. The array keys are scope names; the array - * values are the corresponding scope definitions. Each scope definition is represented - * as an array whose keys must be properties of {@link CDbCriteria}. - */ - public static function scopes() - { - return array(); - } - - /** * PHP getter magic method. * This method is overridden so that attributes and related objects can be accessed like properties. * @param string $name property name @@ -455,13 +316,13 @@ abstract class ActiveRecord extends Model if (isset($this->_attributes[$name])) { return $this->_attributes[$name]; } - $md = $this->getMetaData(); - if (isset($md->table->columns[$name])) { + if (isset($this->getTableSchema()->columns[$name])) { return null; - } elseif (isset($md->relations[$name])) { + } 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]); } } @@ -476,10 +337,9 @@ abstract class ActiveRecord extends Model */ public function __set($name, $value) { - $md = $this->getMetaData(); - if (isset($md->table->columns[$name])) { + if (isset($this->getTableSchema()->columns[$name])) { $this->_attributes[$name] = $value; - } elseif (isset($md->relations[$name])) { + } elseif (method_exists($this, $name)) { $this->_related[$name] = $value; } else { parent::__set($name, $value); @@ -498,8 +358,7 @@ abstract class ActiveRecord extends Model if (isset($this->_attributes[$name]) || isset($this->_related[$name])) { return true; } - $md = $this->getMetaData(); - if (isset($md->table->columns[$name]) || isset($md->relations[$name])) { + if (isset($this->getTableSchema()->columns[$name]) || method_exists($this, $name)) { return false; } else { return parent::__isset($name); @@ -514,10 +373,9 @@ abstract class ActiveRecord extends Model */ public function __unset($name) { - $md = $this->getMetaData(); - if (isset($md->table->columns[$name])) { + if (isset($this->getTableSchema()->columns[$name])) { unset($this->_attributes[$name]); - } elseif (isset($md->relations[$name])) { + } elseif (method_exists($this, $name)) { unset($this->_related[$name]); } else { parent::__unset($name); @@ -525,23 +383,6 @@ abstract class ActiveRecord extends Model } /** - * Calls the named method which is not a class method. - * Do not call this method. This is a PHP magic method that we override - * to implement the named scope feature. - * @param string $name the method name - * @param array $params method parameters - * @return mixed the method return value - */ - public function __call($name, $params) - { - $md = $this->getMetaData(); - if (isset($md->relations[$name])) { - return $this->findByRelation($md->relations[$name], isset($params[0]) ? $params[0] : array()); - } - return parent::__call($name, $params); - } - - /** * Initializes the internal storage for the relation. * This method is internally used by [[ActiveQuery]] when populating relation data. * @param ActiveRelation $relation the relation object @@ -609,19 +450,7 @@ abstract class ActiveRecord extends Model */ public function attributes() { - return array_keys($this->getMetaData()->table->columns); - } - - /** - * Returns a list of scenarios and the corresponding relevant attributes. - * Please refer to [[\yii\base\Model::scenarios()]] for more details. - * The implementation here simply returns an empty array. You may override - * this method to return the scenarios that you want to use with this AR class. - * @return array a list of scenarios and the corresponding relevant attributes. - */ - public function scenarios() - { - return array(); + return array_keys($this->getTableSchema()->columns); } /** @@ -726,7 +555,7 @@ abstract class ActiveRecord extends Model $db = $this->getDbConnection(); $command = $query->insert($this->tableName(), $values)->createCommand($db); if ($command->execute()) { - $table = $this->getMetaData()->table; + $table = $this->getTableSchema(); if ($table->sequenceName !== null) { foreach ($table->primaryKey as $name) { if (!isset($this->_attributes[$name])) { @@ -973,12 +802,12 @@ abstract class ActiveRecord extends Model */ public function getPrimaryKey($asArray = false) { - $table = static::getMetaData()->table; - if (count($table->primaryKey) === 1 && !$asArray) { - return isset($this->_attributes[$table->primaryKey[0]]) ? $this->_attributes[$table->primaryKey[0]] : null; + $keys = $this->primaryKey(); + if (count($keys) === 1 && !$asArray) { + return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null; } else { $values = array(); - foreach ($table->primaryKey as $name) { + foreach ($keys as $name) { $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; } return $values; @@ -998,12 +827,12 @@ abstract class ActiveRecord extends Model */ public function getOldPrimaryKey($asArray = false) { - $table = static::getMetaData()->table; - if (count($table->primaryKey) === 1 && !$asArray) { - return isset($this->_oldAttributes[$table->primaryKey[0]]) ? $this->_oldAttributes[$table->primaryKey[0]] : null; + $keys = $this->primaryKey(); + if (count($keys) === 1 && !$asArray) { + return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null; } else { $values = array(); - foreach ($table->primaryKey as $name) { + foreach ($keys as $name) { $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; } return $values; @@ -1018,7 +847,7 @@ abstract class ActiveRecord extends Model public static function create($row) { $record = static::instantiate($row); - $columns = static::getMetaData()->table->columns; + $columns = static::model()->getTableSchema()->columns; foreach ($row as $name => $value) { if (isset($columns[$name])) { $record->_attributes[$name] = $value; @@ -1042,8 +871,7 @@ abstract class ActiveRecord extends Model */ public static function instantiate($row) { - $class = get_called_class(); - return new $class; + return new static; } /** diff --git a/framework/db/dao/Connection.php b/framework/db/dao/Connection.php index 85f29b4..6bb1576 100644 --- a/framework/db/dao/Connection.php +++ b/framework/db/dao/Connection.php @@ -461,6 +461,17 @@ class Connection extends \yii\base\ApplicationComponent } /** + * Obtains the metadata for the named table. + * @param string $name table name. The table name may contain schema name if any. Do not quote the table name. + * @param boolean $refresh whether to reload the table schema even if it is found in the cache. + * @return TableSchema table metadata. Null if the named table does not exist. + */ + public function getTableSchema($name, $refresh = false) + { + return $this->getDriver()->getTableSchema($name, $refresh); + } + + /** * Returns the ID of the last inserted row or sequence value. * @param string $sequenceName name of the sequence object (required by some DBMS) * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index 0ad8466..2b6ac21 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -1,28 +1,29 @@ array( - 'link' => array('customer_id' => 'id'), - ), - ); + return $this->hasMany('Order', array('customer_id' => 'id')); } + /** + * @param ActiveQuery $query + * @return ActiveQuery + */ public function active($query) { - return $query->andWhere('@.`status` = ' . self::STATUS_ACTIVE); + return $query->andWhere('`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 b56c1c8..1b45f1e 100644 --- a/tests/unit/data/ar/Item.php +++ b/tests/unit/data/ar/Item.php @@ -4,14 +4,8 @@ namespace yiiunit\data\ar; class Item extends ActiveRecord { - public static function tableName() + public function tableName() { return 'tbl_item'; } - - public static function relations() - { - return array( - ); - } } \ No newline at end of file diff --git a/tests/unit/data/ar/Order.php b/tests/unit/data/ar/Order.php index 09365f0..c9ed5b2 100644 --- a/tests/unit/data/ar/Order.php +++ b/tests/unit/data/ar/Order.php @@ -4,40 +4,30 @@ namespace yiiunit\data\ar; class Order extends ActiveRecord { - public static function tableName() + public function tableName() { return 'tbl_order'; } - public static function relations() + public function customer() { - return array( - 'customer:Customer' => array( - 'link' => array('id' => 'customer_id'), - ), - 'orderItems:OrderItem' => array( - 'link' => array('order_id' => 'id'), - ), - 'items:Item[]' => array( - 'via' => 'orderItems', - 'link' => array( - 'id' => 'item_id', - ), - 'order' => '@.id', - ), - 'books:Item[]' => array( - 'joinType' => 'INNER JOIN', - 'via' => array( - 'table' => 'tbl_order_item', - 'link' => array( - 'order_id' => 'id', - ), - ), - 'link' => array( - 'id' => 'item_id', - ), - 'on' => '@.category_id = 1', - ), - ); + 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', array('id' => 'item_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'); } } \ No newline at end of file diff --git a/tests/unit/data/ar/OrderItem.php b/tests/unit/data/ar/OrderItem.php index c2ef796..0141f11 100644 --- a/tests/unit/data/ar/OrderItem.php +++ b/tests/unit/data/ar/OrderItem.php @@ -4,20 +4,18 @@ namespace yiiunit\data\ar; class OrderItem extends ActiveRecord { - public static function tableName() + public function tableName() { return 'tbl_order_item'; } - public static function relations() + public function order() { - return array( - 'order:Order' => array( - 'link' => array('order_id' => 'id'), - ), - 'item:Item' => array( - 'link' => array('item_id' => 'id'), - ), - ); + return $this->hasOne('Order', array('id' => 'order_id')); + } + + public function item() + { + return $this->hasOne('Item', array('id' => 'item_id')); } } \ No newline at end of file diff --git a/tests/unit/framework/db/ar/ActiveRecordTest.php b/tests/unit/framework/db/ar/ActiveRecordTest.php index a9263bc..9b169eb 100644 --- a/tests/unit/framework/db/ar/ActiveRecordTest.php +++ b/tests/unit/framework/db/ar/ActiveRecordTest.php @@ -16,87 +16,6 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase ActiveRecord::$db = $this->getConnection(); } - public function testInsert() - { - $customer = new Customer; - $customer->email = 'user4@example.com'; - $customer->name = 'user4'; - $customer->address = 'address4'; - - $this->assertNull($customer->id); - $this->assertTrue($customer->isNewRecord); - - $customer->save(); - - $this->assertEquals(4, $customer->id); - $this->assertFalse($customer->isNewRecord); - } - - public function testUpdate() - { - // save - $customer = Customer::find(2); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user2', $customer->name); - $this->assertFalse($customer->isNewRecord); - $customer->name = 'user2x'; - $customer->save(); - $this->assertEquals('user2x', $customer->name); - $this->assertFalse($customer->isNewRecord); - $customer2 = Customer::find(2); - $this->assertEquals('user2x', $customer2->name); - - // updateCounters - $pk = array('order_id' => 2, 'item_id' => 4); - $orderItem = OrderItem::find()->where($pk)->one(); - $this->assertEquals(1, $orderItem->quantity); - $ret = $orderItem->updateCounters(array('quantity' => -1)); - $this->assertTrue($ret); - $this->assertEquals(0, $orderItem->quantity); - $orderItem = OrderItem::find()->where($pk)->one(); - $this->assertEquals(0, $orderItem->quantity); - - // updateAll - $customer = Customer::find(3); - $this->assertEquals('user3', $customer->name); - $ret = Customer::updateAll(array( - 'name' => 'temp', - ), array('id' => 3)); - $this->assertEquals(1, $ret); - $customer = Customer::find(3); - $this->assertEquals('temp', $customer->name); - - // updateCounters - $pk = array('order_id' => 1, 'item_id' => 2); - $orderItem = OrderItem::find()->where($pk)->one(); - $this->assertEquals(2, $orderItem->quantity); - $ret = OrderItem::updateAllCounters(array( - 'quantity' => 3, - ), $pk); - $this->assertEquals(1, $ret); - $orderItem = OrderItem::find()->where($pk)->one(); - $this->assertEquals(5, $orderItem->quantity); - } - - public function testDelete() - { - // delete - $customer = Customer::find(2); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user2', $customer->name); - $customer->delete(); - $customer = Customer::find(2); - $this->assertNull($customer); - - // deleteAll - $customers = Customer::find()->all(); - $this->assertEquals(2, count($customers)); - $ret = Customer::deleteAll(); - $this->assertEquals(2, $ret); - $customers = Customer::find()->all(); - $this->assertEquals(0, count($customers)); - } - public function testFind() { // find one @@ -105,6 +24,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $customer = $result->one(); $this->assertTrue($customer instanceof Customer); $this->assertEquals(1, $result->count); + $this->assertEquals(1, count($result)); // find all $result = Customer::find(); @@ -138,6 +58,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertTrue($result[0] instanceof Customer); $this->assertTrue($result[1] instanceof Customer); $this->assertTrue($result[2] instanceof Customer); + $this->assertEquals(3, count($result)); // find by a single primary key $customer = Customer::find(2); @@ -164,186 +85,337 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertEquals(2, Customer::count(array( 'where' => 'id=1 OR id=2', ))); + $this->assertEquals(2, Customer::count()->where('id=1 OR id=2')); } - public function testFindBySql() - { - // find one - $customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user3', $customer->name); - - // find all - $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); - $this->assertEquals(3, count($customers)); - - // find with parameter binding - $customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user2', $customer->name); - - // count - $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); - $query->one(); - $this->assertEquals(3, $query->count); - $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); - $this->assertEquals(3, $query->count); - } - - public function testQueryMethods() - { - $customer = Customer::find()->where('id=:id', array(':id' => 2))->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user2', $customer->name); - - $customer = Customer::find()->where(array('name' => 'user3'))->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals('user3', $customer->name); - - $customer = Customer::find()->select('id')->order('id DESC')->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals(3, $customer->id); - $this->assertEquals(null, $customer->name); - - // scopes - $customers = Customer::find()->active()->all(); - $this->assertEquals(2, count($customers)); - $customers = Customer::find(array( - 'scopes' => array('active'), - ))->all(); - $this->assertEquals(2, count($customers)); - - // asArray - $customers = Customer::find()->order('id')->asArray()->all(); - $this->assertEquals('user2', $customers[1]['name']); - - // index - $customers = Customer::find()->order('id')->index('name')->all(); - $this->assertEquals(2, $customers['user2']['id']); - } - - public function testEagerLoading() - { - // has many - $customers = Customer::find()->with('orders')->order('@.id')->all(); - $this->assertEquals(3, count($customers)); - $this->assertEquals(1, count($customers[0]->orders)); - $this->assertEquals(2, count($customers[1]->orders)); - $this->assertEquals(0, count($customers[2]->orders)); - - // nested - $customers = Customer::find()->with('orders.customer')->order('@.id')->all(); - $this->assertEquals(3, count($customers)); - $this->assertEquals(1, $customers[0]->orders[0]->customer->id); - $this->assertEquals(2, $customers[1]->orders[0]->customer->id); - $this->assertEquals(2, $customers[1]->orders[1]->customer->id); - - // has many via relation - $orders = Order::find()->with('items')->order('@.id')->all(); - $this->assertEquals(3, count($orders)); - $this->assertEquals(1, $orders[0]->items[0]->id); - $this->assertEquals(2, $orders[0]->items[1]->id); - $this->assertEquals(3, $orders[1]->items[0]->id); - $this->assertEquals(4, $orders[1]->items[1]->id); - $this->assertEquals(5, $orders[1]->items[2]->id); - - // has many via join table - $orders = Order::find()->with('books')->order('@.id')->all(); - $this->assertEquals(2, count($orders)); - $this->assertEquals(1, $orders[0]->books[0]->id); - $this->assertEquals(2, $orders[0]->books[1]->id); - $this->assertEquals(2, $orders[1]->books[0]->id); - - // has many and base limited - $orders = Order::find()->with('items')->order('@.id')->limit(2)->all(); - $this->assertEquals(2, count($orders)); - $this->assertEquals(1, $orders[0]->items[0]->id); - - /// customize "with" query - $orders = Order::find()->with(array('items' => function($q) { - $q->order('@.id DESC'); - }))->order('@.id')->limit(2)->all(); - $this->assertEquals(2, count($orders)); - $this->assertEquals(2, $orders[0]->items[0]->id); - - // findBySql with - $orders = Order::findBySql('SELECT * FROM tbl_order WHERE customer_id=2')->with('items')->all(); - $this->assertEquals(2, count($orders)); - - // index and array - $customers = Customer::find()->with('orders.customer')->order('@.id')->index('id')->asArray()->all(); - $this->assertEquals(3, count($customers)); - $this->assertTrue(isset($customers[1], $customers[2], $customers[3])); - $this->assertTrue(is_array($customers[1])); - $this->assertEquals(1, count($customers[1]['orders'])); - $this->assertEquals(2, count($customers[2]['orders'])); - $this->assertEquals(0, count($customers[3]['orders'])); - $this->assertTrue(is_array($customers[1]['orders'][0]['customer'])); - - // count with - $this->assertEquals(3, Order::count()); - $value = Order::count(array( - 'select' => array('COUNT(DISTINCT @.id, @.customer_id)'), - 'with' => 'books', - )); - $this->assertEquals(2, $value); - - } - - public function testLazyLoading() - { - // has one - $order = Order::find(3); - $this->assertTrue($order->customer instanceof Customer); - $this->assertEquals(2, $order->customer->id); - - // has many - $customer = Customer::find(2); - $orders = $customer->orders; - $this->assertEquals(2, count($orders)); - $this->assertEquals(2, $orders[0]->id); - $this->assertEquals(3, $orders[1]->id); - - // has many via join table - $orders = Order::find()->order('@.id')->all(); - $this->assertEquals(3, count($orders)); - $this->assertEquals(2, count($orders[0]->books)); - $this->assertEquals(1, $orders[0]->books[0]->id); - $this->assertEquals(2, $orders[0]->books[1]->id); - $this->assertEquals(array(), $orders[1]->books); - $this->assertEquals(1, count($orders[2]->books)); - $this->assertEquals(2, $orders[2]->books[0]->id); - - // customized relation query - $customer = Customer::find(2); - $orders = $customer->orders(array( - 'where' => '@.id = 3', - )); - $this->assertEquals(1, count($orders)); - $this->assertEquals(3, $orders[0]->id); - - // original results are kept after customized query - $orders = $customer->orders; - $this->assertEquals(2, count($orders)); - $this->assertEquals(2, $orders[0]->id); - $this->assertEquals(3, $orders[1]->id); - - // as array - $orders = $customer->orders(array( - 'asArray' => true, - )); - $this->assertEquals(2, count($orders)); - $this->assertTrue(is_array($orders[0])); - $this->assertEquals(2, $orders[0]['id']); - $this->assertEquals(3, $orders[1]['id']); - - // using anonymous function to customize query condition - $orders = $customer->orders(function($q) { - $q->order('@.id DESC')->asArray(); - }); - $this->assertEquals(2, count($orders)); - $this->assertTrue(is_array($orders[0])); - $this->assertEquals(3, $orders[0]['id']); - $this->assertEquals(2, $orders[1]['id']); - } +// public function testInsert() +// { +// $customer = new Customer; +// $customer->email = 'user4@example.com'; +// $customer->name = 'user4'; +// $customer->address = 'address4'; +// +// $this->assertNull($customer->id); +// $this->assertTrue($customer->isNewRecord); +// +// $customer->save(); +// +// $this->assertEquals(4, $customer->id); +// $this->assertFalse($customer->isNewRecord); +// } +// +// public function testUpdate() +// { +// // save +// $customer = Customer::find(2); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals('user2', $customer->name); +// $this->assertFalse($customer->isNewRecord); +// $customer->name = 'user2x'; +// $customer->save(); +// $this->assertEquals('user2x', $customer->name); +// $this->assertFalse($customer->isNewRecord); +// $customer2 = Customer::find(2); +// $this->assertEquals('user2x', $customer2->name); +// +// // updateCounters +// $pk = array('order_id' => 2, 'item_id' => 4); +// $orderItem = OrderItem::find()->where($pk)->one(); +// $this->assertEquals(1, $orderItem->quantity); +// $ret = $orderItem->updateCounters(array('quantity' => -1)); +// $this->assertTrue($ret); +// $this->assertEquals(0, $orderItem->quantity); +// $orderItem = OrderItem::find()->where($pk)->one(); +// $this->assertEquals(0, $orderItem->quantity); +// +// // updateAll +// $customer = Customer::find(3); +// $this->assertEquals('user3', $customer->name); +// $ret = Customer::updateAll(array( +// 'name' => 'temp', +// ), array('id' => 3)); +// $this->assertEquals(1, $ret); +// $customer = Customer::find(3); +// $this->assertEquals('temp', $customer->name); +// +// // updateCounters +// $pk = array('order_id' => 1, 'item_id' => 2); +// $orderItem = OrderItem::find()->where($pk)->one(); +// $this->assertEquals(2, $orderItem->quantity); +// $ret = OrderItem::updateAllCounters(array( +// 'quantity' => 3, +// ), $pk); +// $this->assertEquals(1, $ret); +// $orderItem = OrderItem::find()->where($pk)->one(); +// $this->assertEquals(5, $orderItem->quantity); +// } +// +// public function testDelete() +// { +// // delete +// $customer = Customer::find(2); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals('user2', $customer->name); +// $customer->delete(); +// $customer = Customer::find(2); +// $this->assertNull($customer); +// +// // deleteAll +// $customers = Customer::find()->all(); +// $this->assertEquals(2, count($customers)); +// $ret = Customer::deleteAll(); +// $this->assertEquals(2, $ret); +// $customers = Customer::find()->all(); +// $this->assertEquals(0, count($customers)); +// } +// +// public function testFind() +// { +// // find one +// $result = Customer::find(); +// $this->assertTrue($result instanceof ActiveQuery); +// $customer = $result->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals(1, $result->count); +// +// // find all +// $result = Customer::find(); +// $customers = $result->all(); +// $this->assertTrue(is_array($customers)); +// $this->assertEquals(3, count($customers)); +// $this->assertTrue($customers[0] instanceof Customer); +// $this->assertTrue($customers[1] instanceof Customer); +// $this->assertTrue($customers[2] instanceof Customer); +// $this->assertEquals(3, $result->count); +// $this->assertEquals(3, count($result)); +// +// // check count first +// $result = Customer::find(); +// $this->assertEquals(3, $result->count); +// $customer = $result->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals(3, $result->count); +// +// // iterator +// $result = Customer::find(); +// $count = 0; +// foreach ($result as $customer) { +// $this->assertTrue($customer instanceof Customer); +// $count++; +// } +// $this->assertEquals($count, $result->count); +// +// // array access +// $result = Customer::find(); +// $this->assertTrue($result[0] instanceof Customer); +// $this->assertTrue($result[1] instanceof Customer); +// $this->assertTrue($result[2] instanceof Customer); +// +// // find by a single primary key +// $customer = Customer::find(2); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals('user2', $customer->name); +// +// // find by attributes +// $customer = Customer::find()->where(array('name' => 'user2'))->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals(2, $customer->id); +// +// // find by Query +// $query = array( +// 'where' => 'id=:id', +// 'params' => array(':id' => 2), +// ); +// $customer = Customer::find($query)->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals('user2', $customer->name); +// +// // find count +// $this->assertEquals(3, Customer::find()->count()); +// $this->assertEquals(3, Customer::count()); +// $this->assertEquals(2, Customer::count(array( +// 'where' => 'id=1 OR id=2', +// ))); +// } +// +// public function testFindBySql() +// { +// // find one +// $customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals('user3', $customer->name); +// +// // find all +// $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); +// $this->assertEquals(3, count($customers)); +// +// // find with parameter binding +// $customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals('user2', $customer->name); +// +// // count +// $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); +// $query->one(); +// $this->assertEquals(3, $query->count); +// $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); +// $this->assertEquals(3, $query->count); +// } +// +// public function testQueryMethods() +// { +// $customer = Customer::find()->where('id=:id', array(':id' => 2))->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals('user2', $customer->name); +// +// $customer = Customer::find()->where(array('name' => 'user3'))->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals('user3', $customer->name); +// +// $customer = Customer::find()->select('id')->orderBy('id DESC')->one(); +// $this->assertTrue($customer instanceof Customer); +// $this->assertEquals(3, $customer->id); +// $this->assertEquals(null, $customer->name); +// +// // scopes +// $customers = Customer::find()->active()->all(); +// $this->assertEquals(2, count($customers)); +// $customers = Customer::find(array( +// 'scopes' => array('active'), +// ))->all(); +// $this->assertEquals(2, count($customers)); +// +// // asArray +// $customers = Customer::find()->orderBy('id')->asArray()->all(); +// $this->assertEquals('user2', $customers[1]['name']); +// +// // index +// $customers = Customer::find()->orderBy('id')->index('name')->all(); +// $this->assertEquals(2, $customers['user2']['id']); +// } +// +// public function testEagerLoading() +// { +// // has many +// $customers = Customer::find()->with('orders')->orderBy('@.id')->all(); +// $this->assertEquals(3, count($customers)); +// $this->assertEquals(1, count($customers[0]->orders)); +// $this->assertEquals(2, count($customers[1]->orders)); +// $this->assertEquals(0, count($customers[2]->orders)); +// +// // nested +// $customers = Customer::find()->with('orders.customer')->orderBy('@.id')->all(); +// $this->assertEquals(3, count($customers)); +// $this->assertEquals(1, $customers[0]->orders[0]->customer->id); +// $this->assertEquals(2, $customers[1]->orders[0]->customer->id); +// $this->assertEquals(2, $customers[1]->orders[1]->customer->id); +// +// // has many via relation +// $orders = Order::find()->with('items')->orderBy('@.id')->all(); +// $this->assertEquals(3, count($orders)); +// $this->assertEquals(1, $orders[0]->items[0]->id); +// $this->assertEquals(2, $orders[0]->items[1]->id); +// $this->assertEquals(3, $orders[1]->items[0]->id); +// $this->assertEquals(4, $orders[1]->items[1]->id); +// $this->assertEquals(5, $orders[1]->items[2]->id); +// +// // has many via join table +// $orders = Order::find()->with('books')->orderBy('@.id')->all(); +// $this->assertEquals(2, count($orders)); +// $this->assertEquals(1, $orders[0]->books[0]->id); +// $this->assertEquals(2, $orders[0]->books[1]->id); +// $this->assertEquals(2, $orders[1]->books[0]->id); +// +// // has many and base limited +// $orders = Order::find()->with('items')->orderBy('@.id')->limit(2)->all(); +// $this->assertEquals(2, count($orders)); +// $this->assertEquals(1, $orders[0]->items[0]->id); +// +// /// customize "with" query +// $orders = Order::find()->with(array('items' => function($q) { +// $q->orderBy('@.id DESC'); +// }))->orderBy('@.id')->limit(2)->all(); +// $this->assertEquals(2, count($orders)); +// $this->assertEquals(2, $orders[0]->items[0]->id); +// +// // findBySql with +// $orders = Order::findBySql('SELECT * FROM tbl_order WHERE customer_id=2')->with('items')->all(); +// $this->assertEquals(2, count($orders)); +// +// // index and array +// $customers = Customer::find()->with('orders.customer')->orderBy('@.id')->index('id')->asArray()->all(); +// $this->assertEquals(3, count($customers)); +// $this->assertTrue(isset($customers[1], $customers[2], $customers[3])); +// $this->assertTrue(is_array($customers[1])); +// $this->assertEquals(1, count($customers[1]['orders'])); +// $this->assertEquals(2, count($customers[2]['orders'])); +// $this->assertEquals(0, count($customers[3]['orders'])); +// $this->assertTrue(is_array($customers[1]['orders'][0]['customer'])); +// +// // count with +// $this->assertEquals(3, Order::count()); +// $value = Order::count(array( +// 'select' => array('COUNT(DISTINCT @.id, @.customer_id)'), +// 'with' => 'books', +// )); +// $this->assertEquals(2, $value); +// +// } +// +// public function testLazyLoading() +// { +// // has one +// $order = Order::find(3); +// $this->assertTrue($order->customer instanceof Customer); +// $this->assertEquals(2, $order->customer->id); +// +// // has many +// $customer = Customer::find(2); +// $orders = $customer->orders; +// $this->assertEquals(2, count($orders)); +// $this->assertEquals(2, $orders[0]->id); +// $this->assertEquals(3, $orders[1]->id); +// +// // has many via join table +// $orders = Order::find()->orderBy('@.id')->all(); +// $this->assertEquals(3, count($orders)); +// $this->assertEquals(2, count($orders[0]->books)); +// $this->assertEquals(1, $orders[0]->books[0]->id); +// $this->assertEquals(2, $orders[0]->books[1]->id); +// $this->assertEquals(array(), $orders[1]->books); +// $this->assertEquals(1, count($orders[2]->books)); +// $this->assertEquals(2, $orders[2]->books[0]->id); +// +// // customized relation query +// $customer = Customer::find(2); +// $orders = $customer->orders(array( +// 'where' => '@.id = 3', +// )); +// $this->assertEquals(1, count($orders)); +// $this->assertEquals(3, $orders[0]->id); +// +// // original results are kept after customized query +// $orders = $customer->orders; +// $this->assertEquals(2, count($orders)); +// $this->assertEquals(2, $orders[0]->id); +// $this->assertEquals(3, $orders[1]->id); +// +// // as array +// $orders = $customer->orders(array( +// 'asArray' => true, +// )); +// $this->assertEquals(2, count($orders)); +// $this->assertTrue(is_array($orders[0])); +// $this->assertEquals(2, $orders[0]['id']); +// $this->assertEquals(3, $orders[1]['id']); +// +// // using anonymous function to customize query condition +// $orders = $customer->orders(function($q) { +// $q->orderBy('@.id DESC')->asArray(); +// }); +// $this->assertEquals(2, count($orders)); +// $this->assertTrue(is_array($orders[0])); +// $this->assertEquals(3, $orders[0]['id']); +// $this->assertEquals(2, $orders[1]['id']); +// } } \ No newline at end of file diff --git a/tests/unit/framework/db/dao/QueryTest.php b/tests/unit/framework/db/dao/QueryTest.php index 8779cd1..2a84617 100644 --- a/tests/unit/framework/db/dao/QueryTest.php +++ b/tests/unit/framework/db/dao/QueryTest.php @@ -56,14 +56,14 @@ class QueryTest extends \yiiunit\MysqlTestCase function testGroup() { $query = new Query; - $query->group('team'); - $this->assertEquals('team', $query->group); + $query->groupBy('team'); + $this->assertEquals('team', $query->groupBy); $query->addGroup('company'); - $this->assertEquals(array('team', 'company'), $query->group); + $this->assertEquals(array('team', 'company'), $query->groupBy); $query->addGroup('age'); - $this->assertEquals(array('team', 'company', 'age'), $query->group); + $this->assertEquals(array('team', 'company', 'age'), $query->groupBy); } function testHaving() @@ -85,14 +85,14 @@ class QueryTest extends \yiiunit\MysqlTestCase function testOrder() { $query = new Query; - $query->order('team'); - $this->assertEquals('team', $query->order); + $query->orderBy('team'); + $this->assertEquals('team', $query->orderBy); - $query->addOrder('company'); - $this->assertEquals(array('team', 'company'), $query->order); + $query->addOrderBy('company'); + $this->assertEquals(array('team', 'company'), $query->orderBy); - $query->addOrder('age'); - $this->assertEquals(array('team', 'company', 'age'), $query->order); + $query->addOrderBy('age'); + $this->assertEquals(array('team', 'company', 'age'), $query->orderBy); } function testLimitOffset()