diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index 875baf82b7..3ea344b312 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -547,32 +547,60 @@ Finally when calling [[delete()]] to delete an ActiveRecord, we will have the fo 3. [[afterDelete()]]: will trigger an [[EVENT_AFTER_DELETE]] event -Scopes ------- +Custom scopes +------------- -A scope is a method that customizes a given [[ActiveQuery]] object. Scope methods are static and are defined -in the ActiveRecord classes. They can be invoked through the [[ActiveQuery]] object that is created -via [[find()]] or [[findBySql()]]. The following is an example: +When [[find()]] or [[findBySql()]] Active Record method is being called without parameters it returns an [[ActiveQuery]] +instance. This object holds all the parameters and conditions for a future query and also allows you to customize these +using a set of methods that are called scopes. By deafault there is a good set of such methods some of which we've +already used above: `where`, `orderBy`, `limit` etc. + +In many cases it is convenient to wrap extra conditions into custom scope methods. In order to do so you need two things. +First is creating a custom query class for your model. For example, a `Comment` may have a `CommentQuery`: ```php -class Comment extends \yii\db\ActiveRecord -{ - // ... +namespace app\models; - /** - * @param ActiveQuery $query - */ - public static function active($query) +import yii\db\ActiveQuery; + +class CommentQuery extends ActiveQuery +{ + public function active($state = true) { - $query->andWhere('status = 1'); + $this->andWhere(['active' => $state]); + return $this; } } - -$comments = Comment::find()->active()->all(); ``` -In the above, the `active()` method is defined in `Comment` while we are calling it -through `ActiveQuery` returned by `Comment::find()`. +Important points are: + +1. Class should extend from `yii\db\ActiveQuery` (or another `ActiveQuery` such as `yii\mongodb\ActiveQuery`). +2. A method should be `public` and should return `$this` in order to allow method chaining. It may accept parameters. +3. Check `ActiveQuery` methods that are very useful for modifying query conditions. + +The second step is to use `CommentQuery` instead of regular `ActiveQuery` for `Comment` model: + +``` +namespace app\models; + +use yii\db\ActiveRecord; + +class Comment extends ActiveRecord +{ + public static function createQuery() + { + return new CommentQuery(['modelClass' => get_called_class()]); + } +} +``` + +That's it. Now you can use your custom scope methods: + +```php +$comments = Comment::find()->active()->all(); +$inactiveComments = Comment::find()->active(false)->all(); +``` You can also use scopes when defining relations. For example, @@ -597,29 +625,32 @@ $posts = Post::find()->with([ ])->all(); ``` -Scopes can be parameterized. For example, we can define and use the following `olderThan` scope: +### Making it IDE-friendly + +In order to make most modern IDE autocomplete happy you need to override return types for some methods of both model +and query like the following: ```php -class Customer extends \yii\db\ActiveRecord +/** + * @method \app\models\CommentQuery|static|null find($q = null) static + * @method \app\models\CommentQuery findBySql($sql, $params = []) static + */ +class Comment extends ActiveRecord { // ... - - /** - * @param ActiveQuery $query - * @param integer $age - */ - public static function olderThan($query, $age = 30) - { - $query->andWhere('age > :age', [':age' => $age]); - } } - -$customers = Customer::find()->olderThan(50)->all(); ``` -The parameters should follow after the `$query` parameter when defining the scope method, and they -can take default values like shown above. - +```php +/** + * @method \app\models\Comment|array|null one($db = null) + * @method \app\models\Comment[]|array all($db = null) + */ +class CommentQuery extends ActiveQuery +{ + // ... +} +``` Transactional operations ------------------------ diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index cd40dbe5f6..d3644e0c47 100644 --- a/docs/guide/upgrade-from-v1.md +++ b/docs/guide/upgrade-from-v1.md @@ -459,6 +459,8 @@ By default, ActiveRecord now only saves dirty attributes. In 1.1, all attributes are saved to database when you call `save()`, regardless of having changed or not, unless you explicitly list the attributes to save. +Scopes are now defined in a custom `ActiveQuery` class instead of model directly. + See [active record docs](active-record.md) for more details. diff --git a/framework/db/ActiveQuery.php b/framework/db/ActiveQuery.php index 1731fc1f5b..df78e87994 100644 --- a/framework/db/ActiveQuery.php +++ b/framework/db/ActiveQuery.php @@ -60,7 +60,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface * Executes query and returns all results as an array. * @param Connection $db the DB connection used to create the DB command. * If null, the DB connection returned by [[modelClass]] will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. + * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. */ public function all($db = null) { diff --git a/framework/db/ActiveQueryTrait.php b/framework/db/ActiveQueryTrait.php index fa24ed3e94..4444f3426e 100644 --- a/framework/db/ActiveQueryTrait.php +++ b/framework/db/ActiveQueryTrait.php @@ -31,32 +31,6 @@ trait ActiveQueryTrait */ public $asArray; - - /** - * PHP magic method. - * This method allows calling static method defined in [[modelClass]] via this query object. - * It is mainly implemented for supporting the feature of scope. - * - * @param string $name the method name to be called - * @param array $params the parameters passed to the method - * @throws \yii\base\InvalidCallException - * @return mixed the method return result - */ - public function __call($name, $params) - { - if (method_exists($this->modelClass, $name)) { - $method = new \ReflectionMethod($this->modelClass, $name); - if (!$method->isStatic() || !$method->isPublic()) { - throw new InvalidCallException("The scope method \"{$this->modelClass}::$name()\" must be public and static."); - } - array_unshift($params, $this); - call_user_func_array([$this->modelClass, $name], $params); - return $this; - } else { - return parent::__call($name, $params); - } - } - /** * Sets the [[asArray]] property. * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. @@ -175,7 +149,7 @@ trait ActiveQueryTrait * Finds records corresponding to one or multiple relations and populates them into the primary models. * @param array $with a list of relations that this query should be performed with. Please * refer to [[with()]] for details about specifying this parameter. - * @param array $models the primary models (can be either AR instances or arrays) + * @param array|ActiveRecord[] $models the primary models (can be either AR instances or arrays) */ public function findWith($with, &$models) { diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index c8d199b2b4..ff16476b87 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -102,7 +102,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface * - an array of name-value pairs: query by a set of column values and return a single record matching all of them. * - null: return a new [[ActiveQuery]] object for further query purpose. * - * @return ActiveQuery|ActiveRecord|null When `$q` is null, a new [[ActiveQuery]] instance + * @return ActiveQuery|static|null When `$q` is null, a new [[ActiveQuery]] instance * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be * returned (null will be returned if there is no matching). * @throws InvalidConfigException if the AR class does not have a primary key diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index 2d9618a51f..0891b159be 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -1,6 +1,7 @@ hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('id'); } - public static function active($query) - { - $query->andWhere('status=1'); - } - public function afterSave($insert) { ActiveRecordTest::$afterSaveInsert = $insert; ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; parent::afterSave($insert); } + + public static function createQuery() + { + return new CustomerQuery(['modelClass' => get_called_class()]); + } } diff --git a/tests/unit/data/ar/CustomerQuery.php b/tests/unit/data/ar/CustomerQuery.php new file mode 100644 index 0000000000..6b1013452b --- /dev/null +++ b/tests/unit/data/ar/CustomerQuery.php @@ -0,0 +1,16 @@ +andWhere('status=1'); + return $this; + } +} + \ No newline at end of file diff --git a/tests/unit/data/ar/elasticsearch/Customer.php b/tests/unit/data/ar/elasticsearch/Customer.php index a4306c705e..8caf48a431 100644 --- a/tests/unit/data/ar/elasticsearch/Customer.php +++ b/tests/unit/data/ar/elasticsearch/Customer.php @@ -35,11 +35,6 @@ class Customer extends ActiveRecord return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('created_at'); } - public static function active($query) - { - $query->andWhere(['status' => 1]); - } - public function afterSave($insert) { ActiveRecordTest::$afterSaveInsert = $insert; @@ -67,4 +62,9 @@ class Customer extends ActiveRecord ]); } + + public static function createQuery() + { + return new CustomerQuery(['modelClass' => get_called_class()]); + } } diff --git a/tests/unit/data/ar/elasticsearch/CustomerQuery.php b/tests/unit/data/ar/elasticsearch/CustomerQuery.php new file mode 100644 index 0000000000..5df080f0b1 --- /dev/null +++ b/tests/unit/data/ar/elasticsearch/CustomerQuery.php @@ -0,0 +1,16 @@ +andWhere(array('status' => 1)); + return $this; + } +} + \ No newline at end of file diff --git a/tests/unit/data/ar/mongodb/Customer.php b/tests/unit/data/ar/mongodb/Customer.php index 7b98fe6331..01f545f048 100644 --- a/tests/unit/data/ar/mongodb/Customer.php +++ b/tests/unit/data/ar/mongodb/Customer.php @@ -2,6 +2,8 @@ namespace yiiunit\data\ar\mongodb; +use yii\mongodb\ActiveQuery; + class Customer extends ActiveRecord { public static function collectionName() @@ -20,13 +22,13 @@ class Customer extends ActiveRecord ]; } - public static function activeOnly($query) - { - $query->andWhere(['status' => 2]); - } - public function getOrders() { return $this->hasMany(CustomerOrder::className(), ['customer_id' => '_id']); } + + public static function createQuery() + { + return new CustomerQuery(['modelClass' => get_called_class()]); + } } \ No newline at end of file diff --git a/tests/unit/data/ar/mongodb/CustomerQuery.php b/tests/unit/data/ar/mongodb/CustomerQuery.php new file mode 100644 index 0000000000..bf222b6d3d --- /dev/null +++ b/tests/unit/data/ar/mongodb/CustomerQuery.php @@ -0,0 +1,16 @@ +andWhere(['status' => 2]); + return $this; + } +} + \ No newline at end of file diff --git a/tests/unit/data/ar/mongodb/file/CustomerFile.php b/tests/unit/data/ar/mongodb/file/CustomerFile.php index a396477825..40da8bf5d8 100644 --- a/tests/unit/data/ar/mongodb/file/CustomerFile.php +++ b/tests/unit/data/ar/mongodb/file/CustomerFile.php @@ -20,8 +20,8 @@ class CustomerFile extends ActiveRecord ); } - public static function activeOnly($query) + public static function createQuery() { - $query->andWhere(['status' => 2]); + return new CustomerFileQuery(['modelClass' => get_called_class()]); } } \ No newline at end of file diff --git a/tests/unit/data/ar/mongodb/file/CustomerFileQuery.php b/tests/unit/data/ar/mongodb/file/CustomerFileQuery.php new file mode 100644 index 0000000000..6adbe801f2 --- /dev/null +++ b/tests/unit/data/ar/mongodb/file/CustomerFileQuery.php @@ -0,0 +1,16 @@ +andWhere(['status' => 2]); + return $this; + } +} + \ No newline at end of file diff --git a/tests/unit/data/ar/redis/Customer.php b/tests/unit/data/ar/redis/Customer.php index 63143ffe56..8e5a1b8d6c 100644 --- a/tests/unit/data/ar/redis/Customer.php +++ b/tests/unit/data/ar/redis/Customer.php @@ -2,6 +2,7 @@ namespace yiiunit\data\ar\redis; +use yii\redis\ActiveQuery; use yiiunit\extensions\redis\ActiveRecordTest; class Customer extends ActiveRecord @@ -24,15 +25,15 @@ class Customer extends ActiveRecord return $this->hasMany(Order::className(), ['customer_id' => 'id']); } - public static function active($query) - { - $query->andWhere(['status' => 1]); - } - public function afterSave($insert) { ActiveRecordTest::$afterSaveInsert = $insert; ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; parent::afterSave($insert); } + + public static function createQuery() + { + return new CustomerQuery(['modelClass' => get_called_class()]); + } } \ No newline at end of file diff --git a/tests/unit/data/ar/redis/CustomerQuery.php b/tests/unit/data/ar/redis/CustomerQuery.php new file mode 100644 index 0000000000..47d69fb95d --- /dev/null +++ b/tests/unit/data/ar/redis/CustomerQuery.php @@ -0,0 +1,16 @@ +andWhere(['status' => 1]); + return $this; + } +} + \ No newline at end of file diff --git a/tests/unit/data/ar/sphinx/ArticleIndex.php b/tests/unit/data/ar/sphinx/ArticleIndex.php index ed1073dcb7..66d6bebc15 100644 --- a/tests/unit/data/ar/sphinx/ArticleIndex.php +++ b/tests/unit/data/ar/sphinx/ArticleIndex.php @@ -1,8 +1,7 @@ andWhere('author_id=1'); - } - public function getSource() { return $this->hasOne(ArticleDb::className(), ['id' => 'id']); @@ -32,4 +26,9 @@ class ArticleIndex extends ActiveRecord { return $this->source->content; } + + public static function createQuery() + { + return new ArticleIndexQuery(['modelClass' => get_called_class()]); + } } \ No newline at end of file diff --git a/tests/unit/data/ar/sphinx/ArticleIndexQuery.php b/tests/unit/data/ar/sphinx/ArticleIndexQuery.php new file mode 100644 index 0000000000..f5843f001d --- /dev/null +++ b/tests/unit/data/ar/sphinx/ArticleIndexQuery.php @@ -0,0 +1,16 @@ +andWhere('author_id=1'); + return $this; + } +} + \ No newline at end of file