Browse Source

Merge branch 'master' of git.yiisoft.com:yii2 into console-color

tags/2.0.0-beta
Qiang Xue 12 years ago
parent
commit
732bbe79d4
  1. 15
      docs/internals/ar.md
  2. 27
      docs/internals/database.md
  3. 214
      docs/model.md
  4. 231
      framework/base/Model.php
  5. 20
      framework/db/ar/ActiveFinder.php
  6. 19
      framework/db/ar/ActiveMetaData.php
  7. 336
      framework/db/ar/ActiveQuery.php
  8. 298
      framework/db/ar/ActiveRecord.php
  9. 46
      framework/db/dao/BaseQuery.php
  10. 11
      framework/db/dao/Connection.php
  11. 8
      framework/db/dao/DataReader.php
  12. 1
      framework/db/dao/Query.php
  13. 10
      framework/db/dao/QueryBuilder.php
  14. 24
      framework/validators/Validator.php
  15. 2
      framework/web/Sort.php
  16. 17
      tests/unit/data/ar/Customer.php
  17. 8
      tests/unit/data/ar/Item.php
  18. 50
      tests/unit/data/ar/Order.php
  19. 18
      tests/unit/data/ar/OrderItem.php
  20. 594
      tests/unit/framework/db/ar/ActiveRecordTest.php
  21. 20
      tests/unit/framework/db/dao/QueryTest.php

15
docs/internals/ar.md

@ -0,0 +1,15 @@
ActiveRecord
============
Query
-----
### Basic Queries
### Relational Queries
### Scopes

27
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

214
docs/model.md

@ -0,0 +1,214 @@
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
------------------------------------
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.

231
framework/base/Model.php

@ -10,6 +10,8 @@
namespace yii\base; namespace yii\base;
use yii\util\StringHelper; use yii\util\StringHelper;
use yii\validators\Validator;
use yii\validators\RequiredValidator;
/** /**
* Model is the base class for data models. * 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 $errors Errors for all attributes or the specified attribute. Empty array is returned if no error.
* @property array $attributes Attribute values (name=>value). * @property array $attributes Attribute values (name=>value).
* @property string $scenario The scenario that this model is in. * @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 * @event ModelEvent beforeValidate an event raised at the beginning of [[validate()]]. You may set
* [[ModelEvent::isValid]] to be false to stop the validation. * [[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 static $_attributes = array(); // class name => array of attribute names
private $_errors; // attribute name => array of errors private $_errors; // attribute name => array of errors
private $_validators; // validators private $_validators; // Vector of validators
private $_scenario; // scenario private $_scenario = 'default';
/**
* 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;
}
/** /**
* Returns the validation rules for attributes. * 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; * - 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 * - 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 * - 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. * 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. * - 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()`. * merge the parent rules with child rules using functions such as `array_merge()`.
* *
* @return array validation rules * @return array validation rules
* @see scenarios
*/ */
public function rules() public function rules()
{ {
@ -152,6 +119,52 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
} }
/** /**
* Returns a list of scenarios and the corresponding active attributes.
* The returned array should be in the following format:
*
* ~~~
* array(
* 'scenario1' => array('attribute11', 'attribute12', ...),
* 'scenario2' => array('attribute21', 'attribute22', ...),
* ...
* )
* ~~~
*
* 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. * Returns the attribute labels.
* *
* Attribute labels are mainly used for display purpose. For example, given an attribute * Attribute labels are mainly used for display purpose. For example, given an attribute
@ -175,30 +188,33 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
/** /**
* Performs the data validation. * Performs the data validation.
* *
* This method executes the validation rules as declared in [[rules()]]. * This method executes the validation rules applicable to the current [[scenario]].
* Only the rules applicable to the current [[scenario]] will be executed. * The following criteria are used to determine whether a rule is currently applicable:
* A rule is considered applicable to a scenario if its `on` option is not set *
* or contains the scenario. * - 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 * This method will call [[beforeValidate()]] and [[afterValidate()]] before and
* after actual validation, respectively. If [[beforeValidate()]] returns false, * after the actual validation, respectively. If [[beforeValidate()]] returns false,
* the validation and [[afterValidate()]] will be cancelled. * 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. * @param array $attributes list of attributes that should be validated.
* If this parameter is empty, it means any attribute listed in the applicable * If this parameter is empty, it means any attribute listed in the applicable
* validation rules should be validated. * validation rules should be validated.
* @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation
* @return boolean whether the validation is successful without any error. * @return boolean whether the validation is successful without any error.
* @see beforeValidate()
* @see afterValidate()
*/ */
public function validate($attributes = null, $clearErrors = true) public function validate($attributes = null, $clearErrors = true)
{ {
if ($clearErrors) { if ($clearErrors) {
$this->clearErrors(); $this->clearErrors();
} }
if ($attributes === null) {
$attributes = $this->activeAttributes();
}
if ($this->beforeValidate()) { if ($this->beforeValidate()) {
foreach ($this->getActiveValidators() as $validator) { foreach ($this->getActiveValidators() as $validator) {
$validator->validate($this, $attributes); $validator->validate($this, $attributes);
@ -214,7 +230,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
* The default implementation raises a `beforeValidate` event. * The default implementation raises a `beforeValidate` event.
* You may override this method to do preliminary checks before validation. * 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. * 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. * If false is returned, the validation will stop and the model is considered invalid.
*/ */
public function beforeValidate() public function beforeValidate()
@ -269,8 +285,9 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
{ {
$validators = array(); $validators = array();
$scenario = $this->getScenario(); $scenario = $this->getScenario();
/** @var $validator Validator */
foreach ($this->getValidators() as $validator) { foreach ($this->getValidators() as $validator) {
if ($validator->applyTo($scenario, $attribute)) { if ($validator->isActive($scenario, $attribute)) {
$validators[] = $validator; $validators[] = $validator;
} }
} }
@ -287,8 +304,10 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
{ {
$validators = new Vector; $validators = new Vector;
foreach ($this->rules() as $rule) { foreach ($this->rules() as $rule) {
if (isset($rule[0], $rule[1])) { // attributes, validator type if ($rule instanceof Validator) {
$validator = \yii\validators\Validator::createValidator($rule[1], $this, $rule[0], array_slice($rule, 2)); $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); $validators->add($validator);
} else { } else {
throw new BadConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); throw new BadConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.');
@ -308,7 +327,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
public function isAttributeRequired($attribute) public function isAttributeRequired($attribute)
{ {
foreach ($this->getActiveValidators($attribute) as $validator) { foreach ($this->getActiveValidators($attribute) as $validator) {
if ($validator instanceof \yii\validators\RequiredValidator) { if ($validator instanceof RequiredValidator) {
return true; return true;
} }
} }
@ -322,13 +341,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
*/ */
public function isAttributeSafe($attribute) public function isAttributeSafe($attribute)
{ {
$validators = $this->getActiveValidators($attribute); return in_array($attribute, $this->safeAttributes(), true);
foreach ($validators as $validator) {
if (!$validator->safe) {
return false;
}
}
return $validators !== array();
} }
/** /**
@ -346,7 +359,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
/** /**
* Returns a value indicating whether there is any validation error. * 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. * @return boolean whether there is any error.
*/ */
public function hasErrors($attribute = null) public function hasErrors($attribute = null)
@ -452,24 +465,22 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
/** /**
* Returns attribute values. * Returns attribute values.
* @param array $names list of attributes whose value needs to be returned. * @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. * 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). * @return array attribute values (name=>value).
*/ */
public function getAttributes($names = null) public function getAttributes($names = null, $except = array())
{ {
$values = array(); $values = array();
if ($names === null) {
if (is_array($names)) { $names = $this->attributes();
foreach ($this->attributeNames() as $name) { }
if (in_array($name, $names, true)) { foreach ($names as $name) {
$values[$name] = $this->$name; $values[$name] = $this->$name;
} }
} foreach ($except as $name) {
} else { unset($values[$name]);
foreach ($this->attributeNames() as $name) {
$values[$name] = $this->$name;
}
} }
return $values; return $values;
@ -480,13 +491,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
* @param array $values attribute values (name=>value) to be assigned to the model. * @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. * @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]]. * A safe attribute is one that is associated with a validation rule in the current [[scenario]].
* @see getSafeAttributeNames * @see safeAttributes()
* @see attributeNames * @see attributes()
*/ */
public function setAttributes($values, $safeOnly = true) public function setAttributes($values, $safeOnly = true)
{ {
if (is_array($values)) { 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) { foreach ($values as $name => $value) {
if (isset($attributes[$name])) { if (isset($attributes[$name])) {
$this->$name = $value; $this->$name = $value;
@ -517,15 +528,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
* Scenario affects how validation is performed and which attributes can * Scenario affects how validation is performed and which attributes can
* be massively assigned. * be massively assigned.
* *
* A validation rule will be performed when calling [[validate()]] * @return string the scenario that this model is in. Defaults to 'default'.
* 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.
*/ */
public function getScenario() public function getScenario()
{ {
@ -543,30 +546,52 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
} }
/** /**
* Returns the attribute names that are safe to be massively assigned. * Returns the attribute names that are safe to be massively assigned in the current scenario.
* A safe attribute is one that is associated with a validation rule in the current [[scenario]].
* @return array safe attribute names * @return array safe attribute names
*/ */
public function getSafeAttributeNames() public function safeAttributes()
{ {
$attributes = array(); $scenario = $this->getScenario();
$unsafe = array(); $scenarios = $this->scenarios();
foreach ($this->getActiveValidators() as $validator) { if (isset($scenarios[$scenario])) {
if (!$validator->safe) { $attributes = array();
foreach ($validator->attributes as $name) { foreach ($scenarios[$scenario] as $attribute) {
$unsafe[] = $name; if ($attribute[0] !== '!') {
} $attributes[] = $attribute;
} else {
foreach ($validator->attributes as $name) {
$attributes[$name] = true;
} }
} }
return $attributes;
} else {
return $this->activeAttributes();
} }
}
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()
{
$scenario = $this->getScenario();
$scenarios = $this->scenarios();
if (isset($scenarios[$scenario])) {
// scenario declared in scenarios()
$attributes = $scenarios[$this->getScenario()];
foreach ($attributes as $i => $attribute) {
if ($attribute[0] === '!') {
$attributes[$i] = substr($attribute, 1);
}
}
} else {
// use validators to determine active attributes
$attributes = array();
foreach ($this->attributes() as $attribute) {
if ($this->getActiveValidators($attribute) !== array()) {
$attributes[] = $attribute;
}
}
} }
return array_keys($attributes); return $attributes;
} }
/** /**
@ -589,7 +614,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
*/ */
public function offsetExists($offset) public function offsetExists($offset)
{ {
return property_exists($this, $offset) && $this->$offset !== null; return $this->$offset !== null;
} }
/** /**

20
framework/db/ar/ActiveFinder.php

@ -467,21 +467,21 @@ class ActiveFinder extends \yii\base\Object
} }
} }
if ($element->query->order !== null) { if ($element->query->orderBy !== null) {
if (!is_array($element->query->order)) { if (!is_array($element->query->orderBy)) {
$element->query->order = preg_split('/\s*,\s*/', trim($element->query->order), -1, PREG_SPLIT_NO_EMPTY); $element->query->orderBy = preg_split('/\s*,\s*/', trim($element->query->orderBy), -1, PREG_SPLIT_NO_EMPTY);
} }
foreach ($element->query->order as $order) { foreach ($element->query->orderBy as $order) {
$query->order[] = strtr($order, $prefixes); $query->orderBy[] = strtr($order, $prefixes);
} }
} }
if ($element->query->group !== null) { if ($element->query->groupBy !== null) {
if (!is_array($element->query->group)) { if (!is_array($element->query->groupBy)) {
$element->query->group = preg_split('/\s*,\s*/', trim($element->query->group), -1, PREG_SPLIT_NO_EMPTY); $element->query->groupBy = preg_split('/\s*,\s*/', trim($element->query->groupBy), -1, PREG_SPLIT_NO_EMPTY);
} }
foreach ($element->query->group as $group) { foreach ($element->query->groupBy as $group) {
$query->group[] = strtr($group, $prefixes); $query->groupBy[] = strtr($group, $prefixes);
} }
} }

19
framework/db/ar/ActiveMetaData.php

@ -26,9 +26,9 @@ class ActiveMetaData
*/ */
public $modelClass; 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. * Returns an instance of ActiveMetaData for the specified model class.
@ -55,21 +55,18 @@ class ActiveMetaData
public function __construct($modelClass) public function __construct($modelClass)
{ {
$this->modelClass = $modelClass; $this->modelClass = $modelClass;
$tableName = $modelClass::tableName(); $this->model = new $modelClass;
$this->table = $modelClass::getDbConnection()->getDriver()->getTableSchema($tableName); $tableName = $this->model->tableName();
$this->table = $this->model->getDbConnection()->getDriver()->getTableSchema($tableName);
if ($this->table === null) { if ($this->table === null) {
throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'."); throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'.");
} }
$primaryKey = $modelClass::primaryKey(); $primaryKey = $this->model->primaryKey();
if ($primaryKey !== null) { if ($primaryKey !== $this->table->primaryKey) {
$this->table->fixPrimaryKey($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."); 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);
}
} }
/** /**

336
framework/db/ar/ActiveQuery.php

@ -10,10 +10,310 @@
namespace yii\db\ar; namespace yii\db\ar;
use yii\db\dao\BaseQuery;
use yii\base\VectorIterator; use yii\base\VectorIterator;
use yii\db\dao\Expression; use yii\db\dao\Expression;
use yii\db\Exception; 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 * 1. eager loading, base limited and has has_many relations
* 2. * 2.
@ -24,7 +324,7 @@ use yii\db\Exception;
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @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. * @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()); 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;
}
} }

298
framework/db/ar/ActiveRecord.php

@ -44,6 +44,10 @@ use yii\util\StringHelper;
abstract class ActiveRecord extends Model 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 * @var array attribute values indexed by attribute names
*/ */
private $_attributes = array(); private $_attributes = array();
@ -56,14 +60,19 @@ abstract class ActiveRecord extends Model
*/ */
private $_related; private $_related;
/** /**
* Returns the metadata for this AR class. * Returns a model instance to support accessing non-static methods such as [[table()]], [[primaryKey()]].
* @param boolean $refresh whether to rebuild the metadata. * @return ActiveRecord
* @return ActiveMetaData the meta for this AR class.
*/ */
public static function getMetaData($refresh = false) public static function model()
{ {
return ActiveMetaData::getInstance(get_called_class(), $refresh); $className = get_called_class();
if (isset(self::$_models[$className])) {
return self::$_models[$className];
} else {
return self::$_models[$className] = new static;
}
} }
/** /**
@ -95,12 +104,12 @@ abstract class ActiveRecord extends Model
* // find all active customers and order them by their age: * // find all active customers and order them by their age:
* $customers = Customer::find() * $customers = Customer::find()
* ->where(array('status' => 1)) * ->where(array('status' => 1))
* ->order('age') * ->orderBy('age')
* ->all(); * ->all();
* // or alternatively: * // or alternatively:
* $customers = Customer::find(array( * $customers = Customer::find(array(
* 'where' => array('status' => 1), * 'where' => array('status' => 1),
* 'order' => 'age', * 'orderBy' => 'age',
* ))->all(); * ))->all();
* ~~~ * ~~~
* *
@ -122,14 +131,14 @@ abstract class ActiveRecord extends Model
} }
} elseif ($q !== null) { } elseif ($q !== null) {
// query by primary key // query by primary key
$primaryKey = static::getMetaData()->table->primaryKey; $primaryKey = static::model()->primaryKey();
return $query->where(array($primaryKey[0] => $q))->one(); return $query->where(array($primaryKey[0] => $q))->one();
} }
return $query; 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 * Note that because the SQL statement is already specified, calling further
* query methods (such as `where()`, `order()`) on [[ActiveQuery]] will have no effect. * query methods (such as `where()`, `order()`) on [[ActiveQuery]] will have no effect.
* Methods such as `with()`, `asArray()` can still be called though. * Methods such as `with()`, `asArray()` can still be called though.
@ -164,10 +173,12 @@ abstract class ActiveRecord extends Model
* echo Customer::count('COUNT(DISTINCT age)')->value(); * echo Customer::count('COUNT(DISTINCT age)')->value();
* ~~~ * ~~~
* *
* @param array $q the query configuration. This should be an array of name-value pairs. * @param array|string $q the query option. This can be one of the followings:
* It will be used to configure the [[ActiveQuery]] object for query purpose.
* *
* @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) public static function count($q = null)
{ {
@ -176,11 +187,12 @@ abstract class ActiveRecord extends Model
foreach ($q as $name => $value) { foreach ($q as $name => $value) {
$query->$name = $value; $query->$name = $value;
} }
} } elseif ($q !== null) {
if ($query->select === null) { $query->select = array($q);
} elseif ($query->select === null) {
$query->select = array('COUNT(*)'); $query->select = array('COUNT(*)');
} }
return $query->value(); return $query;
} }
/** /**
@ -194,7 +206,7 @@ abstract class ActiveRecord extends Model
public static function updateAll($attributes, $condition = '', $params = array()) public static function updateAll($attributes, $condition = '', $params = array())
{ {
$query = new Query; $query = new Query;
$query->update(static::tableName(), $attributes, $condition, $params); $query->update(static::model()->tableName(), $attributes, $condition, $params);
return $query->createCommand(static::getDbConnection())->execute(); return $query->createCommand(static::getDbConnection())->execute();
} }
@ -215,7 +227,7 @@ abstract class ActiveRecord extends Model
$counters[$name] = new Expression($value >= 0 ? "$quotedName+$value" : "$quotedName$value"); $counters[$name] = new Expression($value >= 0 ? "$quotedName+$value" : "$quotedName$value");
} }
$query = new Query; $query = new Query;
$query->update(static::tableName(), $counters, $condition, $params); $query->update(static::model()->tableName(), $counters, $condition, $params);
return $query->createCommand($db)->execute(); return $query->createCommand($db)->execute();
} }
@ -229,7 +241,7 @@ abstract class ActiveRecord extends Model
public static function deleteAll($condition = '', $params = array()) public static function deleteAll($condition = '', $params = array())
{ {
$query = new Query; $query = new Query;
$query->delete(static::tableName(), $condition, $params); $query->delete(static::model()->tableName(), $condition, $params);
return $query->createCommand(static::getDbConnection())->execute(); return $query->createCommand(static::getDbConnection())->execute();
} }
@ -250,128 +262,32 @@ abstract class ActiveRecord extends Model
* You may override this method if the table is not named after this convention. * You may override this method if the table is not named after this convention.
* @return string the table name * @return string the table name
*/ */
public static function tableName() public function tableName()
{ {
return StringHelper::camel2id(basename(get_called_class()), '_'); return StringHelper::camel2id(basename(get_class($this)), '_');
} }
/** /**
* Declares the primary key name for this AR class. * Returns the schema information of the DB table associated with this AR class.
* This method is meant to be overridden in case when the table has no primary key defined * @return TableSchema the schema information of the DB table associated with this AR class.
* (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.
*/ */
public static function primaryKey() public function getTableSchema()
{ {
return $this->getDbConnection()->getTableSchema($this->tableName());
} }
/** /**
* Declares the relations for this AR class. * Returns the primary keys for this AR class.
* * The default implementation will return the primary keys as declared
* Child classes may override this method to specify their relations. * in the DB table that is associated with this AR class.
* * If the DB table does not declare any primary key, you should override
* The following code shows how to declare relations for a `Programmer` AR class: * 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.
* 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:
* <ul>
* <li>BELONGS_TO: e.g. a member belongs to a team;</li>
* <li>HAS_ONE: e.g. a member has at most one profile;</li>
* <li>HAS_MANY: e.g. a team has many members;</li>
* <li>MANY_MANY: e.g. a member has many skills and a skill belongs to a member.</li>
* </ul>
*
* 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:
* <pre>
* 'varName'=>array('relationType', 'className', 'foreign_key', ...additional options)
* </pre>
* 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:
* <ul>
* <li>'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).</li>
* <li>'condition': string, the WHERE clause. Defaults to empty. Note, column references need to
* be disambiguated with prefix 'relationName.' (e.g. relationName.age&gt;20)</li>
* <li>'order': string, the ORDER BY clause. Defaults to empty. Note, column references need to
* be disambiguated with prefix 'relationName.' (e.g. relationName.age DESC)</li>
* <li>'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.</li>
* <li>'joinType': type of join. Defaults to 'LEFT OUTER JOIN'.</li>
* <li>'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.</li>
* <li>'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.</li>
* <li>'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.</li>
* <li>'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.</li>
* <li>'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.</li>
* </ul>
*
* The following options are available for certain relations when lazy loading:
* <ul>
* <li>'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.</li>
* <li>'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.</li>
* <li>'limit': limit of the rows to be selected. This option does not apply to BELONGS_TO relation.</li>
* <li>'offset': offset of the rows to be selected. This option does not apply to BELONGS_TO relation.</li>
* <li>'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.</li>
* </ul>
*
* Below is an example declaring related objects for 'Post' active record class:
* <pre>
* 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'),
* );
* </pre>
*
* @return array list of related object declarations. Defaults to empty array.
*/ */
public static function relations() public function primaryKey()
{ {
return array(); return $this->getTableSchema()->primaryKey;
} }
/** /**
@ -389,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'.
* <pre>
* return array(
* 'published'=>array(
* 'condition'=>'status=1',
* ),
* 'recently'=>array(
* 'order'=>'create_time DESC',
* 'limit'=>5,
* ),
* );
* </pre>
* If the above scopes are declared in a 'Post' model, we can perform the following
* queries:
* <pre>
* $posts=Post::model()->published()->findAll();
* $posts=Post::model()->published()->recently()->findAll();
* $posts=Post::model()->published()->with('comments')->findAll();
* </pre>
* 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. * PHP getter magic method.
* This method is overridden so that attributes and related objects can be accessed like properties. * This method is overridden so that attributes and related objects can be accessed like properties.
* @param string $name property name * @param string $name property name
@ -436,13 +316,13 @@ abstract class ActiveRecord extends Model
if (isset($this->_attributes[$name])) { if (isset($this->_attributes[$name])) {
return $this->_attributes[$name]; return $this->_attributes[$name];
} }
$md = $this->getMetaData(); if (isset($this->getTableSchema()->columns[$name])) {
if (isset($md->table->columns[$name])) {
return null; 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)) { if (isset($this->_related[$name]) || $this->_related !== null && array_key_exists($name, $this->_related)) {
return $this->_related[$name]; return $this->_related[$name];
} else { } else {
// todo
return $this->_related[$name] = $this->findByRelation($md->relations[$name]); return $this->_related[$name] = $this->findByRelation($md->relations[$name]);
} }
} }
@ -457,10 +337,9 @@ abstract class ActiveRecord extends Model
*/ */
public function __set($name, $value) public function __set($name, $value)
{ {
$md = $this->getMetaData(); if (isset($this->getTableSchema()->columns[$name])) {
if (isset($md->table->columns[$name])) {
$this->_attributes[$name] = $value; $this->_attributes[$name] = $value;
} elseif (isset($md->relations[$name])) { } elseif (method_exists($this, $name)) {
$this->_related[$name] = $value; $this->_related[$name] = $value;
} else { } else {
parent::__set($name, $value); parent::__set($name, $value);
@ -479,8 +358,7 @@ abstract class ActiveRecord extends Model
if (isset($this->_attributes[$name]) || isset($this->_related[$name])) { if (isset($this->_attributes[$name]) || isset($this->_related[$name])) {
return true; return true;
} }
$md = $this->getMetaData(); if (isset($this->getTableSchema()->columns[$name]) || method_exists($this, $name)) {
if (isset($md->table->columns[$name]) || isset($md->relations[$name])) {
return false; return false;
} else { } else {
return parent::__isset($name); return parent::__isset($name);
@ -495,10 +373,9 @@ abstract class ActiveRecord extends Model
*/ */
public function __unset($name) public function __unset($name)
{ {
$md = $this->getMetaData(); if (isset($this->getTableSchema()->columns[$name])) {
if (isset($md->table->columns[$name])) {
unset($this->_attributes[$name]); unset($this->_attributes[$name]);
} elseif (isset($md->relations[$name])) { } elseif (method_exists($this, $name)) {
unset($this->_related[$name]); unset($this->_related[$name]);
} else { } else {
parent::__unset($name); parent::__unset($name);
@ -506,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. * Initializes the internal storage for the relation.
* This method is internally used by [[ActiveQuery]] when populating relation data. * This method is internally used by [[ActiveQuery]] when populating relation data.
* @param ActiveRelation $relation the relation object * @param ActiveRelation $relation the relation object
@ -588,9 +448,9 @@ abstract class ActiveRecord extends Model
* This would return all column names of the table associated with this AR class. * This would return all column names of the table associated with this AR class.
* @return array list of attribute names. * @return array list of attribute names.
*/ */
public function attributeNames() public function attributes()
{ {
return array_keys($this->getMetaData()->table->columns); return array_keys($this->getTableSchema()->columns);
} }
/** /**
@ -621,31 +481,10 @@ abstract class ActiveRecord extends Model
$this->_attributes[$name] = $value; $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->attributeNames();
}
$values = array();
foreach ($names as $name) {
$values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
}
return $values;
}
public function getChangedAttributes($names = null) public function getChangedAttributes($names = null)
{ {
if ($names === null) { if ($names === null) {
$names = $this->attributeNames(); $names = $this->attributes();
} }
$names = array_flip($names); $names = array_flip($names);
$attributes = array(); $attributes = array();
@ -716,7 +555,7 @@ abstract class ActiveRecord extends Model
$db = $this->getDbConnection(); $db = $this->getDbConnection();
$command = $query->insert($this->tableName(), $values)->createCommand($db); $command = $query->insert($this->tableName(), $values)->createCommand($db);
if ($command->execute()) { if ($command->execute()) {
$table = $this->getMetaData()->table; $table = $this->getTableSchema();
if ($table->sequenceName !== null) { if ($table->sequenceName !== null) {
foreach ($table->primaryKey as $name) { foreach ($table->primaryKey as $name) {
if (!isset($this->_attributes[$name])) { if (!isset($this->_attributes[$name])) {
@ -931,7 +770,7 @@ abstract class ActiveRecord extends Model
return false; return false;
} }
if ($attributes === null) { if ($attributes === null) {
foreach ($this->attributeNames() as $name) { foreach ($this->attributes() as $name) {
$this->_attributes[$name] = $record->_attributes[$name]; $this->_attributes[$name] = $record->_attributes[$name];
} }
$this->_oldAttributes = $this->_attributes; $this->_oldAttributes = $this->_attributes;
@ -963,12 +802,12 @@ abstract class ActiveRecord extends Model
*/ */
public function getPrimaryKey($asArray = false) public function getPrimaryKey($asArray = false)
{ {
$table = static::getMetaData()->table; $keys = $this->primaryKey();
if (count($table->primaryKey) === 1 && !$asArray) { if (count($keys) === 1 && !$asArray) {
return isset($this->_attributes[$table->primaryKey[0]]) ? $this->_attributes[$table->primaryKey[0]] : null; return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
} else { } else {
$values = array(); $values = array();
foreach ($table->primaryKey as $name) { foreach ($keys as $name) {
$values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
} }
return $values; return $values;
@ -988,12 +827,12 @@ abstract class ActiveRecord extends Model
*/ */
public function getOldPrimaryKey($asArray = false) public function getOldPrimaryKey($asArray = false)
{ {
$table = static::getMetaData()->table; $keys = $this->primaryKey();
if (count($table->primaryKey) === 1 && !$asArray) { if (count($keys) === 1 && !$asArray) {
return isset($this->_oldAttributes[$table->primaryKey[0]]) ? $this->_oldAttributes[$table->primaryKey[0]] : null; return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
} else { } else {
$values = array(); $values = array();
foreach ($table->primaryKey as $name) { foreach ($keys as $name) {
$values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
} }
return $values; return $values;
@ -1008,7 +847,7 @@ abstract class ActiveRecord extends Model
public static function create($row) public static function create($row)
{ {
$record = static::instantiate($row); $record = static::instantiate($row);
$columns = static::getMetaData()->table->columns; $columns = static::model()->getTableSchema()->columns;
foreach ($row as $name => $value) { foreach ($row as $name => $value) {
if (isset($columns[$name])) { if (isset($columns[$name])) {
$record->_attributes[$name] = $value; $record->_attributes[$name] = $value;
@ -1032,8 +871,7 @@ abstract class ActiveRecord extends Model
*/ */
public static function instantiate($row) public static function instantiate($row)
{ {
$class = get_called_class(); return new static;
return new $class;
} }
/** /**

46
framework/db/dao/BaseQuery.php

@ -59,15 +59,15 @@ 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. * @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')`). * 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. * @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')`). * 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. * @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')`). * `array('LEFT JOIN tbl_user ON tbl_user.id=author_id', 'LEFT JOIN tbl_team ON tbl_team.id=team_id')`).
* @see join() * @see join()
*/ */
@ -330,9 +330,9 @@ class BaseQuery extends \yii\base\Component
* @return BaseQuery the query object itself * @return BaseQuery the query object itself
* @see addGroup() * @see addGroup()
*/ */
public function group($columns) public function groupBy($columns)
{ {
$this->group = $columns; $this->groupBy = $columns;
return $this; return $this;
} }
@ -347,16 +347,16 @@ class BaseQuery extends \yii\base\Component
*/ */
public function addGroup($columns) public function addGroup($columns)
{ {
if (empty($this->group)) { if (empty($this->groupBy)) {
$this->group = $columns; $this->groupBy = $columns;
} else { } else {
if (!is_array($this->group)) { if (!is_array($this->groupBy)) {
$this->group = preg_split('/\s*,\s*/', trim($this->group), -1, PREG_SPLIT_NO_EMPTY); $this->groupBy = preg_split('/\s*,\s*/', trim($this->groupBy), -1, PREG_SPLIT_NO_EMPTY);
} }
if (!is_array($columns)) { if (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); $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; return $this;
} }
@ -428,9 +428,9 @@ class BaseQuery extends \yii\base\Component
* @return BaseQuery the query object itself * @return BaseQuery the query object itself
* @see addOrder() * @see addOrder()
*/ */
public function order($columns) public function orderBy($columns)
{ {
$this->order = $columns; $this->orderBy = $columns;
return $this; return $this;
} }
@ -443,18 +443,18 @@ class BaseQuery extends \yii\base\Component
* @return BaseQuery the query object itself * @return BaseQuery the query object itself
* @see order() * @see order()
*/ */
public function addOrder($columns) public function addOrderBy($columns)
{ {
if (empty($this->order)) { if (empty($this->orderBy)) {
$this->order = $columns; $this->orderBy = $columns;
} else { } else {
if (!is_array($this->order)) { if (!is_array($this->orderBy)) {
$this->order = preg_split('/\s*,\s*/', trim($this->order), -1, PREG_SPLIT_NO_EMPTY); $this->orderBy = preg_split('/\s*,\s*/', trim($this->orderBy), -1, PREG_SPLIT_NO_EMPTY);
} }
if (!is_array($columns)) { if (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); $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; return $this;
} }
@ -540,7 +540,7 @@ class BaseQuery extends \yii\base\Component
* takes precedence over this query. * takes precedence over this query.
* - [[where]], [[having]]: the new query's corresponding property value * - [[where]], [[having]]: the new query's corresponding property value
* will be 'AND' together with the existing one. * 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. * corresponding property value will be appended to the existing one.
* *
* In general, the merging makes the resulting query more restrictive and specific. * 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); $this->addParams($query->params);
} }
if ($query->order !== null) { if ($query->orderBy !== null) {
$this->addOrder($query->order); $this->addOrderBy($query->orderBy);
} }
if ($query->group !== null) { if ($query->groupBy !== null) {
$this->addGroup($query->group); $this->addGroup($query->groupBy);
} }
if ($query->join !== null) { if ($query->join !== null) {

11
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. * Returns the ID of the last inserted row or sequence value.
* @param string $sequenceName name of the sequence object (required by some DBMS) * @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 * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object

8
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. * 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() 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. * Returns a single column from the next row of a result set.
* @param integer $columnIndex zero-based column index * @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) 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. * Returns an object populated with the next row of data.
* @param string $className class name of the object to be created and populated * @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 * @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) public function readObject($className, $fields)
{ {
@ -149,7 +149,7 @@ class DataReader extends \yii\base\Object implements \Iterator, \Countable
/** /**
* Closes the reader. * Closes the reader.
* This frees up the resources allocated for executing this SQL statement. * 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() public function close()
{ {

1
framework/db/dao/Query.php

@ -75,6 +75,7 @@ class Query extends BaseQuery
$qb->query = $this; $qb->query = $this;
return call_user_func_array(array($qb, $method), $params); return call_user_func_array(array($qb, $method), $params);
} else { } else {
/** @var $qb QueryBuilder */
return $qb->build($this); return $qb->build($this);
} }
} }

10
framework/db/dao/QueryBuilder.php

@ -69,10 +69,10 @@ class QueryBuilder extends \yii\base\Object
$this->buildFrom($query->from), $this->buildFrom($query->from),
$this->buildJoin($query->join), $this->buildJoin($query->join),
$this->buildWhere($query->where), $this->buildWhere($query->where),
$this->buildGroup($query->group), $this->buildGroup($query->groupBy),
$this->buildHaving($query->having), $this->buildHaving($query->having),
$this->buildUnion($query->union), $this->buildUnion($query->union),
$this->buildOrder($query->order), $this->buildOrder($query->orderBy),
$this->buildLimit($query->limit, $query->offset), $this->buildLimit($query->limit, $query->offset),
); );
return implode($this->separator, array_filter($clauses)); return implode($this->separator, array_filter($clauses));
@ -92,7 +92,7 @@ class QueryBuilder extends \yii\base\Object
* *
* @param string $table the table that new rows will be inserted into. * @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. * @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) 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 * @param mixed $condition the condition that will be put in the WHERE part. Please
* refer to [[Query::where()]] on how to specify condition. * refer to [[Query::where()]] on how to specify condition.
* @param array $params the parameters to be bound to the query. * @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()) 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 * @param mixed $condition the condition that will be put in the WHERE part. Please
* refer to [[Query::where()]] on how to specify condition. * refer to [[Query::where()]] on how to specify condition.
* @param array $params the parameters to be bound to the query. * @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()) public function delete($table, $condition = '', $params = array())
{ {

24
framework/validators/Validator.php

@ -42,8 +42,6 @@ namespace yii\validators;
* - `captcha`: [[CaptchaValidator]] * - `captcha`: [[CaptchaValidator]]
* - `default`: [[DefaultValueValidator]] * - `default`: [[DefaultValueValidator]]
* - `exist`: [[ExistValidator]] * - `exist`: [[ExistValidator]]
* - `safe`: [[SafeValidator]]
* - `unsafe`: [[UnsafeValidator]]
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -58,8 +56,6 @@ abstract class Validator extends \yii\base\Component
'match' => '\yii\validators\RegularExpressionValidator', 'match' => '\yii\validators\RegularExpressionValidator',
'email' => '\yii\validators\EmailValidator', 'email' => '\yii\validators\EmailValidator',
'url' => '\yii\validators\UrlValidator', 'url' => '\yii\validators\UrlValidator',
'safe' => '\yii\validators\SafeValidator',
'unsafe' => '\yii\validators\UnsafeValidator',
'filter' => '\yii\validators\FilterValidator', 'filter' => '\yii\validators\FilterValidator',
'captcha' => '\yii\validators\CaptchaValidator', 'captcha' => '\yii\validators\CaptchaValidator',
'default' => '\yii\validators\DefaultValueValidator', 'default' => '\yii\validators\DefaultValueValidator',
@ -103,11 +99,6 @@ abstract class Validator extends \yii\base\Component
*/ */
public $skipOnError = true; 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. * @var boolean whether to enable client-side validation. Defaults to true.
* Please refer to [[\yii\web\ActiveForm::enableClientValidation]] for more details about * Please refer to [[\yii\web\ActiveForm::enableClientValidation]] for more details about
* client-side validation. * client-side validation.
@ -187,8 +178,10 @@ abstract class Validator extends \yii\base\Component
/** /**
* Validates the specified object. * Validates the specified object.
* @param \yii\base\Model $object the data object being validated * @param \yii\base\Model $object the data object being validated
* @param array $attributes the list of attributes to be validated. Defaults to null, * @param array|null $attributes the list of attributes to be validated.
* meaning every attribute listed in [[attributes]] will 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) 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. * Returns a value indicating whether the validator is active for the given scenario and attribute.
* A validator applies to a scenario as long as any of the following conditions is met: *
* 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 * - the validator's `on` property contains the specified scenario
* *
* @param string $scenario scenario name * @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]]. * the method will also check if the attribute appears in [[attributes]].
* @return boolean whether the validator applies to the specified scenario. * @return boolean whether the validator applies to the specified scenario.
*/ */
public function applyTo($scenario, $attribute = null) public function isActive($scenario, $attribute = null)
{ {
$applies = !isset($this->except[$scenario]) && (empty($this->on) || isset($this->on[$scenario])); $applies = !isset($this->except[$scenario]) && (empty($this->on) || isset($this->on[$scenario]));
return $attribute === null ? $applies : $applies && in_array($attribute, $this->attributes, true); return $attribute === null ? $applies : $applies && in_array($attribute, $this->attributes, true);

2
framework/web/Sort.php

@ -429,7 +429,7 @@ class CSort extends CComponent
$attributes = $this->attributes; $attributes = $this->attributes;
} else { } else {
if ($this->modelClass !== null) { if ($this->modelClass !== null) {
$attributes = CActiveRecord::model($this->modelClass)->attributeNames(); $attributes = CActiveRecord::model($this->modelClass)->attributes();
} else { } else {
return false; return false;
} }

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

@ -1,28 +1,29 @@
<?php <?php
namespace yiiunit\data\ar; namespace yiiunit\data\ar;
use yii\db\ar\ActiveQuery;
class Customer extends ActiveRecord class Customer extends ActiveRecord
{ {
const STATUS_ACTIVE = 1; const STATUS_ACTIVE = 1;
const STATUS_INACTIVE = 2; const STATUS_INACTIVE = 2;
public static function tableName() public function tableName()
{ {
return 'tbl_customer'; return 'tbl_customer';
} }
public static function relations() public function orders()
{ {
return array( return $this->hasMany('Order', array('customer_id' => 'id'));
'orders:Order[]' => array(
'link' => array('customer_id' => 'id'),
),
);
} }
/**
* @param ActiveQuery $query
* @return ActiveQuery
*/
public function active($query) public function active($query)
{ {
return $query->andWhere('@.`status` = ' . self::STATUS_ACTIVE); return $query->andWhere('`status` = ' . self::STATUS_ACTIVE);
} }
} }

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

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

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

@ -4,40 +4,30 @@ namespace yiiunit\data\ar;
class Order extends ActiveRecord class Order extends ActiveRecord
{ {
public static function tableName() public function tableName()
{ {
return 'tbl_order'; return 'tbl_order';
} }
public static function relations() public function customer()
{ {
return array( return $this->hasOne('Customer', array('id' => 'customer_id'));
'customer:Customer' => array( }
'link' => array('id' => 'customer_id'),
), public function orderItems()
'orderItems:OrderItem' => array( {
'link' => array('order_id' => 'id'), return $this->hasMany('OrderItem', array('order_id' => 'id'));
), }
'items:Item[]' => array(
'via' => 'orderItems', public function items()
'link' => array( {
'id' => 'item_id', return $this->hasMany('Item', array('id' => 'item_id'))
), ->via('orderItems')->orderBy('id');
'order' => '@.id', }
),
'books:Item[]' => array( public function books()
'joinType' => 'INNER JOIN', {
'via' => array( return $this->manyMany('Item', array('id' => 'item_id'), 'tbl_order_item', array('item_id', 'id'))
'table' => 'tbl_order_item', ->where('category_id = 1');
'link' => array(
'order_id' => 'id',
),
),
'link' => array(
'id' => 'item_id',
),
'on' => '@.category_id = 1',
),
);
} }
} }

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

@ -4,20 +4,18 @@ namespace yiiunit\data\ar;
class OrderItem extends ActiveRecord class OrderItem extends ActiveRecord
{ {
public static function tableName() public function tableName()
{ {
return 'tbl_order_item'; return 'tbl_order_item';
} }
public static function relations() public function order()
{ {
return array( return $this->hasOne('Order', array('id' => 'order_id'));
'order:Order' => array( }
'link' => array('order_id' => 'id'),
), public function item()
'item:Item' => array( {
'link' => array('item_id' => 'id'), return $this->hasOne('Item', array('id' => 'item_id'));
),
);
} }
} }

594
tests/unit/framework/db/ar/ActiveRecordTest.php

@ -16,87 +16,6 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
ActiveRecord::$db = $this->getConnection(); 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() public function testFind()
{ {
// find one // find one
@ -105,6 +24,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$customer = $result->one(); $customer = $result->one();
$this->assertTrue($customer instanceof Customer); $this->assertTrue($customer instanceof Customer);
$this->assertEquals(1, $result->count); $this->assertEquals(1, $result->count);
$this->assertEquals(1, count($result));
// find all // find all
$result = Customer::find(); $result = Customer::find();
@ -138,6 +58,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$this->assertTrue($result[0] instanceof Customer); $this->assertTrue($result[0] instanceof Customer);
$this->assertTrue($result[1] instanceof Customer); $this->assertTrue($result[1] instanceof Customer);
$this->assertTrue($result[2] instanceof Customer); $this->assertTrue($result[2] instanceof Customer);
$this->assertEquals(3, count($result));
// find by a single primary key // find by a single primary key
$customer = Customer::find(2); $customer = Customer::find(2);
@ -164,186 +85,337 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase
$this->assertEquals(2, Customer::count(array( $this->assertEquals(2, Customer::count(array(
'where' => 'id=1 OR id=2', 'where' => 'id=1 OR id=2',
))); )));
$this->assertEquals(2, Customer::count()->where('id=1 OR id=2'));
} }
public function testFindBySql() // public function testInsert()
{ // {
// find one // $customer = new Customer;
$customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one(); // $customer->email = 'user4@example.com';
$this->assertTrue($customer instanceof Customer); // $customer->name = 'user4';
$this->assertEquals('user3', $customer->name); // $customer->address = 'address4';
//
// find all // $this->assertNull($customer->id);
$customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); // $this->assertTrue($customer->isNewRecord);
$this->assertEquals(3, count($customers)); //
// $customer->save();
// find with parameter binding //
$customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one(); // $this->assertEquals(4, $customer->id);
$this->assertTrue($customer instanceof Customer); // $this->assertFalse($customer->isNewRecord);
$this->assertEquals('user2', $customer->name); // }
//
// count // public function testUpdate()
$query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); // {
$query->one(); // // save
$this->assertEquals(3, $query->count); // $customer = Customer::find(2);
$query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC'); // $this->assertTrue($customer instanceof Customer);
$this->assertEquals(3, $query->count); // $this->assertEquals('user2', $customer->name);
} // $this->assertFalse($customer->isNewRecord);
// $customer->name = 'user2x';
public function testQueryMethods() // $customer->save();
{ // $this->assertEquals('user2x', $customer->name);
$customer = Customer::find()->where('id=:id', array(':id' => 2))->one(); // $this->assertFalse($customer->isNewRecord);
$this->assertTrue($customer instanceof Customer); // $customer2 = Customer::find(2);
$this->assertEquals('user2', $customer->name); // $this->assertEquals('user2x', $customer2->name);
//
$customer = Customer::find()->where(array('name' => 'user3'))->one(); // // updateCounters
$this->assertTrue($customer instanceof Customer); // $pk = array('order_id' => 2, 'item_id' => 4);
$this->assertEquals('user3', $customer->name); // $orderItem = OrderItem::find()->where($pk)->one();
// $this->assertEquals(1, $orderItem->quantity);
$customer = Customer::find()->select('id')->order('id DESC')->one(); // $ret = $orderItem->updateCounters(array('quantity' => -1));
$this->assertTrue($customer instanceof Customer); // $this->assertTrue($ret);
$this->assertEquals(3, $customer->id); // $this->assertEquals(0, $orderItem->quantity);
$this->assertEquals(null, $customer->name); // $orderItem = OrderItem::find()->where($pk)->one();
// $this->assertEquals(0, $orderItem->quantity);
// scopes //
$customers = Customer::find()->active()->all(); // // updateAll
$this->assertEquals(2, count($customers)); // $customer = Customer::find(3);
$customers = Customer::find(array( // $this->assertEquals('user3', $customer->name);
'scopes' => array('active'), // $ret = Customer::updateAll(array(
))->all(); // 'name' => 'temp',
$this->assertEquals(2, count($customers)); // ), array('id' => 3));
// $this->assertEquals(1, $ret);
// asArray // $customer = Customer::find(3);
$customers = Customer::find()->order('id')->asArray()->all(); // $this->assertEquals('temp', $customer->name);
$this->assertEquals('user2', $customers[1]['name']); //
// // updateCounters
// index // $pk = array('order_id' => 1, 'item_id' => 2);
$customers = Customer::find()->order('id')->index('name')->all(); // $orderItem = OrderItem::find()->where($pk)->one();
$this->assertEquals(2, $customers['user2']['id']); // $this->assertEquals(2, $orderItem->quantity);
} // $ret = OrderItem::updateAllCounters(array(
// 'quantity' => 3,
public function testEagerLoading() // ), $pk);
{ // $this->assertEquals(1, $ret);
// has many // $orderItem = OrderItem::find()->where($pk)->one();
$customers = Customer::find()->with('orders')->order('@.id')->all(); // $this->assertEquals(5, $orderItem->quantity);
$this->assertEquals(3, count($customers)); // }
$this->assertEquals(1, count($customers[0]->orders)); //
$this->assertEquals(2, count($customers[1]->orders)); // public function testDelete()
$this->assertEquals(0, count($customers[2]->orders)); // {
// // delete
// nested // $customer = Customer::find(2);
$customers = Customer::find()->with('orders.customer')->order('@.id')->all(); // $this->assertTrue($customer instanceof Customer);
$this->assertEquals(3, count($customers)); // $this->assertEquals('user2', $customer->name);
$this->assertEquals(1, $customers[0]->orders[0]->customer->id); // $customer->delete();
$this->assertEquals(2, $customers[1]->orders[0]->customer->id); // $customer = Customer::find(2);
$this->assertEquals(2, $customers[1]->orders[1]->customer->id); // $this->assertNull($customer);
//
// has many via relation // // deleteAll
$orders = Order::find()->with('items')->order('@.id')->all(); // $customers = Customer::find()->all();
$this->assertEquals(3, count($orders)); // $this->assertEquals(2, count($customers));
$this->assertEquals(1, $orders[0]->items[0]->id); // $ret = Customer::deleteAll();
$this->assertEquals(2, $orders[0]->items[1]->id); // $this->assertEquals(2, $ret);
$this->assertEquals(3, $orders[1]->items[0]->id); // $customers = Customer::find()->all();
$this->assertEquals(4, $orders[1]->items[1]->id); // $this->assertEquals(0, count($customers));
$this->assertEquals(5, $orders[1]->items[2]->id); // }
//
// has many via join table // public function testFind()
$orders = Order::find()->with('books')->order('@.id')->all(); // {
$this->assertEquals(2, count($orders)); // // find one
$this->assertEquals(1, $orders[0]->books[0]->id); // $result = Customer::find();
$this->assertEquals(2, $orders[0]->books[1]->id); // $this->assertTrue($result instanceof ActiveQuery);
$this->assertEquals(2, $orders[1]->books[0]->id); // $customer = $result->one();
// $this->assertTrue($customer instanceof Customer);
// has many and base limited // $this->assertEquals(1, $result->count);
$orders = Order::find()->with('items')->order('@.id')->limit(2)->all(); //
$this->assertEquals(2, count($orders)); // // find all
$this->assertEquals(1, $orders[0]->items[0]->id); // $result = Customer::find();
// $customers = $result->all();
/// customize "with" query // $this->assertTrue(is_array($customers));
$orders = Order::find()->with(array('items' => function($q) { // $this->assertEquals(3, count($customers));
$q->order('@.id DESC'); // $this->assertTrue($customers[0] instanceof Customer);
}))->order('@.id')->limit(2)->all(); // $this->assertTrue($customers[1] instanceof Customer);
$this->assertEquals(2, count($orders)); // $this->assertTrue($customers[2] instanceof Customer);
$this->assertEquals(2, $orders[0]->items[0]->id); // $this->assertEquals(3, $result->count);
// $this->assertEquals(3, count($result));
// findBySql with //
$orders = Order::findBySql('SELECT * FROM tbl_order WHERE customer_id=2')->with('items')->all(); // // check count first
$this->assertEquals(2, count($orders)); // $result = Customer::find();
// $this->assertEquals(3, $result->count);
// index and array // $customer = $result->one();
$customers = Customer::find()->with('orders.customer')->order('@.id')->index('id')->asArray()->all(); // $this->assertTrue($customer instanceof Customer);
$this->assertEquals(3, count($customers)); // $this->assertEquals(3, $result->count);
$this->assertTrue(isset($customers[1], $customers[2], $customers[3])); //
$this->assertTrue(is_array($customers[1])); // // iterator
$this->assertEquals(1, count($customers[1]['orders'])); // $result = Customer::find();
$this->assertEquals(2, count($customers[2]['orders'])); // $count = 0;
$this->assertEquals(0, count($customers[3]['orders'])); // foreach ($result as $customer) {
$this->assertTrue(is_array($customers[1]['orders'][0]['customer'])); // $this->assertTrue($customer instanceof Customer);
// $count++;
// count with // }
$this->assertEquals(3, Order::count()); // $this->assertEquals($count, $result->count);
$value = Order::count(array( //
'select' => array('COUNT(DISTINCT @.id, @.customer_id)'), // // array access
'with' => 'books', // $result = Customer::find();
)); // $this->assertTrue($result[0] instanceof Customer);
$this->assertEquals(2, $value); // $this->assertTrue($result[1] instanceof Customer);
// $this->assertTrue($result[2] instanceof Customer);
} //
// // find by a single primary key
public function testLazyLoading() // $customer = Customer::find(2);
{ // $this->assertTrue($customer instanceof Customer);
// has one // $this->assertEquals('user2', $customer->name);
$order = Order::find(3); //
$this->assertTrue($order->customer instanceof Customer); // // find by attributes
$this->assertEquals(2, $order->customer->id); // $customer = Customer::find()->where(array('name' => 'user2'))->one();
// $this->assertTrue($customer instanceof Customer);
// has many // $this->assertEquals(2, $customer->id);
$customer = Customer::find(2); //
$orders = $customer->orders; // // find by Query
$this->assertEquals(2, count($orders)); // $query = array(
$this->assertEquals(2, $orders[0]->id); // 'where' => 'id=:id',
$this->assertEquals(3, $orders[1]->id); // 'params' => array(':id' => 2),
// );
// has many via join table // $customer = Customer::find($query)->one();
$orders = Order::find()->order('@.id')->all(); // $this->assertTrue($customer instanceof Customer);
$this->assertEquals(3, count($orders)); // $this->assertEquals('user2', $customer->name);
$this->assertEquals(2, count($orders[0]->books)); //
$this->assertEquals(1, $orders[0]->books[0]->id); // // find count
$this->assertEquals(2, $orders[0]->books[1]->id); // $this->assertEquals(3, Customer::find()->count());
$this->assertEquals(array(), $orders[1]->books); // $this->assertEquals(3, Customer::count());
$this->assertEquals(1, count($orders[2]->books)); // $this->assertEquals(2, Customer::count(array(
$this->assertEquals(2, $orders[2]->books[0]->id); // 'where' => 'id=1 OR id=2',
// )));
// customized relation query // }
$customer = Customer::find(2); //
$orders = $customer->orders(array( // public function testFindBySql()
'where' => '@.id = 3', // {
)); // // find one
$this->assertEquals(1, count($orders)); // $customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one();
$this->assertEquals(3, $orders[0]->id); // $this->assertTrue($customer instanceof Customer);
// $this->assertEquals('user3', $customer->name);
// original results are kept after customized query //
$orders = $customer->orders; // // find all
$this->assertEquals(2, count($orders)); // $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all();
$this->assertEquals(2, $orders[0]->id); // $this->assertEquals(3, count($customers));
$this->assertEquals(3, $orders[1]->id); //
// // find with parameter binding
// as array // $customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one();
$orders = $customer->orders(array( // $this->assertTrue($customer instanceof Customer);
'asArray' => true, // $this->assertEquals('user2', $customer->name);
)); //
$this->assertEquals(2, count($orders)); // // count
$this->assertTrue(is_array($orders[0])); // $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC');
$this->assertEquals(2, $orders[0]['id']); // $query->one();
$this->assertEquals(3, $orders[1]['id']); // $this->assertEquals(3, $query->count);
// $query = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC');
// using anonymous function to customize query condition // $this->assertEquals(3, $query->count);
$orders = $customer->orders(function($q) { // }
$q->order('@.id DESC')->asArray(); //
}); // public function testQueryMethods()
$this->assertEquals(2, count($orders)); // {
$this->assertTrue(is_array($orders[0])); // $customer = Customer::find()->where('id=:id', array(':id' => 2))->one();
$this->assertEquals(3, $orders[0]['id']); // $this->assertTrue($customer instanceof Customer);
$this->assertEquals(2, $orders[1]['id']); // $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']);
// }
} }

20
tests/unit/framework/db/dao/QueryTest.php

@ -56,14 +56,14 @@ class QueryTest extends \yiiunit\MysqlTestCase
function testGroup() function testGroup()
{ {
$query = new Query; $query = new Query;
$query->group('team'); $query->groupBy('team');
$this->assertEquals('team', $query->group); $this->assertEquals('team', $query->groupBy);
$query->addGroup('company'); $query->addGroup('company');
$this->assertEquals(array('team', 'company'), $query->group); $this->assertEquals(array('team', 'company'), $query->groupBy);
$query->addGroup('age'); $query->addGroup('age');
$this->assertEquals(array('team', 'company', 'age'), $query->group); $this->assertEquals(array('team', 'company', 'age'), $query->groupBy);
} }
function testHaving() function testHaving()
@ -85,14 +85,14 @@ class QueryTest extends \yiiunit\MysqlTestCase
function testOrder() function testOrder()
{ {
$query = new Query; $query = new Query;
$query->order('team'); $query->orderBy('team');
$this->assertEquals('team', $query->order); $this->assertEquals('team', $query->orderBy);
$query->addOrder('company'); $query->addOrderBy('company');
$this->assertEquals(array('team', 'company'), $query->order); $this->assertEquals(array('team', 'company'), $query->orderBy);
$query->addOrder('age'); $query->addOrderBy('age');
$this->assertEquals(array('team', 'company', 'age'), $query->order); $this->assertEquals(array('team', 'company', 'age'), $query->orderBy);
} }
function testLimitOffset() function testLimitOffset()

Loading…
Cancel
Save