Browse Source

Merge pull request #1097 from yiisoft/db-traits

Db traits
tags/2.0.0-beta
Qiang Xue 11 years ago
parent
commit
b44921126b
  1. 4
      docs/guide/query-builder.md
  2. 78
      framework/yii/ar/ActiveQueryInterface.php
  3. 201
      framework/yii/ar/ActiveQueryTrait.php
  4. 29
      framework/yii/ar/ActiveRelationInterface.php
  5. 247
      framework/yii/ar/ActiveRelationTrait.php
  6. 20
      framework/yii/data/ActiveDataProvider.php
  7. 202
      framework/yii/db/ActiveQuery.php
  8. 24
      framework/yii/db/ActiveRecord.php
  9. 238
      framework/yii/db/ActiveRelation.php
  10. 142
      framework/yii/db/Query.php
  11. 2
      framework/yii/db/QueryBuilder.php
  12. 206
      framework/yii/db/QueryInterface.php
  13. 208
      framework/yii/db/QueryTrait.php
  14. 74
      tests/unit/framework/data/ActiveDataProviderTest.php
  15. 16
      tests/unit/framework/db/ActiveRecordTest.php
  16. 12
      tests/unit/framework/db/QueryTest.php
  17. 14
      tests/unit/framework/db/cubrid/CubridActiveDataProviderTest.php
  18. 14
      tests/unit/framework/db/mssql/MssqlActiveDataProviderTest.php
  19. 14
      tests/unit/framework/db/pgsql/PostgreSQLActiveDataProviderTest.php
  20. 14
      tests/unit/framework/db/sqlite/SqliteActiveDataProviderTest.php

4
docs/guide/query-builder.md

@ -167,8 +167,8 @@ For ordering results `orderBy` and `addOrderBy` could be used:
```php ```php
$query->orderBy([ $query->orderBy([
'id' => Query::SORT_ASC, 'id' => SORT_ASC,
'name' => Query::SORT_DESC, 'name' => SORT_DESC,
]); ]);
``` ```

78
framework/yii/ar/ActiveQueryInterface.php

@ -0,0 +1,78 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\ar;
use yii\db\QueryInterface;
/**
* ActiveQueryInterface defines the common interface to be implemented by active record query classes.
*
* A class implementing this interface should also use [[ActiveQueryTrait]].
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
interface ActiveQueryInterface extends QueryInterface
{
/**
* Sets the [[asArray]] property.
* @param boolean $value whether to return the query results in terms of arrays instead of Active Records.
* @return static the query object itself
*/
public function asArray($value = true);
/**
* Sets the [[indexBy]] property.
* @param string|callable $column the name of the column by which the query results should be indexed by.
* This can also be a callable (e.g. anonymous function) that returns the index value based on the given
* row or model data. The signature of the callable should be:
*
* ~~~
* // $model is an AR instance when `asArray` is false,
* // or an array of column values when `asArray` is true.
* function ($model)
* {
* // return the index value corresponding to $model
* }
* ~~~
*
* @return static the query object itself
*/
public function indexBy($column);
/**
* Specifies the relations with which this query should be performed.
*
* The parameters to this method can be either one or multiple strings, or a single array
* of relation names and the optional callbacks to customize the relations.
*
* A relation name can refer to a relation defined in [[modelClass]]
* or a sub-relation that stands for a relation of a related record.
* For example, `orders.address` means the `address` relation defined
* in the model class corresponding to the `orders` relation.
*
* The followings are some usage examples:
*
* ~~~
* // find customers together with their orders and country
* Customer::find()->with('orders', 'country')->all();
* // find customers together with their orders and the orders' shipping address
* Customer::find()->with('orders.address')->all();
* // find customers together with their country and orders of status 1
* Customer::find()->with([
* 'orders' => function($query) {
* $query->andWhere('status = 1');
* },
* 'country',
* ])->all();
* ~~~
*
* @return static the query object itself
*/
public function with();
}

201
framework/yii/ar/ActiveQueryTrait.php

@ -0,0 +1,201 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\ar;
use yii\db\ActiveRecord;
/**
* ActiveQueryTrait implements the common methods and properties for active record query classes.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
trait ActiveQueryTrait
{
/**
* @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 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;
/**
* PHP magic method.
* This method allows calling static method defined in [[modelClass]] via this query object.
* It is mainly implemented for supporting the feature of scope.
* @param string $name the method name to be called
* @param array $params the parameters passed to the method
* @return mixed the method return result
*/
public function __call($name, $params)
{
if (method_exists($this->modelClass, $name)) {
array_unshift($params, $this);
call_user_func_array([$this->modelClass, $name], $params);
return $this;
} else {
return parent::__call($name, $params);
}
}
/**
* Sets the [[asArray]] property.
* @param boolean $value whether to return the query results in terms of arrays instead of Active Records.
* @return static the query object itself
*/
public function asArray($value = true)
{
$this->asArray = $value;
return $this;
}
/**
* Specifies the relations with which this query should be performed.
*
* The parameters to this method can be either one or multiple strings, or a single array
* of relation names and the optional callbacks to customize the relations.
*
* A relation name can refer to a relation defined in [[modelClass]]
* or a sub-relation that stands for a relation of a related record.
* For example, `orders.address` means the `address` relation defined
* in the model class corresponding to the `orders` relation.
*
* The followings are some usage examples:
*
* ~~~
* // find customers together with their orders and country
* Customer::find()->with('orders', 'country')->all();
* // find customers together with their orders and the orders' shipping address
* Customer::find()->with('orders.address')->all();
* // find customers together with their country and orders of status 1
* Customer::find()->with([
* 'orders' => function($query) {
* $query->andWhere('status = 1');
* },
* 'country',
* ])->all();
* ~~~
*
* @return static the query object itself
*/
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;
}
/**
* Converts found rows into model instances
* @param array $rows
* @return array|ActiveRecord[]
*/
private function createModels($rows)
{
$models = [];
if ($this->asArray) {
if ($this->indexBy === null) {
return $rows;
}
foreach ($rows as $row) {
if (is_string($this->indexBy)) {
$key = $row[$this->indexBy];
} else {
$key = call_user_func($this->indexBy, $row);
}
$models[$key] = $row;
}
} else {
/** @var ActiveRecord $class */
$class = $this->modelClass;
if ($this->indexBy === null) {
foreach ($rows as $row) {
$models[] = $class::create($row);
}
} else {
foreach ($rows as $row) {
$model = $class::create($row);
if (is_string($this->indexBy)) {
$key = $model->{$this->indexBy};
} else {
$key = call_user_func($this->indexBy, $model);
}
$models[$key] = $model;
}
}
}
return $models;
}
/**
* @param ActiveRecord[] $models
* @param array $with
*/
private function populateRelations(&$models, $with)
{
$primaryModel = new $this->modelClass;
$relations = $this->normalizeRelations($primaryModel, $with);
foreach ($relations as $name => $relation) {
if ($relation->asArray === null) {
// inherit asArray from primary query
$relation->asArray = $this->asArray;
}
$relation->findWith($name, $models);
}
}
/**
* @param ActiveRecord $model
* @param array $with
* @return ActiveRelationInterface[]
*/
private function normalizeRelations($model, $with)
{
$relations = [];
foreach ($with as $name => $callback) {
if (is_integer($name)) {
$name = $callback;
$callback = null;
}
if (($pos = strpos($name, '.')) !== false) {
// with sub-relations
$childName = substr($name, $pos + 1);
$name = substr($name, 0, $pos);
} else {
$childName = null;
}
if (!isset($relations[$name])) {
$relation = $model->getRelation($name);
$relation->primaryModel = null;
$relations[$name] = $relation;
} else {
$relation = $relations[$name];
}
if (isset($childName)) {
$relation->with[$childName] = $callback;
} elseif ($callback !== null) {
call_user_func($callback, $relation);
}
}
return $relations;
}
}

29
framework/yii/ar/ActiveRelationInterface.php

@ -0,0 +1,29 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\ar;
/**
* ActiveRelationInterface defines the common interface to be implemented by active record relation classes.
*
* A class implementing this interface should also use [[ActiveRelationTrait]].
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
interface ActiveRelationInterface extends ActiveQueryInterface
{
/**
* Specifies the relation associated with the pivot table.
* @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]].
* @param callable $callable a PHP callback for customizing the relation associated with the pivot table.
* Its signature should be `function($query)`, where `$query` is the query to be customized.
* @return static the relation object itself.
*/
public function via($relationName, $callable = null);
}

247
framework/yii/ar/ActiveRelationTrait.php

@ -0,0 +1,247 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\ar;
use yii\base\InvalidConfigException;
use yii\db\ActiveRecord;
/**
* ActiveRelationTrait implements the common methods and properties for active record relation classes.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
trait ActiveRelationTrait
{
/**
* @var boolean whether this relation should populate all query results into AR instances.
* If false, only the first row of the results will be retrieved.
*/
public $multiple;
/**
* @var ActiveRecord the primary model that this relation is associated with.
* This is used only in lazy loading with dynamic query options.
*/
public $primaryModel;
/**
* @var array the columns of the primary and foreign tables that establish the relation.
* The array keys must be columns of the table for this relation, and the array values
* must be the corresponding columns from the primary table.
* Do not prefix or quote the column names as this will be done automatically by Yii.
*/
public $link;
/**
* @var array the query associated with the pivot table. Please call [[via()]]
* to set this property instead of directly setting it.
*/
public $via;
/**
* Clones internal objects.
*/
public function __clone()
{
// make a clone of "via" object so that the same query object can be reused multiple times
if (is_object($this->via)) {
$this->via = clone $this->via;
} elseif (is_array($this->via)) {
$this->via = [$this->via[0], clone $this->via[1]];
}
}
/**
* Specifies the relation associated with the pivot table.
* @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]].
* @param callable $callable a PHP callback for customizing the relation associated with the pivot table.
* Its signature should be `function($query)`, where `$query` is the query to be customized.
* @return static the relation object itself.
*/
public function via($relationName, $callable = null)
{
$relation = $this->primaryModel->getRelation($relationName);
$this->via = [$relationName, $relation];
if ($callable !== null) {
call_user_func($callable, $relation);
}
return $this;
}
/**
* Finds the related records and populates them into the primary models.
* This method is internally used by [[ActiveQuery]]. Do not call it directly.
* @param string $name the relation name
* @param array $primaryModels primary models
* @return array the related models
* @throws InvalidConfigException
*/
public function findWith($name, &$primaryModels)
{
if (!is_array($this->link)) {
throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
}
if ($this->via instanceof self) {
// via pivot table
/** @var ActiveRelationTrait $viaQuery */
$viaQuery = $this->via;
$viaModels = $viaQuery->findPivotRows($primaryModels);
$this->filterByModels($viaModels);
} elseif (is_array($this->via)) {
// via relation
/** @var ActiveRelationTrait $viaQuery */
list($viaName, $viaQuery) = $this->via;
$viaQuery->primaryModel = null;
$viaModels = $viaQuery->findWith($viaName, $primaryModels);
$this->filterByModels($viaModels);
} else {
$this->filterByModels($primaryModels);
}
if (count($primaryModels) === 1 && !$this->multiple) {
$model = $this->one();
foreach ($primaryModels as $i => $primaryModel) {
if ($primaryModel instanceof ActiveRecord) {
$primaryModel->populateRelation($name, $model);
} else {
$primaryModels[$i][$name] = $model;
}
}
return [$model];
} else {
$models = $this->all();
if (isset($viaModels, $viaQuery)) {
$buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link);
} else {
$buckets = $this->buildBuckets($models, $this->link);
}
$link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
foreach ($primaryModels as $i => $primaryModel) {
$key = $this->getModelKey($primaryModel, $link);
$value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
if ($primaryModel instanceof ActiveRecord) {
$primaryModel->populateRelation($name, $value);
} else {
$primaryModels[$i][$name] = $value;
}
}
return $models;
}
}
/**
* @param array $models
* @param array $link
* @param array $viaModels
* @param array $viaLink
* @return array
*/
private function buildBuckets($models, $link, $viaModels = null, $viaLink = null)
{
$buckets = [];
$linkKeys = array_keys($link);
foreach ($models as $i => $model) {
$key = $this->getModelKey($model, $linkKeys);
if ($this->indexBy !== null) {
$buckets[$key][$i] = $model;
} else {
$buckets[$key][] = $model;
}
}
if ($viaModels !== null) {
$viaBuckets = [];
$viaLinkKeys = array_keys($viaLink);
$linkValues = array_values($link);
foreach ($viaModels as $viaModel) {
$key1 = $this->getModelKey($viaModel, $viaLinkKeys);
$key2 = $this->getModelKey($viaModel, $linkValues);
if (isset($buckets[$key2])) {
foreach ($buckets[$key2] as $i => $bucket) {
if ($this->indexBy !== null) {
$viaBuckets[$key1][$i] = $bucket;
} else {
$viaBuckets[$key1][] = $bucket;
}
}
}
}
$buckets = $viaBuckets;
}
if (!$this->multiple) {
foreach ($buckets as $i => $bucket) {
$buckets[$i] = reset($bucket);
}
}
return $buckets;
}
/**
* @param ActiveRecord|array $model
* @param array $attributes
* @return string
*/
private function getModelKey($model, $attributes)
{
if (count($attributes) > 1) {
$key = [];
foreach ($attributes as $attribute) {
$key[] = $model[$attribute];
}
return serialize($key);
} else {
$attribute = reset($attributes);
return $model[$attribute];
}
}
/**
* @param array $models
*/
private function filterByModels($models)
{
$attributes = array_keys($this->link);
$values = [];
if (count($attributes) === 1) {
// single key
$attribute = reset($this->link);
foreach ($models as $model) {
if (($value = $model[$attribute]) !== null) {
$values[] = $value;
}
}
} else {
// composite keys
foreach ($models as $model) {
$v = [];
foreach ($this->link as $attribute => $link) {
$v[$attribute] = $model[$link];
}
$values[] = $v;
}
}
$this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]);
}
/**
* @param ActiveRecord[] $primaryModels
* @return array
*/
private function findPivotRows($primaryModels)
{
if (empty($primaryModels)) {
return [];
}
$this->filterByModels($primaryModels);
/** @var ActiveRecord $primaryModel */
$primaryModel = reset($primaryModels);
return $this->asArray()->all($primaryModel->getDb());
}
}

20
framework/yii/data/ActiveDataProvider.php

@ -8,11 +8,11 @@
namespace yii\data; namespace yii\data;
use Yii; use Yii;
use yii\ar\ActiveQueryInterface;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\base\Model; use yii\base\Model;
use yii\db\Query;
use yii\db\ActiveQuery;
use yii\db\Connection; use yii\db\Connection;
use yii\db\QueryInterface;
/** /**
* ActiveDataProvider implements a data provider based on [[Query]] and [[ActiveQuery]]. * ActiveDataProvider implements a data provider based on [[Query]] and [[ActiveQuery]].
@ -54,7 +54,7 @@ use yii\db\Connection;
class ActiveDataProvider extends BaseDataProvider class ActiveDataProvider extends BaseDataProvider
{ {
/** /**
* @var Query the query that is used to fetch data models and [[totalCount]] * @var QueryInterface the query that is used to fetch data models and [[totalCount]]
* if it is not explicitly set. * if it is not explicitly set.
*/ */
public $query; public $query;
@ -97,8 +97,8 @@ class ActiveDataProvider extends BaseDataProvider
*/ */
protected function prepareModels() protected function prepareModels()
{ {
if (!$this->query instanceof Query) { if (!$this->query instanceof QueryInterface) {
throw new InvalidConfigException('The "query" property must be an instance of Query or its subclass.'); throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.');
} }
if (($pagination = $this->getPagination()) !== false) { if (($pagination = $this->getPagination()) !== false) {
$pagination->totalCount = $this->getTotalCount(); $pagination->totalCount = $this->getTotalCount();
@ -125,7 +125,7 @@ class ActiveDataProvider extends BaseDataProvider
} }
} }
return $keys; return $keys;
} elseif ($this->query instanceof ActiveQuery) { } elseif ($this->query instanceof ActiveQueryInterface) {
/** @var \yii\db\ActiveRecord $class */ /** @var \yii\db\ActiveRecord $class */
$class = $this->query->modelClass; $class = $this->query->modelClass;
$pks = $class::primaryKey(); $pks = $class::primaryKey();
@ -154,11 +154,11 @@ class ActiveDataProvider extends BaseDataProvider
*/ */
protected function prepareTotalCount() protected function prepareTotalCount()
{ {
if (!$this->query instanceof Query) { if (!$this->query instanceof QueryInterface) {
throw new InvalidConfigException('The "query" property must be an instance of Query or its subclass.'); throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.');
} }
$query = clone $this->query; $query = clone $this->query;
return (int) $query->limit(-1)->offset(-1)->count('*', $this->db); return (int) $query->limit(-1)->offset(-1)->orderBy([])->count('*', $this->db);
} }
/** /**
@ -167,7 +167,7 @@ class ActiveDataProvider extends BaseDataProvider
public function setSort($value) public function setSort($value)
{ {
parent::setSort($value); parent::setSort($value);
if (($sort = $this->getSort()) !== false && empty($sort->attributes) && $this->query instanceof ActiveQuery) { if (($sort = $this->getSort()) !== false && empty($sort->attributes) && $this->query instanceof ActiveQueryInterface) {
/** @var Model $model */ /** @var Model $model */
$model = new $this->query->modelClass; $model = new $this->query->modelClass;
foreach ($model->attributes() as $attribute) { foreach ($model->attributes() as $attribute) {

202
framework/yii/db/ActiveQuery.php

@ -7,12 +7,13 @@
*/ */
namespace yii\db; namespace yii\db;
use yii\ar\ActiveQueryInterface;
use yii\ar\ActiveQueryTrait;
/** /**
* ActiveQuery represents a DB query associated with an Active Record class. * ActiveQuery represents a DB query associated with an Active Record class.
* *
* ActiveQuery instances are usually created by [[ActiveRecord::find()]], [[ActiveRecord::findBySql()]] * ActiveQuery instances are usually created by [[ActiveRecord::find()]] and [[ActiveRecord::findBySql()]].
* and [[ActiveRecord::count()]].
* *
* ActiveQuery mainly provides the following methods to retrieve the query results: * ActiveQuery mainly provides the following methods to retrieve the query results:
* *
@ -42,23 +43,13 @@ namespace yii\db;
* ~~~ * ~~~
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
class ActiveQuery extends Query class ActiveQuery extends Query implements ActiveQueryInterface
{ {
/** use ActiveQueryTrait;
* @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 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 string the SQL statement to be executed for retrieving AR records. * @var string the SQL statement to be executed for retrieving AR records.
* This is set by [[ActiveRecord::findBySql()]]. * This is set by [[ActiveRecord::findBySql()]].
@ -67,25 +58,6 @@ class ActiveQuery extends Query
/** /**
* PHP magic method.
* This method allows calling static method defined in [[modelClass]] via this query object.
* It is mainly implemented for supporting the feature of scope.
* @param string $name the method name to be called
* @param array $params the parameters passed to the method
* @return mixed the method return result
*/
public function __call($name, $params)
{
if (method_exists($this->modelClass, $name)) {
array_unshift($params, $this);
call_user_func_array([$this->modelClass, $name], $params);
return $this;
} else {
return parent::__call($name, $params);
}
}
/**
* Executes query and returns all results as an array. * Executes query and returns all results as an array.
* @param Connection $db the DB connection used to create the DB command. * @param Connection $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used. * If null, the DB connection returned by [[modelClass]] will be used.
@ -164,164 +136,4 @@ class ActiveQuery extends Query
} }
return $db->createCommand($this->sql, $params); return $db->createCommand($this->sql, $params);
} }
/**
* Sets the [[asArray]] property.
* @param boolean $value whether to return the query results in terms of arrays instead of Active Records.
* @return static the query object itself
*/
public function asArray($value = true)
{
$this->asArray = $value;
return $this;
}
/**
* Specifies the relations with which this query should be performed.
*
* The parameters to this method can be either one or multiple strings, or a single array
* of relation names and the optional callbacks to customize the relations.
*
* A relation name can refer to a relation defined in [[modelClass]]
* or a sub-relation that stands for a relation of a related record.
* For example, `orders.address` means the `address` relation defined
* in the model class corresponding to the `orders` relation.
*
* The followings are some usage examples:
*
* ~~~
* // find customers together with their orders and country
* Customer::find()->with('orders', 'country')->all();
* // find customers together with their orders and the orders' shipping address
* Customer::find()->with('orders.address')->all();
* // find customers together with their country and orders of status 1
* Customer::find()->with([
* 'orders' => function($query) {
* $query->andWhere('status = 1');
* },
* 'country',
* ])->all();
* ~~~
*
* @return static the query object itself
*/
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;
}
/**
* Sets the [[indexBy]] property.
* @param string|callable $column the name of the column by which the query results should be indexed by.
* This can also be a callable (e.g. anonymous function) that returns the index value based on the given
* row or model data. The signature of the callable should be:
*
* ~~~
* // $model is an AR instance when `asArray` is false,
* // or an array of column values when `asArray` is true.
* function ($model)
* {
* // return the index value corresponding to $model
* }
* ~~~
*
* @return static the query object itself
*/
public function indexBy($column)
{
return parent::indexBy($column);
}
private function createModels($rows)
{
$models = [];
if ($this->asArray) {
if ($this->indexBy === null) {
return $rows;
}
foreach ($rows as $row) {
if (is_string($this->indexBy)) {
$key = $row[$this->indexBy];
} else {
$key = call_user_func($this->indexBy, $row);
}
$models[$key] = $row;
}
} else {
/** @var ActiveRecord $class */
$class = $this->modelClass;
if ($this->indexBy === null) {
foreach ($rows as $row) {
$models[] = $class::create($row);
}
} else {
foreach ($rows as $row) {
$model = $class::create($row);
if (is_string($this->indexBy)) {
$key = $model->{$this->indexBy};
} else {
$key = call_user_func($this->indexBy, $model);
}
$models[$key] = $model;
}
}
}
return $models;
}
private function populateRelations(&$models, $with)
{
$primaryModel = new $this->modelClass;
$relations = $this->normalizeRelations($primaryModel, $with);
foreach ($relations as $name => $relation) {
if ($relation->asArray === null) {
// inherit asArray from primary query
$relation->asArray = $this->asArray;
}
$relation->findWith($name, $models);
}
}
/**
* @param ActiveRecord $model
* @param array $with
* @return ActiveRelation[]
*/
private function normalizeRelations($model, $with)
{
$relations = [];
foreach ($with as $name => $callback) {
if (is_integer($name)) {
$name = $callback;
$callback = null;
}
if (($pos = strpos($name, '.')) !== false) {
// with sub-relations
$childName = substr($name, $pos + 1);
$name = substr($name, 0, $pos);
} else {
$childName = null;
}
if (!isset($relations[$name])) {
$relation = $model->getRelation($name);
$relation->primaryModel = null;
$relations[$name] = $relation;
} else {
$relation = $relations[$name];
}
if (isset($childName)) {
$relation->with[$childName] = $callback;
} elseif ($callback !== null) {
call_user_func($callback, $relation);
}
}
return $relations;
}
} }

24
framework/yii/db/ActiveRecord.php

@ -8,6 +8,7 @@
namespace yii\db; namespace yii\db;
use yii\ar\ActiveRelationInterface;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\base\Model; use yii\base\Model;
use yii\base\InvalidParamException; use yii\base\InvalidParamException;
@ -36,6 +37,7 @@ use yii\helpers\Inflector;
* the key value is null). This property is read-only. * the key value is null). This property is read-only.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
class ActiveRecord extends Model class ActiveRecord extends Model
@ -252,7 +254,7 @@ class ActiveRecord extends Model
/** /**
* Creates an [[ActiveQuery]] instance. * Creates an [[ActiveQuery]] instance.
* This method is called by [[find()]], [[findBySql()]] and [[count()]] to start a SELECT query. * This method is called by [[find()]], [[findBySql()]] to start a SELECT query.
* You may override this method to return a customized query (e.g. `CustomerQuery` specified * You may override this method to return a customized query (e.g. `CustomerQuery` specified
* written for querying `Customer` purpose.) * written for querying `Customer` purpose.)
* @return ActiveQuery the newly created [[ActiveQuery]] instance. * @return ActiveQuery the newly created [[ActiveQuery]] instance.
@ -385,7 +387,7 @@ class ActiveRecord extends Model
return $this->_related[$name]; return $this->_related[$name];
} }
$value = parent::__get($name); $value = parent::__get($name);
if ($value instanceof ActiveRelation) { if ($value instanceof ActiveRelationInterface) {
return $this->_related[$name] = $value->multiple ? $value->all() : $value->one(); return $this->_related[$name] = $value->multiple ? $value->all() : $value->one();
} else { } else {
return $value; return $value;
@ -474,7 +476,7 @@ class ActiveRecord extends Model
*/ */
public function hasOne($class, $link) public function hasOne($class, $link)
{ {
return new ActiveRelation([ return $this->createActiveRelation([
'modelClass' => $class, 'modelClass' => $class,
'primaryModel' => $this, 'primaryModel' => $this,
'link' => $link, 'link' => $link,
@ -512,7 +514,7 @@ class ActiveRecord extends Model
*/ */
public function hasMany($class, $link) public function hasMany($class, $link)
{ {
return new ActiveRelation([ return $this->createActiveRelation([
'modelClass' => $class, 'modelClass' => $class,
'primaryModel' => $this, 'primaryModel' => $this,
'link' => $link, 'link' => $link,
@ -521,6 +523,18 @@ class ActiveRecord extends Model
} }
/** /**
* Creates an [[ActiveRelation]] instance.
* This method is called by [[hasOne()]] and [[hasMany()]] to create a relation instance.
* You may override this method to return a customized relation.
* @param array $config the configuration passed to the ActiveRelation class.
* @return ActiveRelation the newly created [[ActiveRelation]] instance.
*/
protected function createActiveRelation($config = [])
{
return new ActiveRelation($config);
}
/**
* Populates the named relation with the related records. * Populates the named relation with the related records.
* Note that this method does not check if the relation exists or not. * Note that this method does not check if the relation exists or not.
* @param string $name the relation name (case-sensitive) * @param string $name the relation name (case-sensitive)
@ -1283,7 +1297,7 @@ class ActiveRecord extends Model
$getter = 'get' . $name; $getter = 'get' . $name;
try { try {
$relation = $this->$getter(); $relation = $this->$getter();
if ($relation instanceof ActiveRelation) { if ($relation instanceof ActiveRelationInterface) {
return $relation; return $relation;
} else { } else {
return null; return null;

238
framework/yii/db/ActiveRelation.php

@ -7,8 +7,8 @@
*/ */
namespace yii\db; namespace yii\db;
use yii\ar\ActiveRelationInterface;
use yii\base\InvalidConfigException; use yii\ar\ActiveRelationTrait;
/** /**
* ActiveRelation represents a relation between two Active Record classes. * ActiveRelation represents a relation between two Active Record classes.
@ -22,63 +22,16 @@ use yii\base\InvalidConfigException;
* *
* If a relation involves a pivot table, it may be specified by [[via()]] or [[viaTable()]] method. * If a relation involves a pivot table, it may be specified by [[via()]] or [[viaTable()]] method.
* *
* @property array|ActiveRelation $via the query associated with the pivot table. Please call [[via()]]
* or [[viaTable()]] to set this property instead of directly setting it.
*
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
class ActiveRelation extends ActiveQuery class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
{
/**
* @var boolean whether this relation should populate all query results into AR instances.
* If false, only the first row of the results will be retrieved.
*/
public $multiple;
/**
* @var ActiveRecord the primary model that this relation is associated with.
* This is used only in lazy loading with dynamic query options.
*/
public $primaryModel;
/**
* @var array the columns of the primary and foreign tables that establish the relation.
* The array keys must be columns of the table for this relation, and the array values
* must be the corresponding columns from the primary table.
* Do not prefix or quote the column names as they will be done automatically by Yii.
*/
public $link;
/**
* @var array|ActiveRelation the query associated with the pivot table. Please call [[via()]]
* or [[viaTable()]] to set this property instead of directly setting it.
*/
public $via;
/**
* Clones internal objects.
*/
public function __clone()
{ {
// make a clone of "via" object so that the same query object can be reused multiple times use ActiveRelationTrait;
if (is_object($this->via)) {
$this->via = clone $this->via;
} elseif (is_array($this->via)) {
$this->via = [$this->via[0], clone $this->via[1]];
}
}
/**
* Specifies the relation associated with the pivot table.
* @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]].
* @param callable $callable a PHP callback for customizing the relation associated with the pivot table.
* Its signature should be `function($query)`, where `$query` is the query to be customized.
* @return static the relation object itself.
*/
public function via($relationName, $callable = null)
{
$relation = $this->primaryModel->getRelation($relationName);
$this->via = [$relationName, $relation];
if ($callable !== null) {
call_user_func($callable, $relation);
}
return $this;
}
/** /**
* Specifies the pivot table. * Specifies the pivot table.
@ -139,179 +92,4 @@ class ActiveRelation extends ActiveQuery
} }
return parent::createCommand($db); return parent::createCommand($db);
} }
/**
* Finds the related records and populates them into the primary models.
* This method is internally used by [[ActiveQuery]]. Do not call it directly.
* @param string $name the relation name
* @param array $primaryModels primary models
* @return array the related models
* @throws InvalidConfigException
*/
public function findWith($name, &$primaryModels)
{
if (!is_array($this->link)) {
throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
}
if ($this->via instanceof self) {
// via pivot table
/** @var ActiveRelation $viaQuery */
$viaQuery = $this->via;
$viaModels = $viaQuery->findPivotRows($primaryModels);
$this->filterByModels($viaModels);
} elseif (is_array($this->via)) {
// via relation
/** @var ActiveRelation $viaQuery */
list($viaName, $viaQuery) = $this->via;
$viaQuery->primaryModel = null;
$viaModels = $viaQuery->findWith($viaName, $primaryModels);
$this->filterByModels($viaModels);
} else {
$this->filterByModels($primaryModels);
}
if (count($primaryModels) === 1 && !$this->multiple) {
$model = $this->one();
foreach ($primaryModels as $i => $primaryModel) {
if ($primaryModel instanceof ActiveRecord) {
$primaryModel->populateRelation($name, $model);
} else {
$primaryModels[$i][$name] = $model;
}
}
return [$model];
} else {
$models = $this->all();
if (isset($viaModels, $viaQuery)) {
$buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link);
} else {
$buckets = $this->buildBuckets($models, $this->link);
}
$link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
foreach ($primaryModels as $i => $primaryModel) {
$key = $this->getModelKey($primaryModel, $link);
$value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
if ($primaryModel instanceof ActiveRecord) {
$primaryModel->populateRelation($name, $value);
} else {
$primaryModels[$i][$name] = $value;
}
}
return $models;
}
}
/**
* @param array $models
* @param array $link
* @param array $viaModels
* @param array $viaLink
* @return array
*/
private function buildBuckets($models, $link, $viaModels = null, $viaLink = null)
{
$buckets = [];
$linkKeys = array_keys($link);
foreach ($models as $i => $model) {
$key = $this->getModelKey($model, $linkKeys);
if ($this->indexBy !== null) {
$buckets[$key][$i] = $model;
} else {
$buckets[$key][] = $model;
}
}
if ($viaModels !== null) {
$viaBuckets = [];
$viaLinkKeys = array_keys($viaLink);
$linkValues = array_values($link);
foreach ($viaModels as $viaModel) {
$key1 = $this->getModelKey($viaModel, $viaLinkKeys);
$key2 = $this->getModelKey($viaModel, $linkValues);
if (isset($buckets[$key2])) {
foreach ($buckets[$key2] as $i => $bucket) {
if ($this->indexBy !== null) {
$viaBuckets[$key1][$i] = $bucket;
} else {
$viaBuckets[$key1][] = $bucket;
}
}
}
}
$buckets = $viaBuckets;
}
if (!$this->multiple) {
foreach ($buckets as $i => $bucket) {
$buckets[$i] = reset($bucket);
}
}
return $buckets;
}
/**
* @param ActiveRecord|array $model
* @param array $attributes
* @return string
*/
private function getModelKey($model, $attributes)
{
if (count($attributes) > 1) {
$key = [];
foreach ($attributes as $attribute) {
$key[] = $model[$attribute];
}
return serialize($key);
} else {
$attribute = reset($attributes);
return $model[$attribute];
}
}
/**
* @param array $models
*/
private function filterByModels($models)
{
$attributes = array_keys($this->link);
$values = [];
if (count($attributes) === 1) {
// single key
$attribute = reset($this->link);
foreach ($models as $model) {
if (($value = $model[$attribute]) !== null) {
$values[] = $value;
}
}
} else {
// composite keys
foreach ($models as $model) {
$v = [];
foreach ($this->link as $attribute => $link) {
$v[$attribute] = $model[$link];
}
$values[] = $v;
}
}
$this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]);
}
/**
* @param ActiveRecord[] $primaryModels
* @return array
*/
private function findPivotRows($primaryModels)
{
if (empty($primaryModels)) {
return [];
}
$this->filterByModels($primaryModels);
/** @var ActiveRecord $primaryModel */
$primaryModel = reset($primaryModels);
$db = $primaryModel->getDb();
list ($sql, $params) = $db->getQueryBuilder()->build($this);
return $db->createCommand($sql, $params)->queryAll();
}
} }

142
framework/yii/db/Query.php

@ -33,20 +33,12 @@ use yii\base\Component;
* ~~~ * ~~~
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
class Query extends Component class Query extends Component implements QueryInterface
{ {
/** use QueryTrait;
* Sort ascending
* @see orderBy()
*/
const SORT_ASC = false;
/**
* Sort descending
* @see orderBy()
*/
const SORT_DESC = true;
/** /**
* @var array the columns being selected. For example, `['id', 'name']`. * @var array the columns being selected. For example, `['id', 'name']`.
@ -71,28 +63,6 @@ class Query extends Component
*/ */
public $from; public $from;
/** /**
* @var string|array query condition. This refers to the WHERE clause in a SQL statement.
* For example, `age > 31 AND team = 1`.
* @see where()
*/
public $where;
/**
* @var integer maximum number of records to be returned. If not set or less than 0, it means no limit.
*/
public $limit;
/**
* @var integer zero-based offset from where the records are to be returned. If not set or
* less than 0, it means starting from the beginning.
*/
public $offset;
/**
* @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement.
* The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which
* can be either [[Query::SORT_ASC]] or [[Query::SORT_DESC]]. The array may also contain [[Expression]] objects.
* If that is the case, the expressions will be converted into strings without any change.
*/
public $orderBy;
/**
* @var array how to group the query results. For example, `['company', 'department']`. * @var array how to group the query results. For example, `['company', 'department']`.
* This is used to construct the GROUP BY clause in a SQL statement. * This is used to construct the GROUP BY clause in a SQL statement.
*/ */
@ -130,12 +100,6 @@ class Query extends Component
* For example, `[':name' => 'Dan', ':age' => 31]`. * For example, `[':name' => 'Dan', ':age' => 31]`.
*/ */
public $params; public $params;
/**
* @var string|callable $column the name of the column by which the query results should be indexed by.
* This can also be a callable (e.g. anonymous function) that returns the index value based on the given
* row data. For more details, see [[indexBy()]]. This property is only used by [[all()]].
*/
public $indexBy;
/** /**
@ -154,27 +118,6 @@ class Query extends Component
} }
/** /**
* Sets the [[indexBy]] property.
* @param string|callable $column the name of the column by which the query results should be indexed by.
* This can also be a callable (e.g. anonymous function) that returns the index value based on the given
* row data. The signature of the callable should be:
*
* ~~~
* function ($row)
* {
* // return the index value corresponding to $row
* }
* ~~~
*
* @return static the query object itself
*/
public function indexBy($column)
{
$this->indexBy = $column;
return $this;
}
/**
* Executes the query and returns all results as an array. * Executes the query and returns all results as an array.
* @param Connection $db the database connection used to generate the SQL statement. * @param Connection $db the database connection used to generate the SQL statement.
* If this parameter is not given, the `db` application component will be used. * If this parameter is not given, the `db` application component will be used.
@ -239,7 +182,7 @@ class Query extends Component
* @param string $q the COUNT expression. Defaults to '*'. * @param string $q the COUNT expression. Defaults to '*'.
* Make sure you properly quote column names in the expression. * Make sure you properly quote column names in the expression.
* @param Connection $db the database connection used to generate the SQL statement. * @param Connection $db the database connection used to generate the SQL statement.
* If this parameter is not given, the `db` application component will be used. * If this parameter is not given (or null), the `db` application component will be used.
* @return integer number of records * @return integer number of records
*/ */
public function count($q = '*', $db = null) public function count($q = '*', $db = null)
@ -653,83 +596,6 @@ class Query extends Component
} }
/** /**
* Sets the ORDER BY part of the query.
* @param string|array $columns the columns (and the directions) to be ordered by.
* Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
* @return static the query object itself
* @see addOrderBy()
*/
public function orderBy($columns)
{
$this->orderBy = $this->normalizeOrderBy($columns);
return $this;
}
/**
* Adds additional ORDER BY columns to the query.
* @param string|array $columns the columns (and the directions) to be ordered by.
* Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
* @return static the query object itself
* @see orderBy()
*/
public function addOrderBy($columns)
{
$columns = $this->normalizeOrderBy($columns);
if ($this->orderBy === null) {
$this->orderBy = $columns;
} else {
$this->orderBy = array_merge($this->orderBy, $columns);
}
return $this;
}
protected function normalizeOrderBy($columns)
{
if (is_array($columns)) {
return $columns;
} else {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
$result = [];
foreach ($columns as $column) {
if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) {
$result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC;
} else {
$result[$column] = self::SORT_ASC;
}
}
return $result;
}
}
/**
* Sets the LIMIT part of the query.
* @param integer $limit the limit. Use null or negative value to disable limit.
* @return static the query object itself
*/
public function limit($limit)
{
$this->limit = $limit;
return $this;
}
/**
* Sets the OFFSET part of the query.
* @param integer $offset the offset. Use null or negative value to disable offset.
* @return static the query object itself
*/
public function offset($offset)
{
$this->offset = $offset;
return $this;
}
/**
* Appends a SQL statement using UNION operator. * Appends a SQL statement using UNION operator.
* @param string|Query $sql the SQL statement to be appended using UNION * @param string|Query $sql the SQL statement to be appended using UNION
* @return static the query object itself * @return static the query object itself

2
framework/yii/db/QueryBuilder.php

@ -683,7 +683,7 @@ class QueryBuilder extends \yii\base\Object
if (is_object($direction)) { if (is_object($direction)) {
$orders[] = (string)$direction; $orders[] = (string)$direction;
} else { } else {
$orders[] = $this->db->quoteColumnName($name) . ($direction === Query::SORT_DESC ? ' DESC' : ''); $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
} }
} }

206
framework/yii/db/QueryInterface.php

@ -0,0 +1,206 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
/**
* The QueryInterface defines the minimum set of methods to be implemented by a database query.
*
* The default implementation of this interface is provided by [[QueryTrait]].
*
* It has support for getting [[one]] instance or [[all]].
* Allows pagination via [[limit]] and [[offset]].
* Sorting is supported via [[orderBy]] and items can be limited to match some conditions using [[where]].
*
* By calling [[createCommand()]], we can get a [[Command]] instance which can be further
* used to perform/execute the DB query against a database.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
interface QueryInterface
{
/**
* Executes the query and returns all results as an array.
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
* @return array the query results. If the query results in nothing, an empty array will be returned.
*/
public function all($db = null);
/**
* Executes the query and returns a single row of result.
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
* @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query
* results in nothing.
*/
public function one($db = null);
/**
* Returns the number of records.
* @param string $q the COUNT expression. Defaults to '*'.
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
* @return integer number of records
*/
public function count($q = '*', $db = null);
/**
* Returns a value indicating whether the query result contains any row of data.
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
* @return boolean whether the query result contains any row of data.
*/
public function exists($db = null);
/**
* Sets the [[indexBy]] property.
* @param string|callable $column the name of the column by which the query results should be indexed by.
* This can also be a callable (e.g. anonymous function) that returns the index value based on the given
* row data. The signature of the callable should be:
*
* ~~~
* function ($row)
* {
* // return the index value corresponding to $row
* }
* ~~~
*
* @return static the query object itself
*/
public function indexBy($column);
/**
* Sets the WHERE part of the query.
*
* The method requires a $condition parameter.
*
* The $condition parameter should be an array in one of the following two formats:
*
* - hash format: `['column1' => value1, 'column2' => value2, ...]`
* - operator format: `[operator, operand1, operand2, ...]`
*
* A condition in hash format represents the following SQL expression in general:
* `column1=value1 AND column2=value2 AND ...`. In case when a value is an array,
* an `IN` expression will be generated. And if a value is null, `IS NULL` will be used
* in the generated expression. Below are some examples:
*
* - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`.
* - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`.
* - `['status' => null] generates `status IS NULL`.
*
* A condition in operator format generates the SQL expression according to the specified operator, which
* can be one of the followings:
*
* - `and`: the operands should be concatenated together using `AND`. For example,
* `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array,
* it will be converted into a string using the rules described here. For example,
* `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`.
* The method will NOT do any quoting or escaping.
*
* - `or`: similar to the `and` operator except that the operands are concatenated using `OR`.
*
* - `between`: operand 1 should be the column name, and operand 2 and 3 should be the
* starting and ending values of the range that the column is in.
* For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`.
*
* - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN`
* in the generated condition.
*
* - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing
* the range of the values that the column or DB expression should be in. For example,
* `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`.
* The method will properly quote the column name and escape values in the range.
*
* - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition.
*
* - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing
* the values that the column or DB expression should be like.
* For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`.
* When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated
* using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate
* `name LIKE '%test%' AND name LIKE '%sample%'`.
* The method will properly quote the column name and escape values in the range.
*
* - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE`
* predicates when operand 2 is an array.
*
* - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE`
* in the generated condition.
*
* - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate
* the `NOT LIKE` predicates.
*
* @param array $condition the conditions that should be put in the WHERE part.
* @return static the query object itself
* @see andWhere()
* @see orWhere()
*/
public function where($condition);
/**
* Adds an additional WHERE condition to the existing one.
* The new condition and the existing one will be joined using the 'AND' operator.
* @param string|array $condition the new WHERE condition. Please refer to [[where()]]
* on how to specify this parameter.
* @return static the query object itself
* @see where()
* @see orWhere()
*/
public function andWhere($condition);
/**
* Adds an additional WHERE condition to the existing one.
* The new condition and the existing one will be joined using the 'OR' operator.
* @param string|array $condition the new WHERE condition. Please refer to [[where()]]
* on how to specify this parameter.
* @return static the query object itself
* @see where()
* @see andWhere()
*/
public function orWhere($condition);
/**
* Sets the ORDER BY part of the query.
* @param string|array $columns the columns (and the directions) to be ordered by.
* Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
* @return static the query object itself
* @see addOrderBy()
*/
public function orderBy($columns);
/**
* Adds additional ORDER BY columns to the query.
* @param string|array $columns the columns (and the directions) to be ordered by.
* Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
* @return static the query object itself
* @see orderBy()
*/
public function addOrderBy($columns);
/**
* Sets the LIMIT part of the query.
* @param integer $limit the limit. Use null or negative value to disable limit.
* @return static the query object itself
*/
public function limit($limit);
/**
* Sets the OFFSET part of the query.
* @param integer $offset the offset. Use null or negative value to disable offset.
* @return static the query object itself
*/
public function offset($offset);
}

208
framework/yii/db/QueryTrait.php

@ -0,0 +1,208 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
/**
* The BaseQuery trait represents the minimum method set of a database Query.
*
* It has support for getting [[one]] instance or [[all]].
* Allows pagination via [[limit]] and [[offset]].
* Sorting is supported via [[orderBy]] and items can be limited to match some conditions unsing [[where]].
*
* By calling [[createCommand()]], we can get a [[Command]] instance which can be further
* used to perform/execute the DB query against a database.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
trait QueryTrait
{
/**
* @var string|array query condition. This refers to the WHERE clause in a SQL statement.
* For example, `age > 31 AND team = 1`.
* @see where()
*/
public $where;
/**
* @var integer maximum number of records to be returned. If not set or less than 0, it means no limit.
*/
public $limit;
/**
* @var integer zero-based offset from where the records are to be returned. If not set or
* less than 0, it means starting from the beginning.
*/
public $offset;
/**
* @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement.
* The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which
* can be either [SORT_ASC](http://php.net/manual/en/array.constants.php#constant.sort-asc)
* or [SORT_DESC](http://php.net/manual/en/array.constants.php#constant.sort-desc).
* The array may also contain [[Expression]] objects. If that is the case, the expressions
* will be converted into strings without any change.
*/
public $orderBy;
/**
* @var string|callable $column the name of the column by which the query results should be indexed by.
* This can also be a callable (e.g. anonymous function) that returns the index value based on the given
* row data. For more details, see [[indexBy()]]. This property is only used by [[all()]].
*/
public $indexBy;
/**
* Sets the [[indexBy]] property.
* @param string|callable $column the name of the column by which the query results should be indexed by.
* This can also be a callable (e.g. anonymous function) that returns the index value based on the given
* row data. The signature of the callable should be:
*
* ~~~
* function ($row)
* {
* // return the index value corresponding to $row
* }
* ~~~
*
* @return static the query object itself
*/
public function indexBy($column)
{
$this->indexBy = $column;
return $this;
}
/**
* Sets the WHERE part of the query.
*
* See [[QueryInterface::where()]] for detailed documentation.
*
* @param array $condition the conditions that should be put in the WHERE part.
* @return static the query object itself
* @see andWhere()
* @see orWhere()
*/
public function where($condition)
{
$this->where = $condition;
return $this;
}
/**
* Adds an additional WHERE condition to the existing one.
* The new condition and the existing one will be joined using the 'AND' operator.
* @param string|array $condition the new WHERE condition. Please refer to [[where()]]
* on how to specify this parameter.
* @return static the query object itself
* @see where()
* @see orWhere()
*/
public function andWhere($condition)
{
if ($this->where === null) {
$this->where = $condition;
} else {
$this->where = ['and', $this->where, $condition];
}
return $this;
}
/**
* Adds an additional WHERE condition to the existing one.
* The new condition and the existing one will be joined using the 'OR' operator.
* @param string|array $condition the new WHERE condition. Please refer to [[where()]]
* on how to specify this parameter.
* @return static the query object itself
* @see where()
* @see andWhere()
*/
public function orWhere($condition)
{
if ($this->where === null) {
$this->where = $condition;
} else {
$this->where = ['or', $this->where, $condition];
}
return $this;
}
/**
* Sets the ORDER BY part of the query.
* @param string|array $columns the columns (and the directions) to be ordered by.
* Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
* @return static the query object itself
* @see addOrderBy()
*/
public function orderBy($columns)
{
$this->orderBy = $this->normalizeOrderBy($columns);
return $this;
}
/**
* Adds additional ORDER BY columns to the query.
* @param string|array $columns the columns (and the directions) to be ordered by.
* Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
* @return static the query object itself
* @see orderBy()
*/
public function addOrderBy($columns)
{
$columns = $this->normalizeOrderBy($columns);
if ($this->orderBy === null) {
$this->orderBy = $columns;
} else {
$this->orderBy = array_merge($this->orderBy, $columns);
}
return $this;
}
protected function normalizeOrderBy($columns)
{
if (is_array($columns)) {
return $columns;
} else {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
$result = [];
foreach ($columns as $column) {
if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) {
$result[$matches[1]] = strcasecmp($matches[2], 'desc') ? SORT_ASC : SORT_DESC;
} else {
$result[$column] = SORT_ASC;
}
}
return $result;
}
}
/**
* Sets the LIMIT part of the query.
* @param integer $limit the limit. Use null or negative value to disable limit.
* @return static the query object itself
*/
public function limit($limit)
{
$this->limit = $limit;
return $this;
}
/**
* Sets the OFFSET part of the query.
* @param integer $offset the offset. Use null or negative value to disable offset.
* @return static the query object itself
*/
public function offset($offset)
{
$this->offset = $offset;
return $this;
}
}

74
tests/unit/framework/data/ActiveDataProviderTest.php

@ -10,6 +10,8 @@ namespace yiiunit\framework\data;
use yii\data\ActiveDataProvider; use yii\data\ActiveDataProvider;
use yii\db\Query; use yii\db\Query;
use yiiunit\data\ar\ActiveRecord; use yiiunit\data\ar\ActiveRecord;
use yiiunit\data\ar\Customer;
use yiiunit\data\ar\Item;
use yiiunit\framework\db\DatabaseTestCase; use yiiunit\framework\db\DatabaseTestCase;
use yiiunit\data\ar\Order; use yiiunit\data\ar\Order;
@ -18,6 +20,7 @@ use yiiunit\data\ar\Order;
* @since 2.0 * @since 2.0
* *
* @group data * @group data
* @group db
*/ */
class ActiveDataProviderTest extends DatabaseTestCase class ActiveDataProviderTest extends DatabaseTestCase
{ {
@ -35,6 +38,8 @@ class ActiveDataProviderTest extends DatabaseTestCase
$orders = $provider->getModels(); $orders = $provider->getModels();
$this->assertEquals(3, count($orders)); $this->assertEquals(3, count($orders));
$this->assertTrue($orders[0] instanceof Order); $this->assertTrue($orders[0] instanceof Order);
$this->assertTrue($orders[1] instanceof Order);
$this->assertTrue($orders[2] instanceof Order);
$this->assertEquals([1, 2, 3], $provider->getKeys()); $this->assertEquals([1, 2, 3], $provider->getKeys());
$provider = new ActiveDataProvider([ $provider = new ActiveDataProvider([
@ -47,6 +52,75 @@ class ActiveDataProviderTest extends DatabaseTestCase
$this->assertEquals(2, count($orders)); $this->assertEquals(2, count($orders));
} }
public function testActiveRelation()
{
/** @var Customer $customer */
$customer = Customer::find(2);
$provider = new ActiveDataProvider([
'query' => $customer->getOrders(),
]);
$orders = $provider->getModels();
$this->assertEquals(2, count($orders));
$this->assertTrue($orders[0] instanceof Order);
$this->assertTrue($orders[1] instanceof Order);
$this->assertEquals([2, 3], $provider->getKeys());
$provider = new ActiveDataProvider([
'query' => $customer->getOrders(),
'pagination' => [
'pageSize' => 1,
]
]);
$orders = $provider->getModels();
$this->assertEquals(1, count($orders));
}
public function testActiveRelationVia()
{
/** @var Order $order */
$order = Order::find(2);
$provider = new ActiveDataProvider([
'query' => $order->getItems(),
]);
$items = $provider->getModels();
$this->assertEquals(3, count($items));
$this->assertTrue($items[0] instanceof Item);
$this->assertTrue($items[1] instanceof Item);
$this->assertTrue($items[2] instanceof Item);
$this->assertEquals([3, 4, 5], $provider->getKeys());
$provider = new ActiveDataProvider([
'query' => $order->getItems(),
'pagination' => [
'pageSize' => 2,
]
]);
$items = $provider->getModels();
$this->assertEquals(2, count($items));
}
public function testActiveRelationViaTable()
{
/** @var Order $order */
$order = Order::find(1);
$provider = new ActiveDataProvider([
'query' => $order->getBooks(),
]);
$items = $provider->getModels();
$this->assertEquals(2, count($items));
$this->assertTrue($items[0] instanceof Item);
$this->assertTrue($items[1] instanceof Item);
$provider = new ActiveDataProvider([
'query' => $order->getBooks(),
'pagination' => [
'pageSize' => 1,
]
]);
$items = $provider->getModels();
$this->assertEquals(1, count($items));
}
public function testQuery() public function testQuery()
{ {
$query = new Query; $query = new Query;

16
tests/unit/framework/db/ActiveRecordTest.php

@ -119,10 +119,19 @@ class ActiveRecordTest extends DatabaseTestCase
{ {
/** @var Customer $customer */ /** @var Customer $customer */
$customer = Customer::find(2); $customer = Customer::find(2);
$this->assertFalse($customer->isRelationPopulated('orders'));
$orders = $customer->orders; $orders = $customer->orders;
$this->assertTrue($customer->isRelationPopulated('orders'));
$this->assertEquals(2, count($orders)); $this->assertEquals(2, count($orders));
$this->assertEquals(1, count($customer->populatedRelations));
/** @var Customer $customer */
$customer = Customer::find(2);
$this->assertFalse($customer->isRelationPopulated('orders'));
$orders = $customer->getOrders()->where('id=3')->all(); $orders = $customer->getOrders()->where('id=3')->all();
$this->assertFalse($customer->isRelationPopulated('orders'));
$this->assertEquals(0, count($customer->populatedRelations));
$this->assertEquals(1, count($orders)); $this->assertEquals(1, count($orders));
$this->assertEquals(3, $orders[0]->id); $this->assertEquals(3, $orders[0]->id);
} }
@ -131,8 +140,15 @@ class ActiveRecordTest extends DatabaseTestCase
{ {
$customers = Customer::find()->with('orders')->all(); $customers = Customer::find()->with('orders')->all();
$this->assertEquals(3, count($customers)); $this->assertEquals(3, count($customers));
$this->assertTrue($customers[0]->isRelationPopulated('orders'));
$this->assertTrue($customers[1]->isRelationPopulated('orders'));
$this->assertEquals(1, count($customers[0]->orders)); $this->assertEquals(1, count($customers[0]->orders));
$this->assertEquals(2, count($customers[1]->orders)); $this->assertEquals(2, count($customers[1]->orders));
$customer = Customer::find()->with('orders')->one();
$this->assertTrue($customer->isRelationPopulated('orders'));
$this->assertEquals(1, count($customer->orders));
$this->assertEquals(1, count($customer->populatedRelations));
} }
public function testFindLazyVia() public function testFindLazyVia()

12
tests/unit/framework/db/QueryTest.php

@ -86,19 +86,19 @@ class QueryTest extends DatabaseTestCase
{ {
$query = new Query; $query = new Query;
$query->orderBy('team'); $query->orderBy('team');
$this->assertEquals(['team' => false], $query->orderBy); $this->assertEquals(['team' => SORT_ASC], $query->orderBy);
$query->addOrderBy('company'); $query->addOrderBy('company');
$this->assertEquals(['team' => false, 'company' => false], $query->orderBy); $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy);
$query->addOrderBy('age'); $query->addOrderBy('age');
$this->assertEquals(['team' => false, 'company' => false, 'age' => false], $query->orderBy); $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy);
$query->addOrderBy(['age' => true]); $query->addOrderBy(['age' => SORT_DESC]);
$this->assertEquals(['team' => false, 'company' => false, 'age' => true], $query->orderBy); $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy);
$query->addOrderBy('age ASC, company DESC'); $query->addOrderBy('age ASC, company DESC');
$this->assertEquals(['team' => false, 'company' => true, 'age' => false], $query->orderBy); $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy);
} }
public function testLimitOffset() public function testLimitOffset()

14
tests/unit/framework/db/cubrid/CubridActiveDataProviderTest.php

@ -0,0 +1,14 @@
<?php
namespace yiiunit\framework\db\cubrid;
use yiiunit\framework\data\ActiveDataProviderTest;
/**
* @group db
* @group cubrid
* @group data
*/
class CubridActiveDataProviderTest extends ActiveDataProviderTest
{
public $driverName = 'cubrid';
}

14
tests/unit/framework/db/mssql/MssqlActiveDataProviderTest.php

@ -0,0 +1,14 @@
<?php
namespace yiiunit\framework\db\mssql;
use yiiunit\framework\data\ActiveDataProviderTest;
/**
* @group db
* @group mssql
* @group data
*/
class MssqlActiveDataProviderTest extends ActiveDataProviderTest
{
public $driverName = 'sqlsrv';
}

14
tests/unit/framework/db/pgsql/PostgreSQLActiveDataProviderTest.php

@ -0,0 +1,14 @@
<?php
namespace yiiunit\framework\db\pgsql;
use yiiunit\framework\data\ActiveDataProviderTest;
/**
* @group db
* @group pgsql
* @group data
*/
class PostgreSQLActiveDataProviderTest extends ActiveDataProviderTest
{
public $driverName = 'pgsql';
}

14
tests/unit/framework/db/sqlite/SqliteActiveDataProviderTest.php

@ -0,0 +1,14 @@
<?php
namespace yiiunit\framework\db\sqlite;
use yiiunit\framework\data\ActiveDataProviderTest;
/**
* @group db
* @group sqlite
* @group data
*/
class SqliteActiveDataProviderTest extends ActiveDataProviderTest
{
public $driverName = 'sqlite';
}
Loading…
Cancel
Save