diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php index 01c7037..fbec6c9 100644 --- a/framework/db/ar/ActiveQuery.php +++ b/framework/db/ar/ActiveQuery.php @@ -15,7 +15,7 @@ use yii\base\VectorIterator; use yii\db\dao\Expression; use yii\db\Exception; -class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, \Countable +class ActiveQuery extends BaseQuery { /** * @var string the name of the ActiveRecord class. @@ -24,7 +24,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, /** * @var array list of relations that this query should be performed with */ - public $with; + public $with = array(); /** * @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. @@ -38,28 +38,12 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, /** * @var array list of scopes that should be applied to this query */ - public $scopes; + public $scopes = array(); /** * @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) { @@ -77,7 +61,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, */ public function all() { - return $this->findRecords(); + return $this->find(); } /** @@ -88,8 +72,7 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, */ public function one() { - $this->limit = 1; - $records = $this->findRecords(); + $records = $this->find(); return isset($records[0]) ? $records[0] : null; } @@ -125,15 +108,6 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, } /** - * 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. @@ -150,139 +124,45 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, 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() + protected function find() { + $modelClass = $this->modelClass; /** - * 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 + * @var ActiveRecord $model */ - } - - protected function findByParent($parent) - { - - } - - protected function findRecords() - { - if (!empty($this->with)) { - return $this->findWithRelations(); - } - + $model = $modelClass::model(); + /** + * @var \yii\db\dao\Connection $db + */ + $db = $model->getDbConnection(); if ($this->sql === null) { if ($this->from === null) { - $modelClass = $this->modelClass; - $tableName = $modelClass::model()->getTableSchema()->name; + $tableName = $model->getTableSchema()->name; $this->from = array($tableName); } - $this->sql = $this->connection->getQueryBuilder()->build($this); + foreach ($this->scopes as $name => $config) { + if (is_integer($name)) { + $model->$config($this); + } else { + array_unshift($config, $this); + call_user_func_array(array($model, $name), $config); + } + } + $this->sql = $db->getQueryBuilder()->build($this); } - $command = $this->connection->createCommand($this->sql, $this->params); + $command = $db->createCommand($this->sql, $this->params); $rows = $command->queryAll(); - return $this->createRecords($rows); - } + $records = $this->createRecords($rows); - protected function findWithRelations() - { - $records = $this->findRecords(); + foreach ($this->with as $name => $config) { + $relation = $model->$name(); + foreach ($config as $p => $v) { + $relation->$p = $v; + } + $relation->findWith($records); + } + + return $records; } protected function createRecords($rows) @@ -311,271 +191,3 @@ class ActiveQuery extends BaseQuery implements \IteratorAggregate, \ArrayAccess, return $records; } } - - - -/** - * 1. eager loading, base limited and has has_many relations - * 2. - * ActiveFinder.php is ... - * - * @property integer $count - * - * @author Qiang Xue - * @since 2.0 - */ -class ActiveQuery2 extends BaseActiveQuery implements \IteratorAggregate, \ArrayAccess, \Countable -{ - /** - * @var string the SQL statement to be executed to retrieve primary records. - * This is set by [[ActiveRecord::findBySql()]]. - */ - public $sql; - /** - * @var array list of query results - */ - 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() - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return $this->records; - } - - /** - * 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() - { - if ($this->records === null) { - $this->limit = 1; - $this->records = $this->findRecords(); - } - return isset($this->records[0]) ? $this->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]); - } - - protected function findRecords() - { - return $this->createFinder()->find($this); - } - - protected function createFinder() - { - return new ActiveFinder($this->getDbConnection()); - } - - public function asArray($value = true) - { - $this->asArray = $value; - return $this; - } - - public function with() - { - $this->with = func_get_args(); - if (isset($this->with[0]) && is_array($this->with[0])) { - // the parameter is given as an array - $this->with = $this->with[0]; - } - return $this; - } - - public function index($column) - { - $this->index = $column; - return $this; - } - - public function tableAlias($value) - { - $this->tableAlias = $value; - return $this; - } - - public function scopes($names) - { - $this->scopes = $names; - return $this; - } -} diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 5162c41..0b3bb81 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -252,7 +252,7 @@ abstract class ActiveRecord extends Model */ public static function createActiveQuery() { - return new ActiveQuery(get_called_class()); + return new ActiveQuery(array('modelClass' => get_called_class())); } /** @@ -382,6 +382,35 @@ abstract class ActiveRecord extends Model } } + public function hasOne($class, $link) + { + return new HasOneRelation(array( + 'modelClass' => $class, + 'parentClass' => get_class($this), + 'link' => $link, + )); + } + + public function hasMany($class, $link) + { + return new HasManyRelation(array( + 'modelClass' => $class, + 'parentClass' => get_class($this), + 'link' => $link, + )); + } + + public function manyMany($class, $leftLink, $joinTable, $rightLink) + { + return new ManyManyRelation(array( + 'modelClass' => $class, + 'parentClass' => get_class($this), + 'leftLink' => $leftLink, + 'joinTable' => $joinTable, + 'rightLink' => $rightLink, + )); + } + /** * Initializes the internal storage for the relation. * This method is internally used by [[ActiveQuery]] when populating relation data. diff --git a/framework/db/ar/HasManyRelation.php b/framework/db/ar/HasManyRelation.php new file mode 100644 index 0000000..b62ed1d --- /dev/null +++ b/framework/db/ar/HasManyRelation.php @@ -0,0 +1,8 @@ +select !== $query->select) { - if (empty($this->select)) { - $this->select = $query->select; - } elseif (!empty($query->select)) { - $select1 = is_string($this->select) ? preg_split('/\s*,\s*/', trim($this->select), -1, PREG_SPLIT_NO_EMPTY) : $this->select; - $select2 = is_string($query->select) ? preg_split('/\s*,\s*/', trim($query->select), -1, PREG_SPLIT_NO_EMPTY) : $query->select; - $this->select = array_merge($select1, array_diff($select2, $select1)); - } - } - - if ($query->selectOption !== null) { - $this->selectOption = $query->selectOption; - } - - if ($query->distinct !== null) { - $this->distinct = $query->distinct; - } - - if ($query->from !== null) { - $this->from = $query->from; - } - - if ($query->limit !== null) { - $this->limit = $query->limit; - } - - if ($query->offset !== null) { - $this->offset = $query->offset; - } - - if ($query->where !== null) { - $this->andWhere($query->where); - } - - if ($query->having !== null) { - $this->andHaving($query->having); - } - - if ($query->params !== null) { - $this->addParams($query->params); - } - - if ($query->orderBy !== null) { - $this->addOrderBy($query->orderBy); - } - - if ($query->groupBy !== null) { - $this->addGroup($query->groupBy); - } - - if ($query->join !== null) { - if (empty($this->join)) { - $this->join = $query->join; - } else { - if (!is_array($this->join)) { - $this->join = array($this->join); - } - if (is_array($query->join)) { - $this->join = array_merge($this->join, $query->join); - } else { - $this->join[] = $query->join; - } + $properties = array( + 'select', 'selectOption', 'distinct', 'from', + 'where', 'limit', 'offset', 'orderBy', 'groupBy', + 'join', 'having', 'union', 'params', + ); + foreach ($properties as $name => $value) { + if ($value !== null) { + $this->$name = $value; } } - - if ($query->union !== null) { - if (empty($this->union)) { - $this->union = $query->union; - } else { - if (!is_array($this->union)) { - $this->union = array($this->union); - } - if (is_array($query->union)) { - $this->union = array_merge($this->union, $query->union); - } else { - $this->union[] = $query->union; - } - } - } - return $this; } } diff --git a/tests/unit/framework/db/ar/ActiveRecordTest.php b/tests/unit/framework/db/ar/ActiveRecordTest.php index 9b169eb..923345b 100644 --- a/tests/unit/framework/db/ar/ActiveRecordTest.php +++ b/tests/unit/framework/db/ar/ActiveRecordTest.php @@ -23,42 +23,14 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertTrue($result instanceof ActiveQuery); $customer = $result->one(); $this->assertTrue($customer instanceof Customer); - $this->assertEquals(1, $result->count); - $this->assertEquals(1, count($result)); // find all $result = Customer::find(); $customers = $result->all(); - $this->assertTrue(is_array($customers)); $this->assertEquals(3, count($customers)); $this->assertTrue($customers[0] instanceof Customer); $this->assertTrue($customers[1] instanceof Customer); $this->assertTrue($customers[2] instanceof Customer); - $this->assertEquals(3, $result->count); - $this->assertEquals(3, count($result)); - - // check count first - $result = Customer::find(); - $this->assertEquals(3, $result->count); - $customer = $result->one(); - $this->assertTrue($customer instanceof Customer); - $this->assertEquals(3, $result->count); - - // iterator - $result = Customer::find(); - $count = 0; - foreach ($result as $customer) { - $this->assertTrue($customer instanceof Customer); - $count++; - } - $this->assertEquals($count, $result->count); - - // array access - $result = Customer::find(); - $this->assertTrue($result[0] instanceof Customer); - $this->assertTrue($result[1] instanceof Customer); - $this->assertTrue($result[2] instanceof Customer); - $this->assertEquals(3, count($result)); // find by a single primary key $customer = Customer::find(2); @@ -70,7 +42,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertTrue($customer instanceof Customer); $this->assertEquals(2, $customer->id); - // find by Query + // find by Query array $query = array( 'where' => 'id=:id', 'params' => array(':id' => 2), @@ -80,13 +52,59 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertEquals('user2', $customer->name); // find count - $this->assertEquals(3, Customer::find()->count()); - $this->assertEquals(3, Customer::count()); - $this->assertEquals(2, Customer::count(array( - 'where' => 'id=1 OR id=2', - ))); - $this->assertEquals(2, Customer::count()->where('id=1 OR id=2')); +// $this->assertEquals(3, Customer::count()); +// $this->assertEquals(2, Customer::count(array( +// 'where' => 'id=1 OR id=2', +// ))); +// $this->assertEquals(2, Customer::find()->select('COUNT(*)')->where('id=1 OR id=2')->value()); + } + + public function testFindBySql() + { + // find one + $customer = Customer::findBySql('SELECT * FROM tbl_customer ORDER BY id DESC')->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user3', $customer->name); + + // find all + $customers = Customer::findBySql('SELECT * FROM tbl_customer')->all(); + $this->assertEquals(3, count($customers)); + + // find with parameter binding + $customer = Customer::findBySql('SELECT * FROM tbl_customer WHERE id=:id', array(':id' => 2))->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + } + + public function testScope() + { + $customers = Customer::find(array( + 'scopes' => array('active'), + ))->all(); + $this->assertEquals(2, count($customers)); + + $customers = Customer::find()->active()->all(); + $this->assertEquals(2, count($customers)); } +// +// public function testFindLazy() +// { +// $customer = Customer::find(2); +// $orders = $customer->orders; +// $this->assertEquals(2, count($orders)); +// +// $orders = $customer->orders()->where('id=3')->all(); +// $this->assertEquals(1, count($orders)); +// $this->assertEquals(3, $orders[0]->id); +// } +// +// public function testFindEager() +// { +// $customers = Customer::find()->with('orders')->all(); +// $this->assertEquals(3, count($customers)); +// $this->assertEquals(1, count($customers[0]->orders)); +// $this->assertEquals(2, count($customers[1]->orders)); +// } // public function testInsert() // {