From a92acf65cee8f0a37d76776a0a51c24942691661 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 25 Jan 2012 22:36:18 -0500 Subject: [PATCH] ... --- framework/base/Component.php | 139 ++++-- framework/base/Model.php | 128 ++---- framework/db/ar/ActiveMetaData.php | 76 +++ framework/db/ar/ActiveQuery.php | 233 ++++++++++ framework/db/ar/ActiveRecord.php | 500 ++------------------ framework/db/ar/ActiveRelation.php | 43 ++ framework/db/dao/BaseQuery.php | 715 ++++++++++++++++++++++++++++ framework/db/dao/Query.php | 716 +---------------------------- framework/util/Text.php | 23 +- tests/unit/framework/base/BehaviorTest.php | 23 +- 10 files changed, 1300 insertions(+), 1296 deletions(-) create mode 100644 framework/db/ar/ActiveMetaData.php create mode 100644 framework/db/ar/ActiveQuery.php create mode 100644 framework/db/ar/ActiveRelation.php create mode 100644 framework/db/dao/BaseQuery.php diff --git a/framework/base/Component.php b/framework/base/Component.php index 5d96cb6..1705904 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -29,12 +29,12 @@ namespace yii\base; * Event names are case-insensitive. * * An event can be attached with one or multiple PHP callbacks, called *event handlers*. - * One can call [[raiseEvent]] to raise an event. When an event is raised, the attached + * One can call [[raiseEvent()]] to raise an event. When an event is raised, the attached * event handlers will be invoked automatically in the order they are attached to the event. * * To attach an event handler to an event, call [[attachEventHandler]]. Alternatively, * you can use the assignment syntax: `$component->onClick = $callback;`, - * where `$callback` refers to a valid PHP callback, which can be one of the followings: + * where `$callback` refers to a valid PHP callback which can be one of the followings: * * - global function: `'handleOnClick'` * - object method: `array($object, 'handleOnClick')` @@ -59,21 +59,25 @@ namespace yii\base; * ~~~ * * - * A behavior is an instance of [[Behavior]] or its child class. When a behavior is - * attached to a component, its public properties and methods can be accessed via the - * component directly, as if the component owns those properties and methods. + * A behavior is an instance of [[Behavior]] or its child class. A component can be attached + * with one or multiple behaviors. When a behavior is attached to a component, its public + * properties and methods can be accessed via the component directly, as if the component owns + * those properties and methods. * - * Multiple behaviors can be attached to the same component. - * - * To attach a behavior to a component, call [[attachBehavior]]; to detach a behavior - * from the component, call [[detachBehavior]]. + * To attach a behavior to a component, declare it in [[behaviors()]], or explicitly call [[attachBehavior]]. * * @author Qiang Xue * @since 2.0 */ class Component extends Object { + /** + * @var Vector[] the attached event handlers (event name => handlers) + */ private $_e; + /** + * @var Behavior[] the attached behaviors (behavior name => behavior) + */ private $_b; /** @@ -104,12 +108,11 @@ class Component extends Object $this->_e[$name] = new Vector; } return $this->_e[$name]; - } elseif (isset($this->_b[$name])) { // behavior - return $this->_b[$name]; - } elseif (is_array($this->_b)) { // a behavior property - foreach ($this->_b as $object) { - if ($object->canGetProperty($name)) { - return $object->$name; + } else { // behavior property + $this->ensureBehaviors(); + foreach ($this->_b as $behavior) { + if ($behavior->canGetProperty($name)) { + return $behavior->$name; } } } @@ -143,10 +146,11 @@ class Component extends Object $this->_e[$name] = new Vector; } return $this->_e[$name]->add($value); - } elseif (is_array($this->_b)) { // behavior - foreach ($this->_b as $object) { - if ($object->canSetProperty($name)) { - return $object->$name = $value; + } else { // behavior property + $this->ensureBehaviors(); + foreach ($this->_b as $behavior) { + if ($behavior->canSetProperty($name)) { + return $behavior->$name = $value; } } } @@ -178,12 +182,11 @@ class Component extends Object } elseif (method_exists($this, $name) && strncasecmp($name, 'on', 2) === 0) { // has event handler $name = strtolower($name); return isset($this->_e[$name]) && $this->_e[$name]->getCount(); - } elseif (isset($this->_b[$name])) { // has behavior - return true; - } elseif (is_array($this->_b)) { - foreach ($this->_b as $object) { - if ($object->canGetProperty($name)) { - return $object->$name !== null; + } else { // behavior property + $this->ensureBehaviors(); + foreach ($this->_b as $behavior) { + if ($behavior->canGetProperty($name)) { + return $behavior->$name !== null; } } } @@ -207,16 +210,17 @@ class Component extends Object { $setter = 'set' . $name; if (method_exists($this, $setter)) { // write property - return $this->$setter(null); + $this->$setter(null); + return; } elseif (method_exists($this, $name) && strncasecmp($name, 'on', 2) === 0) { // event unset($this->_e[strtolower($name)]); return; - } elseif (isset($this->_b[$name])) { // behavior - return $this->detachBehavior($name); - } elseif (is_array($this->_b)) { // behavior property - foreach ($this->_b as $object) { - if ($object->canSetProperty($name)) { - return $object->$name = null; + } else { // behavior property + $this->ensureBehaviors(); + foreach ($this->_b as $behavior) { + if ($behavior->canSetProperty($name)) { + $behavior->$name = null; + return; } } } @@ -247,14 +251,42 @@ class Component extends Object } } - if ($this->_b !== null) { - foreach ($this->_b as $object) { - if (method_exists($object, $name)) { - return call_user_func_array(array($object, $name), $params); - } + $this->ensureBehaviors(); + foreach ($this->_b as $object) { + if (method_exists($object, $name)) { + return call_user_func_array(array($object, $name), $params); } } - throw new Exception('Unknown method: ' . get_class($this) . "::$name()"); + + throw new Exception('Calling unknown method: ' . get_class($this) . "::$name()"); + } + + /** + * Returns a list of behaviors that this component should behave as. + * + * Child classes may override this method to specify the behaviors they want to behave as. + * + * The return value of this method should be an array of behavior objects or configurations + * indexed by behavior names. A behavior configuration can be either a string specifying + * the behavior class or an array of the following structure: + * + * ~~~ + * 'behaviorName' => array( + * 'class' => 'BehaviorClass', + * 'property1' => 'value1', + * 'property2' => 'value2', + * ) + * ~~~ + * + * Note that a behavior class must extend from [[Behavior]]. + * + * Behaviors declared in this method will be attached to the component on demand. + * + * @return array the behavior configurations. + */ + public function behaviors() + { + return array(); } /** @@ -270,12 +302,13 @@ class Component extends Object } /** - * Returns a value indicating whether there is any handler attached to the event. + * Returns a value indicating whether there is any handler attached to the named event. * @param string $name the event name * @return boolean whether there is any handler attached to the event. */ public function hasEventHandlers($name) { + $this->ensureBehaviors(); $name = strtolower($name); return isset($this->_e[$name]) && $this->_e[$name]->getCount(); } @@ -365,6 +398,7 @@ class Component extends Object */ public function raiseEvent($name, $event) { + $this->ensureBehaviors(); $name = strtolower($name); if ($event instanceof Event) { $event->name = $name; @@ -406,6 +440,7 @@ class Component extends Object */ public function asa($behavior) { + $this->ensureBehaviors(); return isset($this->_b[$behavior]) ? $this->_b[$behavior] : null; } @@ -415,11 +450,11 @@ class Component extends Object * configuration. After that, the behavior object will be attached to * this component by calling the [[Behavior::attach]] method. * @param string $name the behavior's name. It should uniquely identify this behavior. - * @param mixed $behavior the behavior configuration. This can be one of the following: + * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following: * * - a [[Behavior]] object * - a string specifying the behavior class - * - an object configuration array that will be passed to [[\Yii::createObject]] to create the behavior object. + * - an object configuration array that will be passed to [[\Yii::createObject()]] to create the behavior object. * * @return Behavior the behavior object * @see detachBehavior @@ -457,10 +492,12 @@ class Component extends Object public function detachBehavior($name) { if (isset($this->_b[$name])) { - $this->_b[$name]->detach($this); $behavior = $this->_b[$name]; unset($this->_b[$name]); + $behavior->detach($this); return $behavior; + } else { + return null; } } @@ -470,10 +507,24 @@ class Component extends Object public function detachBehaviors() { if ($this->_b !== null) { - foreach ($this->_b as $name => $behavior) { + $behaviors = $this->_b; + $this->_b = null; + foreach ($behaviors as $name => $behavior) { $this->detachBehavior($name); } - $this->_b = null; + } + } + + /** + * Makes sure that the behaviors declared in [[behaviors()]] are attached to this component. + */ + public function ensureBehaviors() + { + if ($this->_b === null) { + $this->_b = array(); + foreach ($this->behaviors() as $name => $behavior) { + $this->attachBehavior($name, $behavior); + } } } } diff --git a/framework/base/Model.php b/framework/base/Model.php index 3f04685..bceea5a 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -9,6 +9,8 @@ namespace yii\base; +use yii\util\Text; + /** * Model is the base class for data models. * @@ -22,7 +24,6 @@ namespace yii\base; * * Model also provides a set of events for further customization: * - * - [[onAfterInit]]: an event raised at the end of [[init()]] * - [[onBeforeValidate]]: an event raised at the beginning of [[validate()]] * - [[onAfterValidate]]: an event raised at the end of [[validate()]] * @@ -32,7 +33,7 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class Model extends Component implements Initable, \IteratorAggregate, \ArrayAccess +class Model extends Component implements \IteratorAggregate, \ArrayAccess { private static $_attributes = array(); // class name => array of attribute names private $_errors; // attribute name => array of errors @@ -49,50 +50,9 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc } /** - * Initializes this model. - * - * This method is required by the [[Initable]] interface. It is invoked by [[\Yii::createObject]] - * after it creates the new model instance and initializes the model properties. - * - * The default implementation calls [[behaviors]] and registers any available behaviors. - * You may override this method with additional initialization logic (e.g. establish DB connection). - * Make sure you call the parent implementation. - */ - public function init() - { - $this->attachBehaviors($this->behaviors()); - $this->afterInit(); - } - - /** - * Returns a list of behaviors that this model should behave as. - * The return value should be an array of behavior configurations indexed by - * behavior names. Each behavior configuration can be either a string specifying - * the behavior class or an array of the following structure: - * - * ~~~ - * 'behaviorName' => array( - * 'class' => 'BehaviorClass', - * 'property1' => 'value1', - * 'property2' => 'value2', - * ) - * ~~~ - * - * Note that a behavior class must extend from [[Behavior]]. Behaviors declared - * in this method will be attached to the model when [[init]] is invoked. - * - * @return array the behavior configurations. - * @see init - */ - public function behaviors() - { - return array(); - } - - /** * Returns the list of attribute names. * By default, this method returns all public non-static properties of the class. - * You may override this method to change the default. + * You may override this method to change the default behavior. * @return array list of attribute names. */ public function attributeNames() @@ -116,7 +76,7 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc /** * Returns the validation rules for attributes. * - * Validation rules are used by [[validate]] to check if attribute values are valid. + * Validation rules are used by [[validate()]] to check if attribute values are valid. * Child classes may override this method to declare different validation rules. * * Each rule is an array with the following structure: @@ -140,26 +100,31 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc * - additional name-value pairs can be specified to initialize the corresponding validator properties. * Please refer to individual validator class API for possible properties. * - * A validator can be either a model class method or an object. - * If the former, the method must have the following signature: + * A validator can be either an object of a class extending [[\yii\validators\Validator]], + * or a model class method (called *inline validator*) that has the following signature: * * ~~~ * // $params refers to validation parameters given in the rule * function validatorName($attribute, $params) * ~~~ * - * If the latter, the object must be extending from [[\yii\validators\Validator]]. - * Yii provides a set of [[\yii\validators\Validator::builtInValidators|built-in validators]]. - * They each have an alias name which can be used when specifying a validation rule. + * Yii also provides a set of [[\yii\validators\Validator::builtInValidators|built-in validators]]. + * They each has an alias name which can be used when specifying a validation rule. * - * The following are some examples: + * Below are some examples: * * ~~~ * array( + * // built-in "required" validator * array('username', 'required'), + * // built-in "length" validator customized with "min" and "max" properties * array('username', 'length', 'min'=>3, 'max'=>12), + * // built-in "compare" validator that is used in "register" scenario only * array('password', 'compare', 'compareAttribute'=>'password2', 'on'=>'register'), + * // an inline validator defined via the "authenticate()" method in the model class * array('password', 'authenticate', 'on'=>'login'), + * // a validator of class "CaptchaValidator" + * array('captcha', 'CaptchaValidator'), * ); * ~~~ * @@ -180,7 +145,7 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc * `firstName`, we can declare a label `First Name` which is more user-friendly and can * be displayed to end users. * - * By default an attribute label is generated using [[generateAttributeLabel]]. + * By default an attribute label is generated using [[generateAttributeLabel()]]. * This method allows you to explicitly specify attribute labels. * * Note, in order to inherit labels defined in the parent class, a child class needs to @@ -197,21 +162,21 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc /** * Performs the data validation. * - * This method executes the validation rules as declared in [[rules]]. + * This method executes the validation rules as declared in [[rules()]]. * Only the rules applicable to the current [[scenario]] will be executed. * A rule is considered applicable to a scenario if its `on` option is not set * or contains the scenario. * - * This method will call [[beforeValidate]] and [[afterValidate]] before and - * after actual validation, respectively. If [[beforeValidate]] returns false, - * the validation and [[afterValidate]] will be cancelled. + * This method will call [[beforeValidate()]] and [[afterValidate()]] before and + * after actual validation, respectively. If [[beforeValidate()]] returns false, + * the validation and [[afterValidate()]] will be cancelled. * - * Errors found during the validation can be retrieved via [[getErrors]]. + * Errors found during the validation can be retrieved via [[getErrors()]]. * * @param array $attributes list of attributes that should be validated. * If this parameter is empty, it means any attribute listed in the applicable * validation rules should be validated. - * @param boolean $clearErrors whether to call [[clearErrors]] before performing validation + * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation * @return boolean whether the validation is successful without any error. * @see beforeValidate * @see afterValidate @@ -232,19 +197,6 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc } /** - * This method is invoked at the end of [[init()]]. - * The default implementation raises the [[onAfterInit]] event. - * You may override this method to do postprocessing after model creation. - * Make sure you call the parent implementation so that the event is raised properly. - */ - public function afterInit() - { - if ($this->hasEventHandlers('onAfterInit')) { - $this->onAfterInit(new Event($this)); - } - } - - /** * This method is invoked before validation starts. * The default implementation raises the [[onBeforeValidate]] event. * You may override this method to do preliminary checks before validation. @@ -276,24 +228,6 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc } /** - * This event is raised by [[init]] when initializing the model. - * @param Event $event the event parameter - */ - public function onInit($event) - { - $this->raiseEvent(__FUNCTION__, $event); - } - - /** - * This event is raised at the end of [[init()]]. - * @param Event $event the event parameter - */ - public function onAfterInit($event) - { - $this->raiseEvent(__FUNCTION__, $event); - } - - /** * This event is raised before the validation is performed. * @param ValidationEvent $event the event parameter */ @@ -312,9 +246,9 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc } /** - * Returns all the validators declared in [[rules]]. + * Returns all the validators declared in [[rules()]]. * - * This method differs from [[getActiveValidators]] in that the latter + * This method differs from [[getActiveValidators()]] in that the latter * only returns the validators applicable to the current [[scenario]]. * * Because this method returns a [[Vector]] object, you may @@ -339,7 +273,7 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc * Returns the validators applicable to the current [[scenario]]. * @param string $attribute the name of the attribute whose applicable validators should be returned. * If this is null, the validators for ALL attributes in the model will be returned. - * @return array the validators applicable to the current [[scenario]]. + * @return \yii\validators\Validator[] the validators applicable to the current [[scenario]]. */ public function getActiveValidators($attribute = null) { @@ -356,8 +290,8 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc } /** - * Creates validator objects based on the validation rules specified in [[rules]]. - * Unlike [[getValidators]], calling this method each time, it will return a new list of validators. + * Creates validator objects based on the validation rules specified in [[rules()]]. + * Unlike [[getValidators()]], each time this method is called, a new list of validators will be returned. * @return Vector validators */ public function createValidators() @@ -399,7 +333,7 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc */ public function isAttributeSafe($attribute) { - $validators = $this->getActiveValidators(); + $validators = $this->getActiveValidators($attribute); foreach ($validators as $validator) { if (!$validator->safe) { return false; @@ -523,13 +457,13 @@ class Model extends Component implements Initable, \IteratorAggregate, \ArrayAcc */ public function generateAttributeLabel($name) { - return ucwords(trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(?value). */ diff --git a/framework/db/ar/ActiveMetaData.php b/framework/db/ar/ActiveMetaData.php new file mode 100644 index 0000000..edd2baa --- /dev/null +++ b/framework/db/ar/ActiveMetaData.php @@ -0,0 +1,76 @@ + + * @since 2.0 + */ +class ActiveMetaData +{ + /** + * @var TableSchema the table schema information + */ + public $table; + /** + * @var array list of relations + */ + public $relations = array(); + + /** + * Constructor. + * @param string $modelClass the model class name + */ + public function __construct($modelClass) + { + $tableName = $modelClass::tableName(); + $table = $modelClass::getDbConnection()->getDriver()->getTableSchema($tableName); + if ($table === null) { + throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'."); + } + if ($table->primaryKey === null) { + $primaryKey = $modelClass::primaryKey(); + if ($primaryKey !== null) { + $table->fixPrimaryKey($primaryKey); + } else { + throw new Exception("The table '$tableName' for ActiveRecord class '$modelClass' does not have a primary key."); + } + } + $this->table = $table; + + foreach ($modelClass::relations() as $name => $config) { + $this->addRelation($name, $config); + } + } + + /** + * Adds a relation. + * + * $config is an array with three elements: + * relation type, the related active record class and the foreign key. + * + * @throws Exception + * @param string $name $name Name of the relation. + * @param array $config $config Relation parameters. + * @return void + */ + public function addRelation($name, $config) + { + if (preg_match('/^(\w+)\s*:\s*\\\\?([\w\\\\]+)(\[\])?$/', $name, $matches)) { + if (is_string($config)) { + $config = array('on' => $config); + } + $relation = ActiveRelation::newInstance($config); + $relation->name = $matches[1]; + $relation->modelClass = '\\' . $matches[2]; + $relation->hasMany = isset($matches[3]); + $this->relations[$relation->name] = $relation; + } else { + throw new Exception("Relation name in bad format: $name"); + } + } +} \ No newline at end of file diff --git a/framework/db/ar/ActiveQuery.php b/framework/db/ar/ActiveQuery.php new file mode 100644 index 0000000..8d7caca --- /dev/null +++ b/framework/db/ar/ActiveQuery.php @@ -0,0 +1,233 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2012 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\ar; + +/** + * ActiveFinder.php is ... + * todo: add SQL monitor + * + * @author Qiang Xue + * @since 2.0 + */ +class ActiveQuery extends \yii\db\dao\BaseQuery implements \IteratorAggregate, \ArrayAccess, \Countable +{ + public $modelClass; + + public $with; + public $alias; + public $index; + + private $_count; + private $_sql; + private $_countSql; + private $_asArray; + private $_records; + + public function all() + { + return $this->performQuery(); + } + + public function one() + { + $this->limit = 1; + $records = $this->performQuery(); + if (isset($records[0])) { + $this->_count = 1; + return $records[0]; + } else { + $this->_count = 0; + return null; + } + } + + public function asArray($value = true) + { + $this->_asArray = $value; + } + + protected function performQuery() + { + $class = $this->modelClass; + $db = $class::getDbConnection(); + $this->_sql = $this->getSql($db); + $command = $db->createCommand($this->_sql); + $command->bindValues($this->params); + $rows = $command->queryAll(); + if ($this->_asArray) { + $records = $rows; + } else { + $records = array(); + foreach ($rows as $row) { + $records[] = $class::populateRecord($row); + } + } + $this->_count = count($records); + return $records; + } + + public function with() + { + + } +// +// public function getSql($connection = null) +// { +// +// } + + public function setSql($value) + { + $this->_sql = $value; + } + + public function getCountSql() + { + + } + + public function getOneSql() + { + + } + + /** + * Returns the number of items in the vector. + * @return integer the number of items in the vector + */ + public function getCount() + { + if ($this->_count !== null) { + return $this->_count; + } else { + return $this->_count = $this->performCountQuery(); + } + } + + protected function performCountQuery() + { + $select = $this->select; + $this->select = 'COUNT(*)'; + $class = $this->modelClass; + $command = $this->createCommand($class::getDbConnection()); + $this->_countSql = $command->getSql(); + $count = $command->queryScalar(); + $this->select = $select; + return $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->connection->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 Iterator an iterator for traversing the items in the vector. + */ + public function getIterator() + { + $records = $this->performQuery(); + return new \yii\base\VectorIterator($records); + } + + /** + * Returns the number of items in the vector. + * This method is required by the SPL `Countable` interface. + * It will be implicitly called when you use `count($vector)`. + * @return integer number of items in the vector. + */ + public function count() + { + return $this->getCount(); + } + + /** + * 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->performQuery(); + } + 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 mixed 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->performQuery(); + } + 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 mixed $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->performQuery(); + } + $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->performQuery(); + } + unset($this->_records[$offset]); + } +} diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 331dd09..29ca6a7 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -51,26 +51,40 @@ abstract class ActiveRecord extends \yii\base\Model } } - public static function find($query = null) - { - $finder = static::createFinder(); - if ($query instanceof Query) { - $finder->query = $query; - } elseif ($query !== null) { - // todo: findByPk + /** + * @static + * @param string|array|ActiveQuery $q + * @return ActiveQuery + * @throws \yii\db\Exception + */ + public static function find($q = null) + { + $query = $q instanceof ActiveQuery? $q : static::createQuery(); + $query->modelClass = '\\' . get_called_class(); + $query->from = static::tableName(); + if (is_array($q)) { + $query->where($q); + } elseif ($q !== null && $query !== $q) { + $primaryKey = static::getMetaData()->table->primaryKey; + if (is_string($primaryKey)) { + $query->where(array($primaryKey => $q)); + } else { + throw new Exception("Multiple column values are required to find by composite primary keys."); + } } - return $finder; + return $query; } public static function findBySql($sql, $params = array()) { - $finder = static::createFinder(); + $query = static::createQuery(); if (!is_array($params)) { $params = func_get_args(); array_shift($params); } - $finder->setSql($sql); - return $finder->params($params); + $query->setSql($sql); + $query->modelClass = '\\' . get_called_class(); + return $query->params($params); } public static function exists($condition, $params) @@ -93,9 +107,9 @@ abstract class ActiveRecord extends \yii\base\Model } - public static function createFinder() + public static function createQuery() { - return new ActiveFinder('\\' . get_called_class()); + return new ActiveQuery('\\' . get_called_class()); } /** @@ -149,6 +163,26 @@ abstract class ActiveRecord extends \yii\base\Model } /** + * Declares the relations for this ActiveRecord class. + * + * Child classes may want to override this method to specify their relations. + * + * The following shows how to declare relations for a Programmer AR class: + * + * ~~~ + * return array( + * 'manager:Manager' => '?.manager_id = manager.id', + * 'assignments:Assignment[]' => array( + * 'on' => '?.id = assignments.owner_id AND assignments.status=1', + * 'orderBy' => 'assignments.create_time DESC', + * ), + * 'projects:Project[]' => array( + * 'via' => 'assignments', + * 'on' => 'projects.id = assignments.project_id', + * ), + * ); + * ~~~ + * * This method should be overridden to declare related objects. * * There are four types of relations that may exist between two active record objects: @@ -1194,443 +1228,3 @@ abstract class ActiveRecord extends \yii\base\Model return $this->__isset($offset); } } - - -/** - * CBaseActiveRelation is the base class for all active relations. - * @author Qiang Xue - */ -class CBaseActiveRelation extends \yii\base\Component -{ - /** - * @var string name of the related object - */ - public $name; - /** - * @var string name of the related active record class - */ - public $className; - /** - * @var string the foreign key in this relation - */ - public $foreignKey; - /** - * @var mixed list of column names (an array, or a string of names separated by commas) to be selected. - * Do not quote or prefix the column names unless they are used in an expression. - * In that case, you should prefix the column names with 'relationName.'. - */ - public $select = '*'; - /** - * @var string WHERE clause. For {@link CActiveRelation} descendant classes, column names - * referenced in the condition should be disambiguated with prefix 'relationName.'. - */ - public $condition = ''; - /** - * @var array the parameters that are to be bound to the condition. - * The keys are parameter placeholder names, and the values are parameter values. - */ - public $params = array(); - /** - * @var string GROUP BY clause. For {@link CActiveRelation} descendant classes, column names - * referenced in this property should be disambiguated with prefix 'relationName.'. - */ - public $group = ''; - /** - * @var string how to join with other tables. This refers to the JOIN clause in an SQL statement. - * For example, 'LEFT JOIN users ON users.id=authorID'. - */ - public $join = ''; - /** - * @var string HAVING clause. For {@link CActiveRelation} descendant classes, column names - * referenced in this property should be disambiguated with prefix 'relationName.'. - */ - public $having = ''; - /** - * @var string ORDER BY clause. For {@link CActiveRelation} descendant classes, column names - * referenced in this property should be disambiguated with prefix 'relationName.'. - */ - public $order = ''; - - /** - * Constructor. - * @param string $name name of the relation - * @param string $className name of the related active record class - * @param string $foreignKey foreign key for this relation - * @param array $options additional options (name=>value). The keys must be the property names of this class. - */ - public function __construct($name, $className, $foreignKey, $options = array()) - { - $this->name = $name; - $this->className = $className; - $this->foreignKey = $foreignKey; - foreach ($options as $name => $value) - { - $this->$name = $value; - } - } - - /** - * Merges this relation with a criteria specified dynamically. - * @param array $criteria the dynamically specified criteria - * @param boolean $fromScope whether the criteria to be merged is from scopes - */ - public function mergeWith($criteria, $fromScope = false) - { - if ($criteria instanceof CDbCriteria) { - $criteria = $criteria->toArray(); - } - if (isset($criteria['select']) && $this->select !== $criteria['select']) { - if ($this->select === '*') { - $this->select = $criteria['select']; - } - elseif ($criteria['select'] !== '*') - { - $select1 = is_string($this->select) ? preg_split('/\s*,\s*/', trim($this->select), -1, PREG_SPLIT_NO_EMPTY) : $this->select; - $select2 = is_string($criteria['select']) ? preg_split('/\s*,\s*/', trim($criteria['select']), -1, PREG_SPLIT_NO_EMPTY) : $criteria['select']; - $this->select = array_merge($select1, array_diff($select2, $select1)); - } - } - - if (isset($criteria['condition']) && $this->condition !== $criteria['condition']) { - if ($this->condition === '') { - $this->condition = $criteria['condition']; - } - elseif ($criteria['condition'] !== '') - { - $this->condition = "( {$this->condition}) AND ( {$criteria['condition']})"; - } - } - - if (isset($criteria['params']) && $this->params !== $criteria['params']) { - $this->params = array_merge($this->params, $criteria['params']); - } - - if (isset($criteria['order']) && $this->order !== $criteria['order']) { - if ($this->order === '') { - $this->order = $criteria['order']; - } - elseif ($criteria['order'] !== '') - { - $this->order = $criteria['order'] . ', ' . $this->order; - } - } - - if (isset($criteria['group']) && $this->group !== $criteria['group']) { - if ($this->group === '') { - $this->group = $criteria['group']; - } - elseif ($criteria['group'] !== '') - { - $this->group .= ', ' . $criteria['group']; - } - } - - if (isset($criteria['join']) && $this->join !== $criteria['join']) { - if ($this->join === '') { - $this->join = $criteria['join']; - } - elseif ($criteria['join'] !== '') - { - $this->join .= ' ' . $criteria['join']; - } - } - - if (isset($criteria['having']) && $this->having !== $criteria['having']) { - if ($this->having === '') { - $this->having = $criteria['having']; - } - elseif ($criteria['having'] !== '') - { - $this->having = "( {$this->having}) AND ( {$criteria['having']})"; - } - } - } -} - - -/** - * CStatRelation represents a statistical relational query. - * @author Qiang Xue - */ -class CStatRelation extends CBaseActiveRelation -{ - /** - * @var string the statistical expression. Defaults to 'COUNT(*)', meaning - * the count of child objects. - */ - public $select = 'COUNT(*)'; - /** - * @var mixed the default value to be assigned to those records that do not - * receive a statistical query result. Defaults to 0. - */ - public $defaultValue = 0; - - /** - * Merges this relation with a criteria specified dynamically. - * @param array $criteria the dynamically specified criteria - * @param boolean $fromScope whether the criteria to be merged is from scopes - */ - public function mergeWith($criteria, $fromScope = false) - { - if ($criteria instanceof CDbCriteria) { - $criteria = $criteria->toArray(); - } - parent::mergeWith($criteria, $fromScope); - - if (isset($criteria['defaultValue'])) { - $this->defaultValue = $criteria['defaultValue']; - } - } -} - - -/** - * CActiveRelation is the base class for representing active relations that bring back related objects. - * @author Qiang Xue - * @since 2.0 - */ -class CActiveRelation extends CBaseActiveRelation -{ - /** - * @var string join type. Defaults to 'LEFT OUTER JOIN'. - */ - public $joinType = 'LEFT OUTER JOIN'; - /** - * @var string ON clause. The condition specified here will be appended to the joining condition using AND operator. - */ - public $on = ''; - /** - * @var string the alias for the table that this relation refers to. Defaults to null, meaning - * the alias will be the same as the relation name. - */ - public $alias; - /** - * @var string|array specifies which related objects should be eagerly loaded when this related object is lazily loaded. - * For more details about this property, see {@link ActiveRecord::with()}. - */ - public $with = array(); - /** - * @var boolean whether this table should be joined with the primary table. - * When setting this property to be false, the table associated with this relation will - * appear in a separate JOIN statement. - * If this property is set true, then the corresponding table will ALWAYS be joined together - * with the primary table, no matter the primary table is limited or not. - * If this property is not set, the corresponding table will be joined with the primary table - * only when the primary table is not limited. - */ - public $together; - /** - * @var mixed scopes to apply - * Can be set to the one of the following: - *
    - *
  • Single scope: 'scopes'=>'scopeName'.
  • - *
  • Multiple scopes: 'scopes'=>array('scopeName1','scopeName2').
  • - *
- */ - public $scopes; - - /** - * Merges this relation with a criteria specified dynamically. - * @param array $criteria the dynamically specified criteria - * @param boolean $fromScope whether the criteria to be merged is from scopes - */ - public function mergeWith($criteria, $fromScope = false) - { - if ($criteria instanceof CDbCriteria) { - $criteria = $criteria->toArray(); - } - if ($fromScope) { - if (isset($criteria['condition']) && $this->on !== $criteria['condition']) { - if ($this->on === '') { - $this->on = $criteria['condition']; - } - elseif ($criteria['condition'] !== '') - { - $this->on = "( {$this->on}) AND ( {$criteria['condition']})"; - } - } - unset($criteria['condition']); - } - - parent::mergeWith($criteria); - - if (isset($criteria['joinType'])) { - $this->joinType = $criteria['joinType']; - } - - if (isset($criteria['on']) && $this->on !== $criteria['on']) { - if ($this->on === '') { - $this->on = $criteria['on']; - } - elseif ($criteria['on'] !== '') - { - $this->on = "( {$this->on}) AND ( {$criteria['on']})"; - } - } - - if (isset($criteria['with'])) { - $this->with = $criteria['with']; - } - - if (isset($criteria['alias'])) { - $this->alias = $criteria['alias']; - } - - if (isset($criteria['together'])) { - $this->together = $criteria['together']; - } - } -} - - -/** - * CBelongsToRelation represents the parameters specifying a BELONGS_TO relation. - * @author Qiang Xue - * @since 2.0 - */ -class CBelongsToRelation extends CActiveRelation -{ -} - - -/** - * CHasOneRelation represents the parameters specifying a HAS_ONE relation. - * @author Qiang Xue - * @since 2.0 - */ -class CHasOneRelation extends CActiveRelation -{ - /** - * @var string the name of the relation that should be used as the bridge to this relation. - * Defaults to null, meaning don't use any bridge. - */ - public $through; -} - - -/** - * CHasManyRelation represents the parameters specifying a HAS_MANY relation. - * @author Qiang Xue - * @since 2.0 - */ -class CHasManyRelation extends CActiveRelation -{ - /** - * @var integer limit of the rows to be selected. It is effective only for lazy loading this related object. Defaults to -1, meaning no limit. - */ - public $limit = -1; - /** - * @var integer offset of the rows to be selected. It is effective only for lazy loading this related object. Defaults to -1, meaning no offset. - */ - public $offset = -1; - /** - * @var string the name of the column that should be used as the key for storing related objects. - * Defaults to null, meaning using zero-based integer IDs. - */ - public $index; - /** - * @var string the name of the relation that should be used as the bridge to this relation. - * Defaults to null, meaning don't use any bridge. - */ - public $through; - - /** - * Merges this relation with a criteria specified dynamically. - * @param array $criteria the dynamically specified criteria - * @param boolean $fromScope whether the criteria to be merged is from scopes - */ - public function mergeWith($criteria, $fromScope = false) - { - if ($criteria instanceof CDbCriteria) { - $criteria = $criteria->toArray(); - } - parent::mergeWith($criteria, $fromScope); - if (isset($criteria['limit']) && $criteria['limit'] > 0) { - $this->limit = $criteria['limit']; - } - - if (isset($criteria['offset']) && $criteria['offset'] >= 0) { - $this->offset = $criteria['offset']; - } - - if (isset($criteria['index'])) { - $this->index = $criteria['index']; - } - } -} - - -/** - * CManyManyRelation represents the parameters specifying a MANY_MANY relation. - * @author Qiang Xue - * @since 2.0 - */ -class CManyManyRelation extends CHasManyRelation -{ -} - - -/** - * ActiveMetaData represents the meta-data for an Active Record class. - * - * @author Qiang Xue - * @since 2.0 - */ -class ActiveMetaData -{ - /** - * @var TableSchema the table schema information - */ - public $table; - /** - * @var array list of relations - */ - public $relations = array(); - - /** - * Constructor. - * @param string $modelClass the model class name - */ - public function __construct($modelClass) - { - $tableName = $modelClass::tableName(); - $table = $modelClass::getDbConnection()->getDriver()->getTableSchema($tableName); - if ($table === null) { - throw new Exception("Unable to find table '$tableName' for ActiveRecord class '$modelClass'."); - } - if ($table->primaryKey === null) { - $primaryKey = $modelClass::primaryKey(); - if ($primaryKey !== null) { - $table->fixPrimaryKey($primaryKey); - } else { - throw new Exception("The table '$tableName' for ActiveRecord class '$modelClass' does not have a primary key."); - } - } - $this->table = $table; - - foreach ($modelClass::relations() as $name => $config) { - $this->addRelation($name, $config); - } - } - - /** - * Adds a relation. - * - * $config is an array with three elements: - * relation type, the related active record class and the foreign key. - * - * @throws Exception - * @param string $name $name Name of the relation. - * @param array $config $config Relation parameters. - * @return void - */ - public function addRelation($name, $config) - { - // relation class, AR class, FK - if (isset($config[0], $config[1], $config[2])) { - $this->relations[$name] = new $config[0]($name, $config[1], $config[2], array_slice($config, 3)); - } else { - throw new Exception(Yii::t('yii', 'Active record "{class}" has an invalid configuration for relation "{relation}". It must specify the relation type, the related active record class and the foreign key.', array('{class}' => get_class($this->_model), '{relation}' => $name))); - } - } -} diff --git a/framework/db/ar/ActiveRelation.php b/framework/db/ar/ActiveRelation.php new file mode 100644 index 0000000..0de0e84 --- /dev/null +++ b/framework/db/ar/ActiveRelation.php @@ -0,0 +1,43 @@ +joinType !== null) { + $this->joinType = $relation->joinType; + } + if ($relation->alias !== null) { + $this->alias = $relation->alias; + } + if ($relation->on !== null) { + if (!empty($this->on)) { + $this->on = "({$this->on}) AND ({$relation->on})"; + } else { + $this->on = $relation->on; + } + } + if ($relation->via !== null) { + $this->via = $relation->via; + } + if ($relation->index !== null) { + $this->index = $relation->index; + } + // todo: with, scopes + } +} diff --git a/framework/db/dao/BaseQuery.php b/framework/db/dao/BaseQuery.php new file mode 100644 index 0000000..82a2431 --- /dev/null +++ b/framework/db/dao/BaseQuery.php @@ -0,0 +1,715 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2012 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db\dao; + +/** + * Query represents a SQL statement in a way that is independent of DBMS. + * + * Query not only can represent a SELECT statement, it can also represent INSERT, UPDATE, DELETE, + * and other commonly used DDL statements, such as CREATE TABLE, CREATE INDEX, etc. + * + * Query provides a set of methods to facilitate the specification of different clauses. + * These methods can be chained together. For example, + * + * ~~~ + * $query = new Query; + * $query->select('id, name') + * ->from('tbl_user') + * ->limit(10); + * // get the actual SQL statement + * echo $query->getSql(); + * // or execute the query + * $users = $query->createCommand()->queryAll(); + * ~~~ + * + * By calling [[getSql()]], we can obtain the actual SQL statement from a Query object. + * And by calling [[createCommand()]], we can get a [[Command]] instance which can be further + * used to perform/execute the DB query against a database. + * + * @property string $sql the SQL statement represented by this query object. + * + * @author Qiang Xue + * @since 2.0 + */ +class BaseQuery extends \yii\base\Object +{ + /** + * @var string|array the columns being selected. This refers to the SELECT clause in a SQL + * statement. It can be either a string (e.g. `'id, name'`) or an array (e.g. `array('id', 'name')`). + * If not set, if means all columns. + * @see select() + */ + public $select; + /** + * @var string additional option that should be appended to the 'SELECT' keyword. For example, + * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. + */ + public $selectOption; + /** + * @var boolean whether to select distinct rows of data only. If this is set true, + * the SELECT clause would be changed to SELECT DISTINCT. + */ + public $distinct; + /** + * @var string|array the table(s) to be selected from. This refers to the FROM clause in a SQL statement. + * It can be either a string (e.g. `'tbl_user, tbl_post'`) or an array (e.g. `array('tbl_user', 'tbl_post')`). + * @see from() + */ + public $from; + /** + * @var string|array query condition. This refers to the WHERE clause in a SQL statement. + * For example, `age > 31 AND team = 1`. + * @see where() + */ + public $where; + /** + * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. + */ + public $limit; + /** + * @var integer zero-based offset from where the records are to be returned. If not set or + * less than 0, it means starting from the beginning. + */ + public $offset; + /** + * @var string|array how to sort the query results. This refers to the ORDER BY clause in a SQL statement. + * It can be either a string (e.g. `'id ASC, name DESC'`) or an array (e.g. `array('id ASC', 'name DESC')`). + */ + public $orderBy; + /** + * @var string|array how to group the query results. This refers to the GROUP BY clause in a SQL statement. + * It can be either a string (e.g. `'company, department'`) or an array (e.g. `array('company', 'department')`). + */ + public $groupBy; + /** + * @var string|array how to join with other tables. This refers to the JOIN clause in a SQL statement. + * It can either a string (e.g. `'LEFT JOIN tbl_user ON tbl_user.id=author_id'`) or an array (e.g. + * `array('LEFT JOIN tbl_user ON tbl_user.id=author_id', 'LEFT JOIN tbl_team ON tbl_team.id=team_id')`). + * @see join() + */ + public $join; + /** + * @var string|array the condition to be applied in the GROUP BY clause. + * It can be either a string or an array. Please refer to [[where()]] on how to specify the condition. + */ + public $having; + /** + * @var array list of query parameter values indexed by parameter placeholders. + * For example, `array(':name'=>'Dan', ':age'=>31)`. + */ + public $params; + /** + * @var string|array the UNION clause(s) in a SQL statement. This can be either a string + * representing a single UNION clause or an array representing multiple UNION clauses. + * Each union clause can be a string or a `Query` object which refers to the SQL statement. + */ + public $union; + + /** + * Sets the SELECT part of the query. + * @param mixed $columns the columns to be selected. Defaults to '*', meaning all columns. + * 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 boolean $distinct whether to use 'SELECT DISTINCT'. + * @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 Query the query object itself + */ + public function select($columns = '*', $distinct = false, $option = '') + { + $this->select = $columns; + $this->distinct = $distinct; + $this->selectOption = $option; + return $this; + } + + /** + * Sets the FROM part of the query. + * @param mixed $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 Query the query object itself + */ + public function from($tables) + { + $this->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 Query the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition, $params = array()) + { + $this->where = $condition; + if (!is_array($params)) { + $params = func_get_args(); + unset($params[0]); + } + $this->addParams($params); + 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 Query the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition, $params = array()) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = array('and', $this->where, $condition); + } + if (!is_array($params)) { + $params = func_get_args(); + unset($params[0]); + } + $this->addParams($params); + 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 Query the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition, $params = array()) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = array('or', $this->where, $condition); + } + if (!is_array($params)) { + $params = func_get_args(); + unset($params[0]); + } + $this->addParams($params); + 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 Query the query object itself + */ + public function join($table, $condition, $params = array()) + { + $this->join[] = array('JOIN', $table, $condition); + if (!is_array($params)) { + $params = func_get_args(); + array_shift($params); + unset($params[0]); + } + return $this->addParams($params); + } + + /** + * 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 Query the query object itself + */ + public function leftJoin($table, $condition, $params = array()) + { + $this->join[] = array('LEFT JOIN', $table, $condition); + if (!is_array($params)) { + $params = func_get_args(); + array_shift($params); + unset($params[0]); + } + return $this->addParams($params); + } + + /** + * 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 Query the query object itself + */ + public function rightJoin($table, $condition, $params = array()) + { + $this->join[] = array('RIGHT JOIN', $table, $condition); + if (!is_array($params)) { + $params = func_get_args(); + array_shift($params); + unset($params[0]); + } + return $this->addParams($params); + } + + /** + * 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 Query the query object itself + */ + public function crossJoin($table) + { + $this->join[] = array('CROSS JOIN', $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 Query the query object itself + */ + public function naturalJoin($table) + { + $this->join[] = array('NATURAL JOIN', $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 Query the query object itself + * @see addGroupBy() + */ + public function groupBy($columns) + { + $this->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 Query the query object itself + * @see groupBy() + */ + public function addGroupBy($columns) + { + if (empty($this->groupBy)) { + $this->groupBy = $columns; + } else { + if (!is_array($this->groupBy)) { + $this->groupBy = preg_split('/\s*,\s*/', trim($this->groupBy), -1, PREG_SPLIT_NO_EMPTY); + } + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + $this->groupBy = array_merge($this->groupBy, $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 Query the query object itself + * @see andHaving() + * @see orHaving() + */ + public function having($condition, $params = array()) + { + $this->having = $condition; + if (!is_array($params)) { + $params = func_get_args(); + unset($params[0]); + } + $this->addParams($params); + 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 Query the query object itself + * @see having() + * @see orHaving() + */ + public function andHaving($condition, $params = array()) + { + if ($this->having === null) { + $this->having = $condition; + } else { + $this->having = array('and', $this->having, $condition); + } + if (!is_array($params)) { + $params = func_get_args(); + unset($params[0]); + } + $this->addParams($params); + 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 Query the query object itself + * @see having() + * @see andHaving() + */ + public function orHaving($condition, $params = array()) + { + if ($this->having === null) { + $this->having = $condition; + } else { + $this->having = array('or', $this->having, $condition); + } + if (!is_array($params)) { + $params = func_get_args(); + unset($params[0]); + } + $this->addParams($params); + 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 Query the query object itself + * @see addOrderBy() + */ + public function orderBy($columns) + { + $this->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 Query the query object itself + * @see orderBy() + */ + public function addOrderBy($columns) + { + if (empty($this->orderBy)) { + $this->orderBy = $columns; + } else { + if (!is_array($this->orderBy)) { + $this->orderBy = preg_split('/\s*,\s*/', trim($this->orderBy), -1, PREG_SPLIT_NO_EMPTY); + } + if (!is_array($columns)) { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + } + $this->orderBy = array_merge($this->orderBy, $columns); + } + return $this; + } + + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit + * @return Query the query object itself + */ + public function limit($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset + * @return Query the query object itself + */ + public function offset($offset) + { + $this->offset = $offset; + return $this; + } + + /** + * Appends a SQL statement using UNION operator. + * @param string $sql the SQL statement to be appended using UNION + * @return Query the query object itself + */ + public function union($sql) + { + $this->union[] = $sql; + return $this; + } + + /** + * 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 Query the query object itself + * @see addParams() + */ + public function params($params) + { + $this->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 Query the query object itself + * @see params() + */ + public function addParams($params) + { + if ($this->params === null) { + $this->params = $params; + } else { + foreach ($params as $name => $value) { + if (is_integer($name)) { + $this->params[] = $value; + } else { + $this->params[$name] = $value; + } + } + } + return $this; + } + + /** + * Merges this query with another one. + * + * The merging is done according to the following rules: + * + * - [[select]]: the union of both queries' [[select]] property values. + * - [[selectOption]], [[distinct]], [[limit]], [[offset]]: the new query + * takes precedence over this query. + * - [[where]], [[having]]: the new query's corresponding property value + * will be 'AND' together with the existing one. + * - [[params]], [[orderBy]], [[groupBy]], [[join]], [[union]]: the new query's + * corresponding property value will be appended to the existing one. + * + * In general, the merging makes the resulting query more restrictive and specific. + * @param BaseQuery $query the new query to be merged with this query. + * @return BaseQuery the query object itself + */ + public function mergeWith($query) + { + if ($this->select !== $query->select) { + if (empty($this->select)) { + $this->select = $query->select; + } elseif (!empty($query->select)) { + $select1 = is_string($this->select) ? preg_split('/\s*,\s*/', trim($this->select), -1, PREG_SPLIT_NO_EMPTY) : $this->select; + $select2 = is_string($query->select) ? preg_split('/\s*,\s*/', trim($query->select), -1, PREG_SPLIT_NO_EMPTY) : $query->select; + $this->select = array_merge($select1, array_diff($select2, $select1)); + } + } + + if ($query->selectOption !== null) { + $this->selectOption = $query->selectOption; + } + + if ($query->distinct !== null) { + $this->distinct = $query->distinct; + } + + if ($query->limit !== null) { + $this->limit = $query->limit; + } + + if ($query->offset !== null) { + $this->offset = $query->offset; + } + + if ($query->where !== null) { + $this->andWhere($query->where); + } + + if ($query->having !== null) { + $this->andHaving($query->having); + } + + if ($query->params !== null) { + $this->addParams($query->params); + } + + if ($query->orderBy !== null) { + $this->addOrderBy($query->orderBy); + } + + if ($query->groupBy !== null) { + $this->addGroupBy($query->groupBy); + } + + if ($query->join !== null) { + if (empty($this->join)) { + $this->join = $query->join; + } else { + if (!is_array($this->join)) { + $this->join = array($this->join); + } + if (is_array($query->join)) { + $this->join = array_merge($this->join, $query->join); + } else { + $this->join[] = $query->join; + } + } + } + + if ($query->union !== null) { + if (empty($this->union)) { + $this->union = $query->union; + } else { + if (!is_array($this->union)) { + $this->union = array($this->union); + } + if (is_array($query->union)) { + $this->union = array_merge($this->union, $query->union); + } else { + $this->union[] = $query->union; + } + } + } + + return $this; + } + + /** + * Resets the query object to its original state. + * @return Query the query object itself + */ + public function reset() + { + foreach (get_object_vars($this) as $name => $value) { + $this->$name = null; + } + return $this; + } +} diff --git a/framework/db/dao/Query.php b/framework/db/dao/Query.php index 24858bb..d48744e 100644 --- a/framework/db/dao/Query.php +++ b/framework/db/dao/Query.php @@ -39,80 +39,9 @@ namespace yii\db\dao; * @author Qiang Xue * @since 2.0 */ -class Query extends \yii\base\Object +class Query extends BaseQuery { /** - * @var string|array the columns being selected. This refers to the SELECT clause in a SQL - * statement. It can be either a string (e.g. `'id, name'`) or an array (e.g. `array('id', 'name')`). - * If not set, if means all columns. - * @see select() - */ - public $select; - /** - * @var string additional option that should be appended to the 'SELECT' keyword. For example, - * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. - */ - public $selectOption; - /** - * @var string|array the table(s) to be selected from. This refers to the FROM clause in a SQL statement. - * It can be either a string (e.g. `'tbl_user, tbl_post'`) or an array (e.g. `array('tbl_user', 'tbl_post')`). - * @see from() - */ - public $from; - /** - * @var boolean whether to select distinct rows of data only. If this is set true, - * the SELECT clause would be changed to SELECT DISTINCT. - */ - public $distinct; - /** - * @var string|array query condition. This refers to the WHERE clause in a SQL statement. - * For example, `age > 31 AND team = 1`. - * @see where() - */ - public $where; - /** - * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. - */ - public $limit; - /** - * @var integer zero-based offset from where the records are to be returned. If not set or - * less than 0, it means starting from the beginning. - */ - public $offset; - /** - * @var string|array how to sort the query results. This refers to the ORDER BY clause in a SQL statement. - * It can be either a string (e.g. `'id ASC, name DESC'`) or an array (e.g. `array('id ASC', 'name DESC')`). - */ - public $orderBy; - /** - * @var string|array how to group the query results. This refers to the GROUP BY clause in a SQL statement. - * It can be either a string (e.g. `'company, department'`) or an array (e.g. `array('company', 'department')`). - */ - public $groupBy; - /** - * @var string|array how to join with other tables. This refers to the JOIN clause in a SQL statement. - * It can either a string (e.g. `'LEFT JOIN tbl_user ON tbl_user.id=author_id'`) or an array (e.g. - * `array('LEFT JOIN tbl_user ON tbl_user.id=author_id', 'LEFT JOIN tbl_team ON tbl_team.id=team_id')`). - * @see join() - */ - public $join; - /** - * @var string|array the condition to be applied in the GROUP BY clause. - * It can be either a string or an array. Please refer to [[where()]] on how to specify the condition. - */ - public $having; - /** - * @var array list of query parameter values indexed by parameter placeholders. - * For example, `array(':name'=>'Dan', ':age'=>31)`. - */ - public $params; - /** - * @var string|array the UNION clause(s) in a SQL statement. This can be either a string - * representing a single UNION clause or an array representing multiple UNION clauses. - * Each union clause can be a string or a `Query` object which refers to the SQL statement. - */ - public $union; - /** * @var array the operation that this query represents. This refers to the method call as well as * the corresponding parameters for constructing a non-query SQL statement (e.g. INSERT, CREATE TABLE). * This property is mainly maintained by methods such as [[insert()]], [[update()]], [[createTable()]]. @@ -120,497 +49,33 @@ class Query extends \yii\base\Object */ public $operation; - /** - /** - * Sets the SELECT part of the query. - * @param mixed $columns the columns to be selected. Defaults to '*', meaning all columns. - * 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 boolean $distinct whether to use 'SELECT DISTINCT'. - * @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 Query the query object itself - */ - public function select($columns = '*', $distinct = false, $option = '') - { - $this->select = $columns; - $this->distinct = $distinct; - $this->selectOption = $option; - return $this; - } - - /** - * Sets the FROM part of the query. - * @param mixed $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 Query the query object itself - */ - public function from($tables) - { - $this->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 Query the query object itself - * @see andWhere() - * @see orWhere() - */ - public function where($condition, $params = array()) - { - $this->where = $condition; - if (!is_array($params)) { - $params = func_get_args(); - array_shift($params); - } - $this->addParams($params); - 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 Query the query object itself - * @see where() - * @see orWhere() - */ - public function andWhere($condition, $params = array()) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = array('and', $this->where, $condition); - } - if (!is_array($params)) { - $params = func_get_args(); - array_shift($params); - } - $this->addParams($params); - 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 Query the query object itself - * @see where() - * @see andWhere() - */ - public function orWhere($condition, $params = array()) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = array('or', $this->where, $condition); - } - if (!is_array($params)) { - $params = func_get_args(); - array_shift($params); - } - $this->addParams($params); - 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 Query the query object itself - */ - public function join($table, $condition, $params = array()) - { - $this->join[] = array('JOIN', $table, $condition); - if (!is_array($params)) { - $params = func_get_args(); - array_shift($params); - array_shift($params); - } - return $this->addParams($params); - } - - /** - * 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 Query the query object itself - */ - public function leftJoin($table, $condition, $params = array()) - { - $this->join[] = array('LEFT JOIN', $table, $condition); - if (!is_array($params)) { - $params = func_get_args(); - array_shift($params); - array_shift($params); - } - return $this->addParams($params); - } - - /** - * 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 Query the query object itself - */ - public function rightJoin($table, $condition, $params = array()) - { - $this->join[] = array('RIGHT JOIN', $table, $condition); - if (!is_array($params)) { - $params = func_get_args(); - array_shift($params); - array_shift($params); - } - return $this->addParams($params); - } - - /** - * 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 Query the query object itself - */ - public function crossJoin($table) - { - $this->join[] = array('CROSS JOIN', $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 Query the query object itself - */ - public function naturalJoin($table) - { - $this->join[] = array('NATURAL JOIN', $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 Query the query object itself - * @see addGroupBy() - */ - public function groupBy($columns) - { - $this->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 Query the query object itself - * @see groupBy() - */ - public function addGroupBy($columns) - { - if (empty($this->groupBy)) { - $this->groupBy = $columns; - } else { - if (!is_array($this->groupBy)) { - $this->groupBy = preg_split('/\s*,\s*/', trim($this->groupBy), -1, PREG_SPLIT_NO_EMPTY); - } - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - $this->groupBy = array_merge($this->groupBy, $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 Query the query object itself - * @see andHaving() - * @see orHaving() - */ - public function having($condition, $params = array()) - { - $this->having = $condition; - if (!is_array($params)) { - $params = func_get_args(); - array_shift($params); - } - $this->addParams($params); - 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 Query the query object itself - * @see having() - * @see orHaving() - */ - public function andHaving($condition, $params = array()) - { - if ($this->having === null) { - $this->having = $condition; - } else { - $this->having = array('and', $this->having, $condition); - } - if (!is_array($params)) { - $params = func_get_args(); - array_shift($params); - } - $this->addParams($params); - 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 Query the query object itself - * @see having() - * @see andHaving() - */ - public function orHaving($condition, $params = array()) - { - if ($this->having === null) { - $this->having = $condition; - } else { - $this->having = array('or', $this->having, $condition); - } - if (!is_array($params)) { - $params = func_get_args(); - array_shift($params); - } - $this->addParams($params); - 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 Query the query object itself - * @see addOrderBy() - */ - public function orderBy($columns) - { - $this->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 Query the query object itself - * @see orderBy() - */ - public function addOrderBy($columns) - { - if (empty($this->orderBy)) { - $this->orderBy = $columns; - } else { - if (!is_array($this->orderBy)) { - $this->orderBy = preg_split('/\s*,\s*/', trim($this->orderBy), -1, PREG_SPLIT_NO_EMPTY); - } - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } - $this->orderBy = array_merge($this->orderBy, $columns); - } - return $this; - } - - /** - * Sets the LIMIT part of the query. - * @param integer $limit the limit - * @return Query the query object itself - */ - public function limit($limit) - { - $this->limit = $limit; - return $this; - } - - /** - * Sets the OFFSET part of the query. - * @param integer $offset the offset - * @return Query the query object itself - */ - public function offset($offset) - { - $this->offset = $offset; - return $this; - } - - /** - * Appends a SQL statement using UNION operator. - * @param string $sql the SQL statement to be appended using UNION - * @return Query the query object itself - */ - public function union($sql) - { - $this->union[] = $sql; - return $this; - } - - /** - * 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 Query the query object itself - * @see addParams() + * Generates and returns the SQL statement according to this query. + * @param Connection $connection the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return string the generated SQL statement */ - public function params($params) + public function getSql($connection = null) { - if (!is_array($params)) { - $params = func_get_args(); + if ($connection === null) { + $connection = \Yii::$application->db; } - $this->params = $params; - return $this; + return $connection->getQueryBuilder()->build($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 Query the query object itself - * @see params() + * Creates a DB command that can be used to execute this query. + * @param Connection $connection the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return Command the created DB command instance. */ - public function addParams($params) + public function createCommand($connection = null) { - if (!is_array($params)) { - $params = func_get_args(); - } - foreach ($params as $name => $value) { - if (is_integer($name)) { - $this->params[] = $value; - } else { - $this->params[$name] = $value; - } + if ($connection === null) { + $connection = \Yii::$application->db; } - return $this; + return $connection->createCommand($this); } /** @@ -643,7 +108,7 @@ class Query extends \yii\base\Object $params = func_get_args(); array_shift($params); array_shift($params); - array_shift($params); + unset($params[0]); } $this->addParams($params); $this->operation = array(__FUNCTION__, $table, $columns, $condition, array()); @@ -664,7 +129,7 @@ class Query extends \yii\base\Object if (!is_array($params)) { $params = func_get_args(); array_shift($params); - array_shift($params); + unset($params[0]); } $this->operation = array(__FUNCTION__, $table, $condition); return $this->addParams($params); @@ -839,147 +304,4 @@ class Query extends \yii\base\Object $this->operation = array(__FUNCTION__, $name, $table); return $this; } - - /** - * Generates and returns the SQL statement according to this query. - * @param Connection $connection the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return string the generated SQL statement - */ - public function getSql($connection = null) - { - if ($connection === null) { - $connection = \Yii::$application->db; - } - return $connection->getQueryBuilder()->build($this); - } - - /** - * Creates a DB command that can be used to execute this query. - * @param Connection $connection the database connection used to generate the SQL statement. - * If this parameter is not given, the `db` application component will be used. - * @return Command the created DB command instance. - */ - public function createCommand($connection = null) - { - if ($connection === null) { - $connection = \Yii::$application->db; - } - return $connection->createCommand($this); - } - - /** - * Resets the query object to its original state. - * @return Query the query object itself - */ - public function reset() - { - foreach (get_object_vars($this) as $name => $value) { - $this->$name = null; - } - return $this; - } - - /** - * Merges this query with another one. - * - * The merging is done according to the following rules: - * - * - [[select]]: the union of both queries' [[select]] property values. - * - [[selectOption]], [[distinct]], [[limit]], [[offset]]: the new query - * takes precedence over this query. - * - [[where]], [[having]]: the new query's corresponding property value - * will be 'AND' together with the existing one. - * - [[params]], [[orderBy]], [[groupBy]], [[join]], [[union]]: the new query's - * corresponding property value will be appended to the existing one. - * - * In general, the merging makes the resulting query more restrictive and specific. - * @param array|Query $query the new query to be merged with this query. - * @return Query the query object itself - */ - public function mergeWith($query) - { - if (is_array($query)) { - $class = '\\' . get_class($this); - $query = $class::newInstance($query); - } - - if ($this->select !== $query->select) { - if (empty($this->select)) { - $this->select = $query->select; - } elseif (!empty($query->select)) { - $select1 = is_string($this->select) ? preg_split('/\s*,\s*/', trim($this->select), -1, PREG_SPLIT_NO_EMPTY) : $this->select; - $select2 = is_string($query->select) ? preg_split('/\s*,\s*/', trim($query->select), -1, PREG_SPLIT_NO_EMPTY) : $query->select; - $this->select = array_merge($select1, array_diff($select2, $select1)); - } - } - - if ($query->selectOption !== null) { - $this->selectOption = $query->selectOption; - } - - if ($query->distinct !== null) { - $this->distinct = $query->distinct; - } - - if ($query->limit !== null) { - $this->limit = $query->limit; - } - - if ($query->offset !== null) { - $this->offset = $query->offset; - } - - if ($query->where !== null) { - $this->andWhere($query->where); - } - - if ($query->having !== null) { - $this->andHaving($query->having); - } - - if ($query->params !== null) { - $this->addParams($query->params); - } - - if ($query->orderBy !== null) { - $this->addOrderBy($query->orderBy); - } - - if ($query->groupBy !== null) { - $this->addGroupBy($query->groupBy); - } - - if ($query->join !== null) { - if (empty($this->join)) { - $this->join = $query->join; - } else { - if (!is_array($this->join)) { - $this->join = array($this->join); - } - if (is_array($query->join)) { - $this->join = array_merge($this->join, $query->join); - } else { - $this->join[] = $query->join; - } - } - } - - if ($query->union !== null) { - if (empty($this->union)) { - $this->union = $query->union; - } else { - if (!is_array($this->union)) { - $this->union = array($this->union); - } - if (is_array($query->union)) { - $this->union = array_merge($this->union, $query->union); - } else { - $this->union[] = $query->union; - } - } - } - - return $this; - } } diff --git a/framework/util/Text.php b/framework/util/Text.php index c41a6e2..7ff7555 100644 --- a/framework/util/Text.php +++ b/framework/util/Text.php @@ -44,8 +44,27 @@ class Text return $name . 's'; } - public static function dd($value) + /** + * Converts a class name into space-separated words. + * For example, 'PostTag' will be converted as '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) + { + $label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(? __NAMESPACE__ . '\BarBehavior', + ); + } +} + class BarBehavior extends \yii\base\Behavior { public $behaviorProperty = 'behavior property'; @@ -21,12 +31,19 @@ class BehaviorTest extends \yiiunit\TestCase { public function testAttachAndAccessing() { - $bar = BarClass::newInstance(); + $bar = new BarClass(); $behavior = new BarBehavior(); $bar->attachBehavior('bar', $behavior); $this->assertEquals('behavior property', $bar->behaviorProperty); $this->assertEquals('behavior method', $bar->behaviorMethod()); - $this->assertEquals('behavior property', $bar->bar->behaviorProperty); - $this->assertEquals('behavior method', $bar->bar->behaviorMethod()); + $this->assertEquals('behavior property', $bar->asa('bar')->behaviorProperty); + $this->assertEquals('behavior method', $bar->asa('bar')->behaviorMethod()); + } + + public function testAutomaticAttach() + { + $foo = new FooClass(); + $this->assertEquals('behavior property', $foo->behaviorProperty); + $this->assertEquals('behavior method', $foo->behaviorMethod()); } }