From 9d8a4012732fc187c74e2b2b7d372893afa949fa Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 10 Feb 2012 11:25:14 -0500 Subject: [PATCH] ... --- framework/YiiBase.php | 11 +- framework/base/Model.php | 8 +- framework/base/ModelBehavior.php | 4 +- framework/base/ModelEvent.php | 27 ++ framework/base/Object.php | 2 +- framework/base/ValidationEvent.php | 28 -- framework/db/ar/ActiveFinder.php | 908 +++++++++++++++++++++++++++++++++++++ framework/db/ar/ActiveMetaData.php | 26 +- framework/db/ar/ActiveQuery.php | 908 ------------------------------------- framework/db/ar/ActiveRecord.php | 731 +++++++++++------------------ framework/db/ar/JoinElement.php | 4 +- framework/util/Text.php | 20 +- 12 files changed, 1259 insertions(+), 1418 deletions(-) create mode 100644 framework/base/ModelEvent.php delete mode 100644 framework/base/ValidationEvent.php create mode 100644 framework/db/ar/ActiveFinder.php delete mode 100644 framework/db/ar/ActiveQuery.php diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 26cd7fa..9e8b8b3 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -75,17 +75,16 @@ class YiiBase ); /** * @var array initial property values that will be applied to objects newly created via [[createObject]]. - * The array keys are fully qualified namespaced class names, and the array values are the corresponding - * name-value pairs for initializing the created class instances. Please make sure class names are starting - * with a backslash. For example, + * The array keys are class names without leading backslashes "\", and the array values are the corresponding + * name-value pairs for initializing the created class instances. For example, * * ~~~ * array( - * '\Bar' => array( + * 'Bar' => array( * 'prop1' => 'value1', * 'prop2' => 'value2', * ), - * '\mycompany\foo\Car' => array( + * 'mycompany\foo\Car' => array( * 'prop1' => 'value1', * 'prop2' => 'value2', * ), @@ -375,7 +374,7 @@ class YiiBase $object = new $class; } - $class = '\\' . get_class($object); + $class = get_class($object); if (isset(\Yii::$objectConfig[$class])) { $config = array_merge(\Yii::$objectConfig[$class], $config); diff --git a/framework/base/Model.php b/framework/base/Model.php index bceea5a..aa361b1 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -207,7 +207,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess public function beforeValidate() { if ($this->hasEventHandlers('onBeforeValidate')) { - $event = new ValidationEvent($this); + $event = new ModelEvent($this); $this->onBeforeValidate($event); return $event->isValid; } @@ -229,7 +229,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * This event is raised before the validation is performed. - * @param ValidationEvent $event the event parameter + * @param ModelEvent $event the event parameter */ public function onBeforeValidate($event) { @@ -457,7 +457,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function generateAttributeLabel($name) { - return Text::name2words($name, true); + return Text::camel2words($name, true); } /** @@ -583,7 +583,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Returns an iterator for traversing the attributes in the model. * This method is required by the interface IteratorAggregate. - * @return CMapIterator an iterator for traversing the items in the list. + * @return DictionaryIterator an iterator for traversing the items in the list. */ public function getIterator() { diff --git a/framework/base/ModelBehavior.php b/framework/base/ModelBehavior.php index fd4b69f..3ead0b7 100644 --- a/framework/base/ModelBehavior.php +++ b/framework/base/ModelBehavior.php @@ -52,9 +52,9 @@ class ModelBehavior extends Behavior /** * Responds to [[Model::onBeforeValidate]] event. * Override this method if you want to handle the corresponding event of the [[owner]]. - * You may set the [[ValidationEvent::isValid|isValid]] property of the event parameter + * You may set the [[ModelEvent::isValid|isValid]] property of the event parameter * to be false to cancel the validation process. - * @param ValidationEvent $event event parameter + * @param ModelEvent $event event parameter */ public function beforeValidate($event) { diff --git a/framework/base/ModelEvent.php b/framework/base/ModelEvent.php new file mode 100644 index 0000000..d09795e --- /dev/null +++ b/framework/base/ModelEvent.php @@ -0,0 +1,27 @@ + + * @since 2.0 + */ +class ModelEvent extends Event +{ + /** + * @var boolean whether the model is in valid status. Defaults to true. + * A model is in valid status if it passes validation, or other checks. + */ + public $isValid = true; +} diff --git a/framework/base/Object.php b/framework/base/Object.php index 551238a..3d9f7a7 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -299,7 +299,7 @@ class Object */ public static function newInstance($config = array()) { - $class = '\\' . get_called_class(); + $class = get_called_class(); if (($n = func_num_args()) > 1) { $args = func_get_args(); diff --git a/framework/base/ValidationEvent.php b/framework/base/ValidationEvent.php deleted file mode 100644 index 2b9db53..0000000 --- a/framework/base/ValidationEvent.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @since 2.0 - */ -class ValidationEvent extends Event -{ - /** - * @var boolean whether the model passes the validation by the event handler. - * Defaults to true. If it is set false, the [[Model::validate|model validation]] will be cancelled. - * @see Model::onBeforeValidate - */ - public $isValid = true; -} diff --git a/framework/db/ar/ActiveFinder.php b/framework/db/ar/ActiveFinder.php new file mode 100644 index 0000000..f651fea --- /dev/null +++ b/framework/db/ar/ActiveFinder.php @@ -0,0 +1,908 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2012 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\ar; + +use yii\base\VectorIterator; +use yii\db\dao\Query; +use yii\db\Exception; + +/** + * ActiveFinder.php is ... + * todo: add SQL monitor + * + * todo: add ActiveFinderBuilder + * todo: quote join/on part of the relational query + * todo: modify QueryBuilder about join() methods + * todo: unify ActiveFinder and ActiveRelation in query building process + * todo: intelligent table aliasing (first table name, then relation name, finally t?) + * todo: allow using tokens in primary query fragments + * todo: findBySql + * todo: base limited + * todo: lazy loading + * todo: scope + * todo: test via option + * + * @author Qiang Xue + * @since 2.0 + */ +class ActiveFinder extends \yii\base\Object implements \IteratorAggregate, \ArrayAccess, \Countable +{ + /** + * @var string the name of the ActiveRecord class. + */ + public $modelClass; + /** + * @var Query the Query object + */ + public $query; + /** + * @var array list of relations that this query should be performed with + */ + public $with; + /** + * @var string the table alias to be used for query + */ + public $tableAlias; + /** + * @var string the name of the column that the result should be indexed by + */ + public $indexBy; + /** + * @var boolean whether to return query results as arrays + */ + public $asArray; + /** + * @var array list of scopes that should be applied to this query + */ + public $scopes; + /** + * @var array list of query results + */ + public $records; + public $sql; + + /** + * @param string $modelClass the name of the ActiveRecord class. + */ + public function __construct($modelClass) + { + $this->modelClass = $modelClass; + $this->query = new Query; + } + + public function all() + { + if ($this->records === null) { + $this->records = $this->findRecords(); + } + return $this->records; + } + + public function one($limitOne = true) + { + if ($this->records === null) { + if ($limitOne) { + $this->limit(1); + } + $this->records = $this->findRecords(); + } + return isset($this->records[0]) ? $this->records[0] : null; + } + + public function exists() + { + // todo + return $this->select(array('1'))->asArray(true)->one() !== null; + } + + 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 indexBy($column) + { + $this->indexBy = $column; + return $this; + } + + public function tableAlias($value) + { + $this->tableAlias = $value; + return $this; + } + + /** + * 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 CCacheDependency $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)`. + * @param boolean $bySql whether to get the count by performing a SQL COUNT query. + * If this is false, it will count the number of records brought back by this query. + * @return integer number of items in the vector. + */ + public function count($bySql = false) + { + if ($bySql) { + return $this->performCountQuery(); + } else { + 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]); + } + + /** + * Sets the SELECT part of the query. + * @param string|array $columns the columns to be selected. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * Columns can contain table prefixes (e.g. "tbl_user.id") and/or column aliases (e.g. "tbl_user.id AS user_id"). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @param string $option additional option that should be appended to the 'SELECT' keyword. For example, + * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. + * @return ActiveFinder the query object itself + */ + public function select($columns, $option = '') + { + $this->query->select($columns, $option); + return $this; + } + + /** + * Sets the value indicating whether to SELECT DISTINCT or not. + * @param bool $value whether to SELECT DISTINCT or not. + * @return ActiveFinder the query object itself + */ + public function distinct($value = true) + { + $this->query->distinct($value); + return $this; + } + + /** + * Sets the FROM part of the query. + * @param string|array $tables the table(s) to be selected from. This can be either a string (e.g. 'tbl_user') + * or an array (e.g. array('tbl_user', 'tbl_profile')) specifying one or several table names. + * Table names can contain schema prefixes (e.g. 'public.tbl_user') and/or table aliases (e.g. 'tbl_user u'). + * The method will automatically quote the table names unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @return ActiveFinder the query object itself + */ + public function from($tables) + { + $this->query->from($tables); + return $this; + } + + /** + * Sets the WHERE part of the query. + * + * The method requires a $condition parameter, and optionally a $params parameter + * specifying the values to be bound to the query. + * + * The $condition parameter should be either a string (e.g. 'id=1') or an array. + * If the latter, it must be in one of the following two formats: + * + * - hash format: `array('column1' => value1, 'column2' => value2, ...)` + * - operator format: `array(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: + * + * - `array('type'=>1, 'status'=>2)` generates `(type=1) AND (status=2)`. + * - `array('id'=>array(1,2,3), 'status'=>2)` generates `(id IN (1,2,3)) AND (status=2)`. + * - `array('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, + * `array('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, + * `array('and', 'type=1', array('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, `array('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, + * `array('in', 'id', array(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, `array('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, `array('like', 'name', array('%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 string|array $condition the conditions that should be put in the WHERE part. + * @param array $params the parameters (name=>value) to be bound to the query. + * For anonymous parameters, they can alternatively be specified as separate parameters to this method. + * For example, `where('type=? AND status=?', 100, 1)`. + * @return ActiveFinder the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition, $params = array()) + { + if (is_array($params)) { + $this->query->where($condition, $params); + } else { + call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); + } + 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. + * @param array $params the parameters (name=>value) to be bound to the query. + * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. + * @return ActiveFinder the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition, $params = array()) + { + if (is_array($params)) { + $this->query->andWhere($condition, $params); + } else { + call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); + } + 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. + * @param array $params the parameters (name=>value) to be bound to the query. + * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. + * @return ActiveFinder the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition, $params = array()) + { + if (is_array($params)) { + $this->query->orWhere($condition, $params); + } else { + call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); + } + return $this; + } + + /** + * Appends an INNER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param string|array $condition the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. + * @return ActiveFinder the query object itself + */ + public function join($table, $condition, $params = array()) + { + if (is_array($params)) { + $this->query->join($table, $condition, $params); + } else { + call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); + } + return $this; + } + + /** + * Appends a LEFT OUTER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param string|array $condition the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query + * @return ActiveFinder the query object itself + */ + public function leftJoin($table, $condition, $params = array()) + { + if (is_array($params)) { + $this->query->leftJoin($table, $condition, $params); + } else { + call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); + } + return $this; + } + + /** + * Appends a RIGHT OUTER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param string|array $condition the join condition that should appear in the ON part. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query + * @return ActiveFinder the query object itself + */ + public function rightJoin($table, $condition, $params = array()) + { + if (is_array($params)) { + $this->query->rightJoin($table, $condition, $params); + } else { + call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); + } + return $this; + } + + /** + * Appends a CROSS JOIN part to the query. + * Note that not all DBMS support CROSS JOIN. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @return ActiveFinder the query object itself + */ + public function crossJoin($table) + { + $this->query->crossJoin($table); + return $this; + } + + /** + * Appends a NATURAL JOIN part to the query. + * Note that not all DBMS support NATURAL JOIN. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @return ActiveFinder the query object itself + */ + public function naturalJoin($table) + { + $this->query->naturalJoin($table); + return $this; + } + + /** + * Sets the GROUP BY part of the query. + * @param string|array $columns the columns to be grouped by. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return ActiveFinder the query object itself + * @see addGroupBy() + */ + public function groupBy($columns) + { + $this->query->groupBy($columns); + return $this; + } + + /** + * Adds additional group-by columns to the existing ones. + * @param string|array $columns additional columns to be grouped by. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return ActiveFinder the query object itself + * @see groupBy() + */ + public function addGroupBy($columns) + { + $this->query->addGroupBy($columns); + return $this; + } + + /** + * Sets the HAVING part of the query. + * @param string|array $condition the conditions to be put after HAVING. + * Please refer to [[where()]] on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. + * @return ActiveFinder the query object itself + * @see andHaving() + * @see orHaving() + */ + public function having($condition, $params = array()) + { + if (is_array($params)) { + $this->query->having($condition, $params); + } else { + call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); + } + return $this; + } + + /** + * Adds an additional HAVING 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 HAVING condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. + * @return ActiveFinder the query object itself + * @see having() + * @see orHaving() + */ + public function andHaving($condition, $params = array()) + { + if (is_array($params)) { + $this->query->andHaving($condition, $params); + } else { + call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); + } + return $this; + } + + /** + * Adds an additional HAVING 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 HAVING condition. Please refer to [[where()]] + * on how to specify this parameter. + * @param array $params the parameters (name=>value) to be bound to the query. + * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. + * @return ActiveFinder the query object itself + * @see having() + * @see andHaving() + */ + public function orHaving($condition, $params = array()) + { + if (is_array($params)) { + $this->query->orHaving($condition, $params); + } else { + call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); + } + 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. array('id ASC', 'name DESC')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return ActiveFinder the query object itself + * @see addOrderBy() + */ + public function orderBy($columns) + { + $this->query->orderBy($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. array('id ASC', 'name DESC')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return ActiveFinder the query object itself + * @see orderBy() + */ + public function addOrderBy($columns) + { + $this->query->addOrderBy($columns); + return $this; + } + + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit + * @return ActiveFinder the query object itself + */ + public function limit($limit) + { + $this->query->limit($limit); + return $this; + } + + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset + * @return ActiveFinder the query object itself + */ + public function offset($offset) + { + $this->query->offset($offset); + return $this; + } + + /** + * Appends a SQL statement using UNION operator. + * @param string|Query $sql the SQL statement to be appended using UNION + * @return ActiveFinder the query object itself + */ + public function union($sql) + { + $this->query->union($sql); + return $this; + } + + public function getParams() + { + return $this->query->params; + } + + /** + * Sets the parameters to be bound to the query. + * @param array list of query parameter values indexed by parameter placeholders. + * For example, `array(':name'=>'Dan', ':age'=>31)`. + * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. + * @return ActiveFinder the query object itself + * @see addParams() + */ + public function params($params) + { + $this->query->params($params); + return $this; + } + + /** + * Adds additional parameters to be bound to the query. + * @param array list of query parameter values indexed by parameter placeholders. + * For example, `array(':name'=>'Dan', ':age'=>31)`. + * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. + * @return ActiveFinder the query object itself + * @see params() + */ + public function addParams($params) + { + $this->query->addParams($params); + return $this; + } + + public function joinWith() + { + // todo: inner join with one or multiple relations as filters + } + + protected function findRecords() + { + if (!empty($this->with)) { + // todo: handle findBySql() and limit cases + $joinTree = $this->buildRelationalQuery(); + } + + if ($this->sql === null) { + $this->initFrom($this->query); + $command = $this->query->createCommand($this->getDbConnection()); + $this->sql = $command->getSql(); + } else { + $command = $this->getDbConnection()->createCommand($this->sql); + $command->bindValues($this->query->params); + } +echo $command->sql; + $rows = $command->queryAll(); + + if (!empty($this->with)) { + foreach ($rows as $row) { + $joinTree->populateData($row); + } + return array_values($joinTree->records); + } + + if ($this->asArray) { + if ($this->indexBy === null) { + return $rows; + } + $records = array(); + foreach ($rows as $row) { + $records[$row[$this->indexBy]] = $row; + } + return $records; + } else { + $records = array(); + $class = $this->modelClass; + if ($this->indexBy === null) { + foreach ($rows as $row) { + $records[] = $class::populateData($row); + } + } else { + $attribute = $this->indexBy; + foreach ($rows as $row) { + $record = $class::populateData($row); + $records[$record->$attribute] = $record; + } + } + return $records; + } + } + + protected function performCountQuery() + { + if ($this->sql === null) { + $this->initFrom($this->query); + $this->query->select = 'COUNT(*)'; + $command = $this->query->createCommand($this->getDbConnection()); + $this->sql = $command->getSql(); + } else { + $command = $this->getDbConnection()->createCommand($this->sql); + $command->bindValues($this->query->params); + } + return $command->queryScalar(); + } + + protected function initFrom($query) + { + if ($query->from === null) { + $modelClass = $this->modelClass; + $tableName = $modelClass::tableName(); + if ($this->tableAlias !== null) { + $tableName .= ' ' . $this->tableAlias; + } + $query->from = array($tableName); + } + } + + protected function buildRelationalQuery() + { + $joinTree = new JoinElement($this, null, null); + $this->buildJoinTree($joinTree, $this->with); + $this->buildTableAlias($joinTree); + $query = new Query; + foreach ($joinTree->children as $child) { + $child->buildQuery($query); + } + + $select = $joinTree->buildSelect($this->query->select); + if (!empty($query->select)) { + $this->query->select = array_merge($select, $query->select); + } else { + $this->query->select = $select; + } + if (!empty($query->where)) { + $this->query->andWhere('(' . implode(') AND (', $query->where) . ')'); + } + if (!empty($query->having)) { + $this->query->andHaving('(' . implode(') AND (', $query->having) . ')'); + } + if (!empty($query->join)) { + if ($this->query->join === null) { + $this->query->join = $query->join; + } else { + $this->query->join = array_merge($this->query->join, $query->join); + } + } + if (!empty($query->orderBy)) { + $this->query->addOrderBy($query->orderBy); + } + if (!empty($query->groupBy)) { + $this->query->addGroupBy($query->groupBy); + } + if (!empty($query->params)) { + $this->query->addParams($query->params); + } + + return $joinTree; + } + + /** + * @param JoinElement $parent + * @param array|string $with + * @param array $config + * @return null|JoinElement + * @throws \yii\db\Exception + */ + protected function buildJoinTree($parent, $with, $config = array()) + { + if (is_array($with)) { + foreach ($with as $name => $value) { + if (is_string($value)) { + $this->buildJoinTree($parent, $value); + } elseif (is_string($name) && is_array($value)) { + $this->buildJoinTree($parent, $name, $value); + } + } + return null; + } + + if (($pos = strrpos($with, '.')) !== false) { + $parent = $this->buildJoinTree($parent, substr($with, 0, $pos)); + $with = substr($with, $pos + 1); + } + + if (isset($parent->children[$with])) { + $child = $parent->children[$with]; + $child->joinOnly = false; + } else { + $modelClass = $parent->relation->modelClass; + $relations = $modelClass::getMetaData()->relations; + if (!isset($relations[$with])) { + throw new Exception("$modelClass has no relation named '$with'."); + } + $relation = clone $relations[$with]; + if ($relation->via !== null && isset($relations[$relation->via])) { + $relation->via = null; + $parent2 = $this->buildJoinTree($parent, $relation->via); + if ($parent2->joinOnly === null) { + $parent2->joinOnly = true; + } + $child = new JoinElement($relation, $parent2, $parent); + } else { + $child = new JoinElement($relation, $parent, $parent); + } + } + + foreach ($config as $name => $value) { + $child->relation->$name = $value; + } + + return $child; + } + + protected function buildTableAlias($element, &$count = 0) + { + if ($element->relation->tableAlias === null) { + $element->relation->tableAlias = 't' . ($count++); + } + foreach ($element->children as $child) { + $this->buildTableAlias($child, $count); + } + } +} diff --git a/framework/db/ar/ActiveMetaData.php b/framework/db/ar/ActiveMetaData.php index 9728ed9..41391bb 100644 --- a/framework/db/ar/ActiveMetaData.php +++ b/framework/db/ar/ActiveMetaData.php @@ -14,6 +14,10 @@ use yii\db\dao\TableSchema; class ActiveMetaData { /** + * @var ActiveMetaData[] list of ActiveMetaData instances indexed by the model class names + */ + public static $instances; + /** * @var TableSchema the table schema information */ public $table; @@ -27,14 +31,32 @@ class ActiveMetaData public $relations = array(); /** + * Returns an instance of ActiveMetaData for the specified model class. + * Note that each model class only has a single ActiveMetaData instance. + * This method will only create the ActiveMetaData instance if it is not previously + * done so for the specified model class. + * @param string $modelClass the model class name. Make sure the class name do NOT have a leading backslash "\". + * @param boolean $refresh whether to recreate the ActiveMetaData instance. Defaults to false. + * @return ActiveMetaData the ActiveMetaData instance for the specified model class. + */ + public static function getInstance($modelClass, $refresh = false) + { + if (isset(self::$instances[$modelClass]) && !$refresh) { + return self::$instances[$modelClass]; + } else { + return self::$instances[$modelClass] = new self($modelClass); + } + } + + /** * Constructor. * @param string $modelClass the model class name */ public function __construct($modelClass) { + $this->modelClass = $modelClass; $tableName = $modelClass::tableName(); $this->table = $modelClass::getDbConnection()->getDriver()->getTableSchema($tableName); - $this->modelClass = $modelClass; if ($this->table === null) { throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'."); } @@ -71,7 +93,7 @@ class ActiveMetaData $relation->name = $matches[1]; $modelClass = $matches[2]; if (strpos($modelClass, '\\') !== false) { - $relation->modelClass = '\\' . ltrim($modelClass, '\\'); + $relation->modelClass = ltrim($modelClass, '\\'); } else { $relation->modelClass = dirname($this->modelClass) . '\\' . $modelClass; } diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php deleted file mode 100644 index 8bbce5d..0000000 --- a/framework/db/ar/ActiveQuery.php +++ /dev/null @@ -1,908 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2012 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -namespace yii\db\ar; - -use yii\base\VectorIterator; -use yii\db\dao\Query; -use yii\db\Exception; - -/** - * ActiveFinder.php is ... - * todo: add SQL monitor - * - * todo: add ActiveQueryBuilder - * todo: quote join/on part of the relational query - * todo: modify QueryBuilder about join() methods - * todo: unify ActiveQuery and ActiveRelation in query building process - * todo: intelligent table aliasing (first table name, then relation name, finally t?) - * todo: allow using tokens in primary query fragments - * todo: findBySql - * todo: base limited - * todo: lazy loading - * todo: scope - * todo: test via option - * - * @author Qiang Xue - * @since 2.0 - */ -class ActiveQuery extends \yii\base\Object implements \IteratorAggregate, \ArrayAccess, \Countable -{ - /** - * @var string the name of the ActiveRecord class. - */ - public $modelClass; - /** - * @var \yii\db\dao\Query the Query object - */ - public $query; - /** - * @var array list of relations that this query should be performed with - */ - public $with; - /** - * @var string the table alias to be used for query - */ - public $tableAlias; - /** - * @var string the name of the column that the result should be indexed by - */ - public $indexBy; - /** - * @var boolean whether to return query results as arrays - */ - public $asArray; - /** - * @var array list of scopes that should be applied to this query - */ - public $scopes; - /** - * @var array list of query results - */ - public $records; - public $sql; - - /** - * @param string $modelClass the name of the ActiveRecord class. - */ - public function __construct($modelClass) - { - $this->modelClass = $modelClass; - $this->query = new Query; - } - - public function all() - { - if ($this->records === null) { - $this->records = $this->findRecords(); - } - return $this->records; - } - - public function one($limitOne = true) - { - if ($this->records === null) { - if ($limitOne) { - $this->limit(1); - } - $this->records = $this->findRecords(); - } - return isset($this->records[0]) ? $this->records[0] : null; - } - - public function exists() - { - // todo - return $this->select(array('1'))->asArray(true)->one() !== null; - } - - 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 indexBy($column) - { - $this->indexBy = $column; - return $this; - } - - public function tableAlias($value) - { - $this->tableAlias = $value; - return $this; - } - - /** - * 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 CCacheDependency $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)`. - * @param boolean $bySql whether to get the count by performing a SQL COUNT query. - * If this is false, it will count the number of records brought back by this query. - * @return integer number of items in the vector. - */ - public function count($bySql = false) - { - if ($bySql) { - return $this->performCountQuery(); - } else { - 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]); - } - - /** - * Sets the SELECT part of the query. - * @param string|array $columns the columns to be selected. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). - * Columns can contain table prefixes (e.g. "tbl_user.id") and/or column aliases (e.g. "tbl_user.id AS user_id"). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @param string $option additional option that should be appended to the 'SELECT' keyword. For example, - * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. - * @return ActiveQuery the query object itself - */ - public function select($columns, $option = '') - { - $this->query->select($columns, $option); - return $this; - } - - /** - * Sets the value indicating whether to SELECT DISTINCT or not. - * @param bool $value whether to SELECT DISTINCT or not. - * @return ActiveQuery the query object itself - */ - public function distinct($value = true) - { - $this->query->distinct($value); - return $this; - } - - /** - * Sets the FROM part of the query. - * @param string|array $tables the table(s) to be selected from. This can be either a string (e.g. 'tbl_user') - * or an array (e.g. array('tbl_user', 'tbl_profile')) specifying one or several table names. - * Table names can contain schema prefixes (e.g. 'public.tbl_user') and/or table aliases (e.g. 'tbl_user u'). - * The method will automatically quote the table names unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @return ActiveQuery the query object itself - */ - public function from($tables) - { - $this->query->from($tables); - return $this; - } - - /** - * Sets the WHERE part of the query. - * - * The method requires a $condition parameter, and optionally a $params parameter - * specifying the values to be bound to the query. - * - * The $condition parameter should be either a string (e.g. 'id=1') or an array. - * If the latter, it must be in one of the following two formats: - * - * - hash format: `array('column1' => value1, 'column2' => value2, ...)` - * - operator format: `array(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: - * - * - `array('type'=>1, 'status'=>2)` generates `(type=1) AND (status=2)`. - * - `array('id'=>array(1,2,3), 'status'=>2)` generates `(id IN (1,2,3)) AND (status=2)`. - * - `array('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, - * `array('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, - * `array('and', 'type=1', array('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, `array('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, - * `array('in', 'id', array(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, `array('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, `array('like', 'name', array('%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 string|array $condition the conditions that should be put in the WHERE part. - * @param array $params the parameters (name=>value) to be bound to the query. - * For anonymous parameters, they can alternatively be specified as separate parameters to this method. - * For example, `where('type=? AND status=?', 100, 1)`. - * @return ActiveQuery the query object itself - * @see andWhere() - * @see orWhere() - */ - public function where($condition, $params = array()) - { - if (is_array($params)) { - $this->query->where($condition, $params); - } else { - call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); - } - 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. - * @param array $params the parameters (name=>value) to be bound to the query. - * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. - * @return ActiveQuery the query object itself - * @see where() - * @see orWhere() - */ - public function andWhere($condition, $params = array()) - { - if (is_array($params)) { - $this->query->andWhere($condition, $params); - } else { - call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); - } - 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. - * @param array $params the parameters (name=>value) to be bound to the query. - * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. - * @return ActiveQuery the query object itself - * @see where() - * @see andWhere() - */ - public function orWhere($condition, $params = array()) - { - if (is_array($params)) { - $this->query->orWhere($condition, $params); - } else { - call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); - } - return $this; - } - - /** - * Appends an INNER JOIN part to the query. - * @param string $table the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @param string|array $condition the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. - * @return ActiveQuery the query object itself - */ - public function join($table, $condition, $params = array()) - { - if (is_array($params)) { - $this->query->join($table, $condition, $params); - } else { - call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); - } - return $this; - } - - /** - * Appends a LEFT OUTER JOIN part to the query. - * @param string $table the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @param string|array $condition the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query - * @return ActiveQuery the query object itself - */ - public function leftJoin($table, $condition, $params = array()) - { - if (is_array($params)) { - $this->query->leftJoin($table, $condition, $params); - } else { - call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); - } - return $this; - } - - /** - * Appends a RIGHT OUTER JOIN part to the query. - * @param string $table the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @param string|array $condition the join condition that should appear in the ON part. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query - * @return ActiveQuery the query object itself - */ - public function rightJoin($table, $condition, $params = array()) - { - if (is_array($params)) { - $this->query->rightJoin($table, $condition, $params); - } else { - call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); - } - return $this; - } - - /** - * Appends a CROSS JOIN part to the query. - * Note that not all DBMS support CROSS JOIN. - * @param string $table the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @return ActiveQuery the query object itself - */ - public function crossJoin($table) - { - $this->query->crossJoin($table); - return $this; - } - - /** - * Appends a NATURAL JOIN part to the query. - * Note that not all DBMS support NATURAL JOIN. - * @param string $table the table to be joined. - * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). - * The method will automatically quote the table name unless it contains some parenthesis - * (which means the table is given as a sub-query or DB expression). - * @return ActiveQuery the query object itself - */ - public function naturalJoin($table) - { - $this->query->naturalJoin($table); - return $this; - } - - /** - * Sets the GROUP BY part of the query. - * @param string|array $columns the columns to be grouped by. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return ActiveQuery the query object itself - * @see addGroupBy() - */ - public function groupBy($columns) - { - $this->query->groupBy($columns); - return $this; - } - - /** - * Adds additional group-by columns to the existing ones. - * @param string|array $columns additional columns to be grouped by. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return ActiveQuery the query object itself - * @see groupBy() - */ - public function addGroupBy($columns) - { - $this->query->addGroupBy($columns); - return $this; - } - - /** - * Sets the HAVING part of the query. - * @param string|array $condition the conditions to be put after HAVING. - * Please refer to [[where()]] on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. - * @return ActiveQuery the query object itself - * @see andHaving() - * @see orHaving() - */ - public function having($condition, $params = array()) - { - if (is_array($params)) { - $this->query->having($condition, $params); - } else { - call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); - } - return $this; - } - - /** - * Adds an additional HAVING 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 HAVING condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. - * @return ActiveQuery the query object itself - * @see having() - * @see orHaving() - */ - public function andHaving($condition, $params = array()) - { - if (is_array($params)) { - $this->query->andHaving($condition, $params); - } else { - call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); - } - return $this; - } - - /** - * Adds an additional HAVING 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 HAVING condition. Please refer to [[where()]] - * on how to specify this parameter. - * @param array $params the parameters (name=>value) to be bound to the query. - * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. - * @return ActiveQuery the query object itself - * @see having() - * @see andHaving() - */ - public function orHaving($condition, $params = array()) - { - if (is_array($params)) { - $this->query->orHaving($condition, $params); - } else { - call_user_func_array(array($this->query, __FUNCTION__), func_get_args()); - } - 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. array('id ASC', 'name DESC')). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return ActiveQuery the query object itself - * @see addOrderBy() - */ - public function orderBy($columns) - { - $this->query->orderBy($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. array('id ASC', 'name DESC')). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return ActiveQuery the query object itself - * @see orderBy() - */ - public function addOrderBy($columns) - { - $this->query->addOrderBy($columns); - return $this; - } - - /** - * Sets the LIMIT part of the query. - * @param integer $limit the limit - * @return ActiveQuery the query object itself - */ - public function limit($limit) - { - $this->query->limit($limit); - return $this; - } - - /** - * Sets the OFFSET part of the query. - * @param integer $offset the offset - * @return ActiveQuery the query object itself - */ - public function offset($offset) - { - $this->query->offset($offset); - return $this; - } - - /** - * Appends a SQL statement using UNION operator. - * @param string|Query $sql the SQL statement to be appended using UNION - * @return ActiveQuery the query object itself - */ - public function union($sql) - { - $this->query->union($sql); - return $this; - } - - public function getParams() - { - return $this->query->params; - } - - /** - * Sets the parameters to be bound to the query. - * @param array list of query parameter values indexed by parameter placeholders. - * For example, `array(':name'=>'Dan', ':age'=>31)`. - * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. - * @return ActiveQuery the query object itself - * @see addParams() - */ - public function params($params) - { - $this->query->params($params); - return $this; - } - - /** - * Adds additional parameters to be bound to the query. - * @param array list of query parameter values indexed by parameter placeholders. - * For example, `array(':name'=>'Dan', ':age'=>31)`. - * Please refer to [[where()]] on alternative syntax of specifying anonymous parameters. - * @return ActiveQuery the query object itself - * @see params() - */ - public function addParams($params) - { - $this->query->addParams($params); - return $this; - } - - public function joinWith() - { - // todo: inner join with one or multiple relations as filters - } - - protected function findRecords() - { - if (!empty($this->with)) { - // todo: handle findBySql() and limit cases - $joinTree = $this->buildRelationalQuery(); - } - - if ($this->sql === null) { - $this->initFrom($this->query); - $command = $this->query->createCommand($this->getDbConnection()); - $this->sql = $command->getSql(); - } else { - $command = $this->getDbConnection()->createCommand($this->sql); - $command->bindValues($this->query->params); - } -echo $command->sql; - $rows = $command->queryAll(); - - if (!empty($this->with)) { - foreach ($rows as $row) { - $joinTree->populateData($row); - } - return array_values($joinTree->records); - } - - if ($this->asArray) { - if ($this->indexBy === null) { - return $rows; - } - $records = array(); - foreach ($rows as $row) { - $records[$row[$this->indexBy]] = $row; - } - return $records; - } else { - $records = array(); - $class = $this->modelClass; - if ($this->indexBy === null) { - foreach ($rows as $row) { - $records[] = $class::populateData($row); - } - } else { - $attribute = $this->indexBy; - foreach ($rows as $row) { - $record = $class::populateData($row); - $records[$record->$attribute] = $record; - } - } - return $records; - } - } - - protected function performCountQuery() - { - if ($this->sql === null) { - $this->initFrom($this->query); - $this->query->select = 'COUNT(*)'; - $command = $this->query->createCommand($this->getDbConnection()); - $this->sql = $command->getSql(); - } else { - $command = $this->getDbConnection()->createCommand($this->sql); - $command->bindValues($this->query->params); - } - return $command->queryScalar(); - } - - protected function initFrom($query) - { - if ($query->from === null) { - $modelClass = $this->modelClass; - $tableName = $modelClass::tableName(); - if ($this->tableAlias !== null) { - $tableName .= ' ' . $this->tableAlias; - } - $query->from = array($tableName); - } - } - - protected function buildRelationalQuery() - { - $joinTree = new JoinElement($this, null, null); - $this->buildJoinTree($joinTree, $this->with); - $this->buildTableAlias($joinTree); - $query = new Query; - foreach ($joinTree->children as $child) { - $child->buildQuery($query); - } - - $select = $joinTree->buildSelect($this->query->select); - if (!empty($query->select)) { - $this->query->select = array_merge($select, $query->select); - } else { - $this->query->select = $select; - } - if (!empty($query->where)) { - $this->query->andWhere('(' . implode(') AND (', $query->where) . ')'); - } - if (!empty($query->having)) { - $this->query->andHaving('(' . implode(') AND (', $query->having) . ')'); - } - if (!empty($query->join)) { - if ($this->query->join === null) { - $this->query->join = $query->join; - } else { - $this->query->join = array_merge($this->query->join, $query->join); - } - } - if (!empty($query->orderBy)) { - $this->query->addOrderBy($query->orderBy); - } - if (!empty($query->groupBy)) { - $this->query->addGroupBy($query->groupBy); - } - if (!empty($query->params)) { - $this->query->addParams($query->params); - } - - return $joinTree; - } - - /** - * @param JoinElement $parent - * @param array|string $with - * @param array $config - * @return null|JoinElement - * @throws \yii\db\Exception - */ - protected function buildJoinTree($parent, $with, $config = array()) - { - if (is_array($with)) { - foreach ($with as $name => $value) { - if (is_string($value)) { - $this->buildJoinTree($parent, $value); - } elseif (is_string($name) && is_array($value)) { - $this->buildJoinTree($parent, $name, $value); - } - } - return null; - } - - if (($pos = strrpos($with, '.')) !== false) { - $parent = $this->buildJoinTree($parent, substr($with, 0, $pos)); - $with = substr($with, $pos + 1); - } - - if (isset($parent->children[$with])) { - $child = $parent->children[$with]; - $child->joinOnly = false; - } else { - $modelClass = $parent->relation->modelClass; - $relations = $modelClass::getMetaData()->relations; - if (!isset($relations[$with])) { - throw new Exception("$modelClass has no relation named '$with'."); - } - $relation = clone $relations[$with]; - if ($relation->via !== null && isset($relations[$relation->via])) { - $relation->via = null; - $parent2 = $this->buildJoinTree($parent, $relation->via); - if ($parent2->joinOnly === null) { - $parent2->joinOnly = true; - } - $child = new JoinElement($relation, $parent2, $parent); - } else { - $child = new JoinElement($relation, $parent, $parent); - } - } - - foreach ($config as $name => $value) { - $child->relation->$name = $value; - } - - return $child; - } - - protected function buildTableAlias($element, &$count = 0) - { - if ($element->relation->tableAlias === null) { - $element->relation->tableAlias = 't' . ($count++); - } - foreach ($element->children as $child) { - $this->buildTableAlias($child, $count); - } - } -} diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index d429c75..6324811 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -10,10 +10,15 @@ namespace yii\db\ar; +use yii\base\Model; +use yii\base\Event; +use yii\base\ModelEvent; use yii\db\Exception; use yii\db\dao\Connection; use yii\db\dao\TableSchema; use yii\db\dao\Query; +use yii\db\dao\Expression; +use yii\util\Text; /** * ActiveRecord is the base class for classes representing relational data. @@ -23,18 +28,16 @@ use yii\db\dao\Query; * * @property array $attributes */ -abstract class ActiveRecord extends \yii\base\Model +abstract class ActiveRecord extends Model { /** - * @var ActiveMetaData[] list of AR metadata indexed by AR class names + * @var array attribute values indexed by attribute names + */ + private $_attributes = array(); + /** + * @var array old attribute values indexed by attribute names. */ - private static $_md; - - private $_new = false; // whether this instance is new or not - private $_attributes = array(); // attribute name => attribute value private $_oldAttributes; - private $_related = array(); // attribute name => related objects - private $_pk; // old primary key value /** * Returns the metadata for this AR class. @@ -43,78 +46,119 @@ abstract class ActiveRecord extends \yii\base\Model */ public static function getMetaData($refresh = false) { - $class = get_called_class(); - if (!$refresh && isset(self::$_md[$class])) { - return self::$_md[$class]; - } else { - return self::$_md[$class] = new ActiveMetaData('\\' . $class); - } + return ActiveMetaData::getInstance(get_called_class(), $refresh); } /** - * @param string|array|Query $q - * @return ActiveQuery - * @throws Exception + * Creates an [[ActiveFinder]] instance for query purpose. + * + * Because [[ActiveFinder]] implements a set of query building methods, + * additional query conditions can be specified by calling these methods. + * + * Below are some usage examples: + * + * ~~~ + * // find all customers + * $customers = Customer::find()->all(); + * // find a single customer whose ID is 10 + * $customer = Customer::find(10)->one(); + * // find all active customers and order them by their age: + * $customers = Customer::find(array('status' => 1))->orderBy('age')->all(); + * ~~~ + * + * @param mixed $q the query parameter. This can be one of the followings: + * + * - a scalar value (integer, string): query by a single primary key value. + * - an array of name-value pairs: query by a set of column values. + * - a [[Query]] object: query by a full query object. + * + * @return ActiveFinder the [[ActiveFinder]] instance for query purpose. + * @throws Exception if the query parameter is invalid. */ public static function find($q = null) { - $query = static::createActiveQuery(); + $finder = static::createActiveFinder(); if ($q instanceof Query) { - $query->query = $q; + $finder->query = $q; } elseif (is_array($q)) { - // query by attributes - $query->where($q); + // query by a set of column values + $finder->where($q); } elseif ($q !== null) { // query by primary key $primaryKey = static::getMetaData()->table->primaryKey; if (count($primaryKey) === 1) { - $query->where(array($primaryKey[0] => $q)); + $finder->where(array($primaryKey[0] => $q)); } else { - throw new Exception('Multiple values are required to query by composite keys.'); + throw new Exception('Multiple values are required to query by composite primary keys.'); } } - return $query; + return $finder; } + /** + * Creates an [[ActiveFinder]] instance and query by a given SQL statement. + * Note that because the SQL statement is already specified, calling further + * query methods (such as `where()`, `orderBy()`) on [[ActiveFinder]] will have no effect. + * @param string $sql the SQL statement to be executed + * @param array $params parameters to be bound to the SQL statement during execution. + * @return ActiveFinder the [[ActiveFinder]] instance + */ public static function findBySql($sql, $params = array()) { if (!is_array($params)) { $params = func_get_args(); - array_shift($params); + unset($params[0]); } - $query = static::createActiveQuery(); - $query->sql = $sql; - return $query->params($params); + $finder = static::createActiveFinder(); + $finder->sql = $sql; + return $finder->params($params); } - public static function updateAll() + public static function updateAll($attributes, $condition = '', $params = array()) { - + $class = get_called_class(); + $query = new Query; + $query->update($class::tableName(), $attributes, $condition, $params); + return $query->createCommand($class::getDbConnection())->execute(); } - public static function updateCounters() + public static function updateCounters($counters, $condition = '', $params = array()) { - + $class = get_called_class(); + $db = $class::getDbConnection(); + foreach ($counters as $name => $value) { + $value = (int)$value; + $quotedName = $db->quoteColumnName($name, true); + $counters[$name] = new Expression($value >= 0 ? "$quotedName+$value" : "$quotedName$value"); + } + $query = new Query; + $query->update($class::tableName(), $counters, $condition, $params); + return $query->createCommand($class::getDbConnection())->execute(); } - public static function deleteAll() + public static function deleteAll($condition = '', $params = array()) { - + $class = get_called_class(); + $query = new Query; + $query->delete($class::tableName(), $condition, $params); + return $query->createCommand($class::getDbConnection())->execute(); } /** - * @return ActiveFinder + * Creates a [[ActiveFinder]] instance. + * This method is mainly called by [[find()]] and [[findBySql()]]. + * @return ActiveFinder the newly created [[ActiveFinder]] instance. */ public static function createActiveFinder() { - return new ActiveFinder('\\' . get_called_class()); + return new ActiveFinder(get_called_class()); } /** - * Returns the database connection used by active record. + * Returns the database connection used by this AR class. * By default, the "db" application component is used as the database connection. * You may override this method if you want to use a different database connection. - * @return Connection the database connection used by active record. + * @return Connection the database connection used by this AR class. */ public static function getDbConnection() { @@ -122,36 +166,24 @@ abstract class ActiveRecord extends \yii\base\Model } /** - * Returns the default named scope that should be implicitly applied to all queries for this model. - * Note, default scope only applies to SELECT queries. It is ignored for INSERT, UPDATE and DELETE queries. - * The default implementation simply returns an empty array. You may override this method - * if the model needs to be queried with some default criteria (e.g. only active records should be returned). - * @return array the query criteria. This will be used as the parameter to the constructor - * of {@link CDbCriteria}. - */ - public static function defaultScope() - { - return array(); - } - - /** - * Returns the name of the associated database table. - * By default this method returns the class name as the table name. + * Declares the name of the database table associated with this AR class. + * By default this method returns the class name as the table name by calling [[Text::camel2id()]]. + * For example, 'Customer' becomes 'customer', and 'OrderDetail' becomes 'order_detail'. * You may override this method if the table is not named after this convention. * @return string the table name */ public static function tableName() { - return basename(get_called_class()); + return Text::camel2id(basename(get_called_class()), '_'); } /** - * Returns the primary key of the associated database table. + * Declares the primary key name for this AR class. * This method is meant to be overridden in case when the table is not defined with a primary key * (for some legacy database). If the table is already defined with 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. - * @return mixed the primary key of the associated database table. + * 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. @@ -161,22 +193,22 @@ abstract class ActiveRecord extends \yii\base\Model } /** - * Declares the relations for this ActiveRecord class. + * Declares the relations for this AR class. * - * Child classes may want to override this method to specify their relations. + * Child classes may override this method to specify their relations. * - * The following shows how to declare relations for a Programmer AR class: + * The following shows how to declare relations for a `Programmer` AR class: * * ~~~ * return array( - * 'manager:Manager' => '?.manager_id = manager.id', + * 'manager:Manager' => '@.id = ?.manager_id', * 'assignments:Assignment[]' => array( - * 'on' => '?.id = assignments.owner_id AND assignments.status=1', - * 'orderBy' => 'assignments.create_time DESC', + * 'on' => '@.owner_id = ?.id AND @.status = 1', + * 'orderBy' => '@.create_time DESC', * ), * 'projects:Project[]' => array( * 'via' => 'assignments', - * 'on' => 'projects.id = assignments.project_id', + * 'on' => '@.id = ?.project_id', * ), * ); * ~~~ @@ -265,6 +297,19 @@ abstract class ActiveRecord extends \yii\base\Model } /** + * Returns the default named scope that should be implicitly applied to all queries for this model. + * Note, default scope only applies to SELECT queries. It is ignored for INSERT, UPDATE and DELETE queries. + * The default implementation simply returns an empty array. You may override this method + * if the model needs to be queried with some default criteria (e.g. only active records should be returned). + * @return array the query criteria. This will be used as the parameter to the constructor + * of {@link CDbCriteria}. + */ + public static function defaultScope() + { + return array(); + } + + /** * 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 @@ -301,50 +346,25 @@ abstract class ActiveRecord extends \yii\base\Model } /** - * Constructor. - * @param string $scenario scenario name. See {@link CModel::scenario} for more details about this parameter. - */ - public function __construct($scenario = 'insert') - { - if ($scenario === null) // internally used by populateData() and model() - { - return; - } - - $this->setScenario($scenario); - $this->setIsNewRecord(true); - } - - /** - * PHP sleep magic method. - * This method ensures that the model meta data reference is set to null. - * @return array - */ - public function __sleep() - { - return array_keys((array)$this); - } - - /** * PHP getter magic method. - * This method is overridden so that AR attributes 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 * @return mixed property value * @see getAttribute */ public function __get($name) { - if (isset($this->_attributes[$name])) { + if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) { return $this->_attributes[$name]; - } elseif (isset($this->getMetaData()->table->columns[$name])) { - return null; - } elseif (isset($this->_related[$name])) { - return $this->_related[$name]; - } elseif (isset($this->getMetaData()->relations[$name])) { - return $this->getRelatedRecord($name); } else { - return parent::__get($name); + $md = $this->getMetaData(); + if (isset($md->table->columns[$name])) { + return null; + } elseif (isset($md->relations[$name])) { + return $this->_attributes[$name] = $this->loadRelatedRecord($md->relations[$name]); + } } + return parent::__get($name); } /** @@ -355,10 +375,9 @@ abstract class ActiveRecord extends \yii\base\Model */ public function __set($name, $value) { - if (isset($this->getMetaData()->table->columns[$name])) { + $md = $this->getMetaData(); + if (isset($md->table->columns[$name]) || isset($md->relations[$name])) { $this->_attributes[$name] = $value; - } elseif (isset($this->getMetaData()->relations[$name])) { - $this->_related[$name] = $value; } else { parent::__set($name, $value); } @@ -375,12 +394,8 @@ abstract class ActiveRecord extends \yii\base\Model { if (isset($this->_attributes[$name])) { return true; - } elseif (isset($this->getMetaData()->columns[$name])) { + } elseif (isset($this->getMetaData()->table->columns[$name]) || isset($this->getMetaData()->relations[$name])) { return false; - } elseif (isset($this->_related[$name])) { - return true; - } elseif (isset($this->getMetaData()->relations[$name])) { - return $this->getRelatedRecord($name) !== null; } else { return parent::__isset($name); } @@ -394,10 +409,9 @@ abstract class ActiveRecord extends \yii\base\Model */ public function __unset($name) { - if (isset($this->getMetaData()->columns[$name])) { + $md = $this->getMetaData(); + if (isset($md->table->columns[$name]) || isset($md->relations[$name])) { unset($this->_attributes[$name]); - } elseif (isset($this->getMetaData()->relations[$name])) { - unset($this->_related[$name]); } else { parent::__unset($name); } @@ -408,39 +422,34 @@ abstract class ActiveRecord extends \yii\base\Model * 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 $parameters method parameters + * @param array $params method parameters * @return mixed the method return value */ - public function __call($name, $parameters) + public function __call($name, $params) { - if (isset($this->getMetaData()->relations[$name])) { - if (empty($parameters)) { - return $this->getRelatedRecord($name, false); - } else { - return $this->getRelatedRecord($name, false, $parameters[0]); - } - } - - $scopes = $this->scopes(); - if (isset($scopes[$name])) { - $this->getDbCriteria()->mergeWith($scopes[$name]); - return $this; + $md = $this->getMetaData(); + if (isset($md->relations[$name])) { + return $this->loadRelatedRecord($md->relations[$name], isset($params[0]) ? $params[0] : array()); } - - return parent::__call($name, $parameters); + return parent::__call($name, $params); } - public function initRelatedRecord($relation) + /** + * Initializes the internal storage for the relation. + * This method is internally used by [[ActiveFinder]] when populating relation data. + * @param ActiveRelation $relation the relation object + */ + public function initRelation($relation) { - $this->_related[$relation->name] = $relation->hasMany ? array() : null; + $this->_attributes[$relation->name] = $relation->hasMany ? array() : null; } public function addRelatedRecord($relation, $record) { if ($relation->hasMany) { - $this->_related[$relation->name][] = $record; + $this->_attributes[$relation->name][] = $record; } else { - $this->_related[$relation->name] = $record; + $this->_attributes[$relation->name] = $record; } } @@ -458,69 +467,16 @@ abstract class ActiveRecord extends \yii\base\Model * @return mixed the related object(s). * @throws Exception if the relation is not specified in {@link relations}. */ - public function getRelatedRecord($name, $refresh = false, $params = array()) + public function loadRelatedRecord($relation, $params = array()) { - if (!$refresh && $params === array() && (isset($this->_related[$name]) || array_key_exists($name, $this->_related))) { - return $this->_related[$name]; - } - - $md = $this->getMetaData(); - if (!isset($md->relations[$name])) { - throw new Exception(Yii::t('yii', '{class} does not have relation "{name}".', array('{class}' => get_class($this), '{name}' => $name))); - } - - Yii::trace('lazy loading ' . get_class($this) . '.' . $name, 'system.db.ar.ActiveRecord'); - $relation = $md->relations[$name]; - if ($this->getIsNewRecord() && !$refresh && ($relation instanceof CHasOneRelation || $relation instanceof CHasManyRelation)) { - return $relation instanceof CHasOneRelation ? null : array(); - } - - if ($params !== array()) // dynamic query - { - $exists = isset($this->_related[$name]) || array_key_exists($name, $this->_related); - if ($exists) { - $save = $this->_related[$name]; - } - $r = array($name => $params); - } else { - $r = $name; - } - unset($this->_related[$name]); - - $finder = new CActiveFinder($this, $r); - $finder->lazyFind($this); - - if (!isset($this->_related[$name])) { - if ($relation instanceof CHasManyRelation) { - $this->_related[$name] = array(); - } elseif ($relation instanceof CStatRelation) { - $this->_related[$name] = $relation->defaultValue; - } else { - $this->_related[$name] = null; - } - } - - if ($params !== array()) { - $results = $this->_related[$name]; - if ($exists) { - $this->_related[$name] = $save; - } else { - unset($this->_related[$name]); + if (is_string($relation)) { + $md = $this->getMetaData(); + if (!isset($md->relations[$relation])) { + throw new Exception(get_class($this) . ' has no relation named "' . $relation . '".'); } - return $results; - } else { - return $this->_related[$name]; + $relation = $md->relations[$relation]; } - } - - /** - * Returns a value indicating whether the named related object(s) has been loaded. - * @param string $name the relation name - * @return boolean a value indicating whether the named related object(s) has been loaded. - */ - public function hasRelated($name) - { - return isset($this->_related[$name]) || array_key_exists($name, $this->_related); + $finder = $this->createActiveFinder(); } /** @@ -530,69 +486,7 @@ abstract class ActiveRecord extends \yii\base\Model */ public function attributeNames() { - return array_keys($this->getMetaData()->columns); - } - - /** - * Returns the text label for the specified attribute. - * This method overrides the parent implementation by supporting - * returning the label defined in relational object. - * In particular, if the attribute name is in the form of "post.author.name", - * then this method will derive the label from the "author" relation's "name" attribute. - * @param string $attribute the attribute name - * @return string the attribute label - * @see generateAttributeLabel - */ - public function getAttributeLabel($attribute) - { - $labels = $this->attributeLabels(); - if (isset($labels[$attribute])) { - return $labels[$attribute]; - } elseif (strpos($attribute, '.') !== false) { - $segs = explode('.', $attribute); - $name = array_pop($segs); - $model = $this; - foreach ($segs as $seg) { - $relations = $model->getMetaData()->relations; - if (isset($relations[$seg])) { - $model = ActiveRecord::model($relations[$seg]->className); - } else { - break; - } - } - return $model->getAttributeLabel($name); - } else { - return $this->generateAttributeLabel($attribute); - } - } - - /** - * Returns the named relation declared for this AR class. - * @param string $name the relation name - * @return CActiveRelation the named relation declared for this AR class. Null if the relation does not exist. - */ - public function getActiveRelation($name) - { - return isset($this->getMetaData()->relations[$name]) ? $this->getMetaData()->relations[$name] : null; - } - - /** - * Returns the metadata of the table that this AR belongs to - * @return CDbTableSchema the metadata of the table that this AR belongs to - */ - public function getTableSchema() - { - return $this->getMetaData()->tableSchema; - } - - /** - * Checks whether this AR has the named attribute - * @param string $name attribute name - * @return boolean whether this AR has the named attribute (table column). - */ - public function hasAttribute($name) - { - return isset($this->getMetaData()->columns[$name]); + return array_keys($this->getMetaData()->table->columns); } /** @@ -608,11 +502,7 @@ abstract class ActiveRecord extends \yii\base\Model */ public function getAttribute($name) { - if (property_exists($this, $name)) { - return $this->$name; - } elseif (isset($this->_attributes[$name])) { - return $this->_attributes[$name]; - } + return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; } /** @@ -620,53 +510,47 @@ abstract class ActiveRecord extends \yii\base\Model * You may also use $this->AttributeName to set the attribute value. * @param string $name the attribute name * @param mixed $value the attribute value. - * @return boolean whether the attribute exists and the assignment is conducted successfully * @see hasAttribute */ public function setAttribute($name, $value) { - if (property_exists($this, $name)) { - $this->$name = $value; - } elseif (isset($this->getMetaData()->table->columns[$name])) { - $this->_attributes[$name] = $value; - } else { - return false; - } - return true; + $this->_attributes[$name] = $value; } /** * Returns all column attribute values. * Note, related objects are not returned. - * @param mixed $names names of attributes whose value needs to be 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 = true) + public function getAttributes($names = null) { - $attributes = $this->_attributes; - foreach ($this->getMetaData()->columns as $name => $column) { - if (property_exists($this, $name)) { - $attributes[$name] = $this->$name; - } elseif ($names === true && !isset($attributes[$name])) { - $attributes[$name] = null; - } + if ($names === null) { + $names = $this->attributeNames(); } - if (is_array($names)) { - $attrs = array(); - foreach ($names as $name) { - if (property_exists($this, $name)) { - $attrs[$name] = $this->$name; - } else { - $attrs[$name] = isset($attributes[$name]) ? $attributes[$name] : null; - } + $values = array(); + foreach ($names as $name) { + $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; + } + return $values; + } + + public function getChangedAttributes($names = null) + { + if ($names === null) { + $names = $this->attributeNames(); + } + $names = array_flip($names); + $attributes = array(); + foreach ($this->_attributes as $name => $value) { + if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) { + $attributes[$name] = $value; } - return $attrs; - } else { - return $attributes; } + return $attributes; } /** @@ -710,7 +594,7 @@ abstract class ActiveRecord extends \yii\base\Model */ public function getIsNewRecord() { - return $this->_new; + return empty($this->_oldAttributes); } /** @@ -720,13 +604,22 @@ abstract class ActiveRecord extends \yii\base\Model */ public function setIsNewRecord($value) { - $this->_new = $value; + if ($value) { + $this->_oldAttributes = null; + } else { + $this->_oldAttributes = array(); + foreach ($this->attributeNames() as $name) { + if (isset($this->_attributes[$name])) { + $this->_oldAttributes[$name] = $this->_attributes[$name]; + } + } + } } /** * This event is raised before the record is saved. - * By setting {@link CModelEvent::isValid} to be false, the normal {@link save()} process will be stopped. - * @param CModelEvent $event the event parameter + * By setting {@link ModelEvent::isValid} to be false, the normal {@link save()} process will be stopped. + * @param ModelEvent $event the event parameter */ public function onBeforeSave($event) { @@ -735,7 +628,7 @@ abstract class ActiveRecord extends \yii\base\Model /** * This event is raised after the record is saved. - * @param CEvent $event the event parameter + * @param Event $event the event parameter */ public function onAfterSave($event) { @@ -744,8 +637,8 @@ abstract class ActiveRecord extends \yii\base\Model /** * This event is raised before the record is deleted. - * By setting {@link CModelEvent::isValid} to be false, the normal {@link delete()} process will be stopped. - * @param CModelEvent $event the event parameter + * By setting {@link ModelEvent::isValid} to be false, the normal {@link delete()} process will be stopped. + * @param ModelEvent $event the event parameter */ public function onBeforeDelete($event) { @@ -754,7 +647,7 @@ abstract class ActiveRecord extends \yii\base\Model /** * This event is raised after the record is deleted. - * @param CEvent $event the event parameter + * @param Event $event the event parameter */ public function onAfterDelete($event) { @@ -770,15 +663,11 @@ abstract class ActiveRecord extends \yii\base\Model * Make sure you call the parent implementation so that the event is raised properly. * @return boolean whether the saving should be executed. Defaults to true. */ - protected function beforeSave() + public function beforeSave() { - if ($this->hasEventHandler('onBeforeSave')) { - $event = new CModelEvent($this); - $this->onBeforeSave($event); - return $event->isValid; - } else { - return true; - } + $event = new ModelEvent($this); + $this->onBeforeSave($event); + return $event->isValid; } /** @@ -787,11 +676,9 @@ abstract class ActiveRecord extends \yii\base\Model * You may override this method to do postprocessing after record saving. * Make sure you call the parent implementation so that the event is raised properly. */ - protected function afterSave() + public function afterSave() { - if ($this->hasEventHandler('onAfterSave')) { - $this->onAfterSave(new CEvent($this)); - } + $this->onAfterSave(new Event($this)); } /** @@ -801,15 +688,11 @@ abstract class ActiveRecord extends \yii\base\Model * Make sure you call the parent implementation so that the event is raised properly. * @return boolean whether the record should be deleted. Defaults to true. */ - protected function beforeDelete() + public function beforeDelete() { - if ($this->hasEventHandler('onBeforeDelete')) { - $event = new CModelEvent($this); - $this->onBeforeDelete($event); - return $event->isValid; - } else { - return true; - } + $event = new ModelEvent($this); + $this->onBeforeDelete($event); + return $event->isValid; } /** @@ -818,45 +701,9 @@ abstract class ActiveRecord extends \yii\base\Model * You may override this method to do postprocessing after the record is deleted. * Make sure you call the parent implementation so that the event is raised properly. */ - protected function afterDelete() + public function afterDelete() { - if ($this->hasEventHandler('onAfterDelete')) { - $this->onAfterDelete(new CEvent($this)); - } - } - - /** - * This method is invoked before an AR finder executes a find call. - * The find calls include {@link find}, {@link findAll}, {@link findByPk}, - * {@link findAllByPk}, {@link findByAttributes} and {@link findAllByAttributes}. - * The default implementation raises the {@link onBeforeFind} event. - * If you override this method, make sure you call the parent implementation - * so that the event is raised properly. - * - * Starting from version 1.1.5, this method may be called with a hidden {@link CDbCriteria} - * parameter which represents the current query criteria as passed to a find method of AR. - */ - protected function beforeFind() - { - if ($this->hasEventHandler('onBeforeFind')) { - $event = new CModelEvent($this); - // for backward compatibility - $event->criteria = func_num_args() > 0 ? func_get_arg(0) : null; - $this->onBeforeFind($event); - } - } - - /** - * This method is invoked after each record is instantiated by a find method. - * The default implementation raises the {@link onAfterFind} event. - * You may override this method to do postprocessing after each newly found record is instantiated. - * Make sure you call the parent implementation so that the event is raised properly. - */ - protected function afterFind() - { - if ($this->hasEventHandler('onAfterFind')) { - $this->onAfterFind(new CEvent($this)); - } + $this->onAfterDelete(new Event($this)); } /** @@ -869,36 +716,30 @@ abstract class ActiveRecord extends \yii\base\Model * @param array $attributes list of attributes that need to be saved. Defaults to null, * meaning all attributes that are loaded from DB will be saved. * @return boolean whether the attributes are valid and the record is inserted successfully. - * @throws CException if the record is not new + * @throws Exception if the record is not new */ public function insert($attributes = null) { - if (!$this->getIsNewRecord()) { - throw new Exception(Yii::t('yii', 'The active record cannot be inserted to database because it is not new.')); - } if ($this->beforeSave()) { - Yii::trace(get_class($this) . '.insert()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $table = $this->getMetaData()->tableSchema; - $command = $builder->createInsertCommand($table, $this->getAttributes($attributes)); + $db = $this->getDbConnection(); + $query = new Query; + $values = $this->getChangedAttributes($attributes); + $command = $query->insert($this->tableName(), $values)->createCommand($db); if ($command->execute()) { - $primaryKey = $table->primaryKey; + $table = $this->getMetaData()->table; if ($table->sequenceName !== null) { - if (is_string($primaryKey) && $this->$primaryKey === null) { - $this->$primaryKey = $builder->getLastInsertID($table); - } elseif (is_array($primaryKey)) { - foreach ($primaryKey as $pk) { - if ($this->$pk === null) { - $this->$pk = $builder->getLastInsertID($table); - break; - } + foreach ($table->primaryKey as $name) { + if ($this->$name === null) { + $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName); + break; } } } - $this->_pk = $this->getPrimaryKey(); + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $this->_attributes[$name]; + } $this->afterSave(); $this->setIsNewRecord(false); - $this->setScenario('update'); return true; } } @@ -912,21 +753,20 @@ abstract class ActiveRecord extends \yii\base\Model * @param array $attributes list of attributes that need to be saved. Defaults to null, * meaning all attributes that are loaded from DB will be saved. * @return boolean whether the update is successful - * @throws CException if the record is new + * @throws Exception if the record is new */ public function update($attributes = null) { - if ($this->getIsNewRecord()) { - throw new Exception(Yii::t('yii', 'The active record cannot be updated because it is new.')); - } if ($this->beforeSave()) { - Yii::trace(get_class($this) . '.update()', 'system.db.ar.ActiveRecord'); - if ($this->_pk === null) { - $this->_pk = $this->getPrimaryKey(); + $values = $this->getChangedAttributes($attributes); + if ($values !== array()) { + $this->updateAll($values, $this->getOldPrimaryKey(true)); + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $this->_attributes[$name]; + } } - $this->updateByPk($this->getOldPrimaryKey(), $this->getAttributes($attributes)); - $this->_pk = $this->getPrimaryKey(); $this->afterSave(); + $this->setIsNewRecord(false); return true; } else { return false; @@ -936,25 +776,26 @@ abstract class ActiveRecord extends \yii\base\Model /** * Saves a selected list of attributes. * Unlike {@link save}, this method only saves the specified attributes - * of an existing row dataset and does NOT call either {@link beforeSave} or {@link afterSave}. - * Also note that this method does neither attribute filtering nor validation. + * of an existing row and does NOT call either {@link beforeSave} or {@link afterSave}. + * Also note that this method does not validate attributes. * So do not use this method with untrusted data (such as user posted data). * You may consider the following alternative if you want to do so: - *
-	 * $postRecord=Post::model()->findByPk($postID);
-	 * $postRecord->attributes=$_POST['post'];
-	 * $postRecord->save();
-	 * 
+ * + * ~~~ + * $user = User::find($id)->one; + * $user->attributes = $_POST['User']; + * $user->save(); + * ~~~ + * * @param array $attributes attributes to be updated. Each element represents an attribute name * or an attribute value indexed by its name. If the latter, the record's * attribute will be changed accordingly before saving. * @return boolean whether the update is successful - * @throws CException if the record is new or any database error + * @throws Exception if the record is new or any database error */ public function saveAttributes($attributes) { if (!$this->getIsNewRecord()) { - Yii::trace(get_class($this) . '.saveAttributes()', 'system.db.ar.ActiveRecord'); $values = array(); foreach ($attributes as $name => $value) { if (is_integer($name)) { @@ -963,17 +804,13 @@ abstract class ActiveRecord extends \yii\base\Model $values[$name] = $this->$name = $value; } } - if ($this->_pk === null) { - $this->_pk = $this->getPrimaryKey(); - } - if ($this->updateByPk($this->getOldPrimaryKey(), $values) > 0) { - $this->_pk = $this->getPrimaryKey(); - return true; - } else { - return false; + $this->updateAll($values, $this->getOldPrimaryKey(true)); + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $value; } + return true; } else { - throw new Exception(Yii::t('yii', 'The active record cannot be updated because it is new.')); + throw new Exception('The active record cannot be updated because it is new.'); } } @@ -993,59 +830,55 @@ abstract class ActiveRecord extends \yii\base\Model */ public function saveCounters($counters) { - Yii::trace(get_class($this) . '.saveCounters()', 'system.db.ar.ActiveRecord'); - $builder = $this->getCommandBuilder(); - $table = $this->getTableSchema(); - $criteria = $builder->createPkCriteria($table, $this->getOldPrimaryKey()); - $command = $builder->createUpdateCounterCommand($this->getTableSchema(), $counters, $criteria); - if ($command->execute()) { + if (!$this->getIsNewRecord()) { + $this->updateCounters($counters, $this->getOldPrimaryKey(true)); foreach ($counters as $name => $value) { - $this->$name = $this->$name + $value; + $this->$name += $value; + $this->_oldAttributes[$name] = $this->$name; } return true; } else { - return false; + throw new Exception('The active record cannot be updated because it is new.'); } } /** * Deletes the row corresponding to this active record. * @return boolean whether the deletion is successful. - * @throws CException if the record is new + * @throws Exception if the record is new */ public function delete() { if (!$this->getIsNewRecord()) { - Yii::trace(get_class($this) . '.delete()', 'system.db.ar.ActiveRecord'); if ($this->beforeDelete()) { - $result = $this->deleteByPk($this->getPrimaryKey()) > 0; + $result = $this->deleteAll($this->getPrimaryKey(true)) > 0; + $this->_oldAttributes = null; $this->afterDelete(); return $result; } else { return false; } } else { - throw new Exception(Yii::t('yii', 'The active record cannot be deleted because it is new.')); + throw new Exception('The active record cannot be deleted because it is new.'); } } /** * Repopulates this active record with the latest data. + * @param array $attributes * @return boolean whether the row still exists in the database. If true, the latest data will be populated to this active record. */ - public function refresh() + public function refresh($attributes = null) { - Yii::trace(get_class($this) . '.refresh()', 'system.db.ar.ActiveRecord'); - if (!$this->getIsNewRecord() && ($record = $this->findByPk($this->getPrimaryKey())) !== null) { + if (!$this->getIsNewRecord() && ($record = $this->find($this->getPrimaryKey(true))) !== null) { + if ($attributes === null) { + $attributes = $this->attributeNames(); + } $this->_attributes = array(); - $this->_related = array(); - foreach ($this->getMetaData()->columns as $name => $column) { - if (property_exists($this, $name)) { - $this->$name = $record->$name; - } else { - $this->_attributes[$name] = $record->$name; - } + foreach ($attributes as $name) { + $this->_attributes[$name] = $record->_attributes[$name]; } + $this->_oldAttributes = $this->_attributes; return true; } else { return false; @@ -1065,13 +898,15 @@ abstract class ActiveRecord extends \yii\base\Model /** * Returns the primary key value. + * @param boolean $asArray whether to return the primary key value as an array. If true, + * the return value will be an array with column name as key and column value as value. * @return mixed the primary key value. An array (column name=>column value) is returned if the primary key is composite. * If primary key is not defined, null will be returned. */ - public function getPrimaryKey() + public function getPrimaryKey($asArray = false) { $table = static::getMetaData()->table; - if (count($table->primaryKey) === 1) { + if (count($table->primaryKey) === 1 && !$asArray) { return $this->{$table->primaryKey[0]}; } else { $values = array(); @@ -1083,44 +918,27 @@ abstract class ActiveRecord extends \yii\base\Model } /** - * Sets the primary key value. - * After calling this method, the old primary key value can be obtained from {@link oldPrimaryKey}. - * @param mixed $value the new primary key value. If the primary key is composite, the new value - * should be provided as an array (column name=>column value). - */ - public function setPrimaryKey($value) - { - $this->_pk = $this->getPrimaryKey(); - $table = $this->getMetaData()->table; - if (count($table->primaryKey) === 1) { - $this->{$table->primaryKey[0]} = $value; - } else { - foreach ($table->primaryKey as $name) { - $this->$name = $value[$name]; - } - } - } - - /** * Returns the old primary key value. * This refers to the primary key value that is populated into the record * after executing a find method (e.g. find(), findAll()). * The value remains unchanged even if the primary key attribute is manually assigned with a different value. + * @param boolean $asArray whether to return the primary key value as an array. If true, + * the return value will be an array with column name as key and column value as value. * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key is composite. * If primary key is not defined, null will be returned. */ - public function getOldPrimaryKey() + public function getOldPrimaryKey($asArray = false) { - return $this->_pk; - } - - /** - * Sets the old primary key value. - * @param mixed $value the old primary key value. - */ - public function setOldPrimaryKey($value) - { - $this->_pk = $value; + $table = static::getMetaData()->table; + if (count($table->primaryKey) === 1 && !$asArray) { + return isset($this->_oldAttributes[$table->primaryKey[0]]) ? $this->_oldAttributes[$table->primaryKey[0]] : null; + } else { + $values = array(); + foreach ($table->primaryKey as $name) { + $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; + } + return $values; + } } /** @@ -1135,16 +953,13 @@ abstract class ActiveRecord extends \yii\base\Model public static function populateData($row) { $record = static::instantiate($row); - $record->setScenario('update'); $columns = static::getMetaData()->table->columns; foreach ($row as $name => $value) { - if (property_exists($record, $name)) { - $record->$name = $value; - } elseif (isset($columns[$name])) { + if (isset($columns[$name])) { $record->_attributes[$name] = $value; } } - $record->_pk = $record->getPrimaryKey(); + $record->_oldAttributes = $record->_attributes; return $record; } @@ -1158,7 +973,7 @@ abstract class ActiveRecord extends \yii\base\Model * @param array $row list of attribute values for the active records. * @return ActiveRecord the active record */ - protected static function instantiate($row) + public static function instantiate($row) { return static::newInstance(); } diff --git a/framework/db/ar/JoinElement.php b/framework/db/ar/JoinElement.php index 2f4a71a..12ddb09 100644 --- a/framework/db/ar/JoinElement.php +++ b/framework/db/ar/JoinElement.php @@ -1,6 +1,6 @@ * @link http://www.yiiframework.com/ @@ -80,7 +80,7 @@ class JoinElement extends \yii\base\Object $record = $modelClass::populateData($attributes); foreach ($this->children as $child) { if ($child->relation->select !== false) { - $record->initRelatedRecord($child->relation); + $record->initRelation($child->relation); } } $this->records[$pk] = $record; diff --git a/framework/util/Text.php b/framework/util/Text.php index e2834ce..7a773e2 100644 --- a/framework/util/Text.php +++ b/framework/util/Text.php @@ -57,26 +57,32 @@ class Text } /** - * Converts a class name into space-separated words. - * For example, 'PostTag' will be converted as 'Post Tag'. + * Converts a CamelCase name into space-separated words. + * For example, 'PostTag' will be converted to 'Post Tag'. * @param string $name the string to be converted * @param boolean $ucwords whether to capitalize the first letter in each word * @return string the resulting words */ - public static function name2words($name, $ucwords = true) + public static function camel2words($name, $ucwords = true) { $label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(?