---
title: Craft CMS entry query scopes with Yii behaviors
description: Use Yii's behavior model to create reusable scopes for your element queries.
pubDate: 2023-09-01
---

import Notice from "../../components/Notice.astro";

<Notice>
    I [posed this idea](https://github.com/craftcms/cms/discussions/13636) in the Craft CMS GitHub discussion board for additional thoughts and contributions. It's worth visiting it to see if any new findings or considerations have been mentioned!

    **Update 2023-09-10**: Thanks to both [Brandon Kelly](https://github.com/craftcms/cms/discussions/13636#discussioncomment-6960289) and [@wsydney76](https://github.com/craftcms/cms/discussions/13636#discussioncomment-6905927) for supplying recommendations to optimize the original code examples. Those optimizations are included below the original code blocks.
</Notice>

[Behaviors](https://www.yiiframework.com/doc/guide/2.0/en/concept-behaviors) are a powerful way to extend your Craft entries, assets, users, and more.

[Andrew Welch wrote about behaviors](https://nystudio107.com/blog/extending-craft-cms-with-validation-rules-and-behaviors) in 2020 and there's a great example of using them to hook into the **beforeSave()** event and creating computed propeties like **getHappyName()** when fetching users via Twig (or PHP).

Here's how Yii defines behaviors:

> Behaviors, also known as mixins, allow you to enhance the functionality of an existing component class without needing to change the class's inheritance. Attaching a behavior to a component "injects" the behavior's methods and properties into the component, making those methods and properties accessible as if they were defined in the component class itself.

That means we can use behaviors for all sorts of functionalty. Not least of which is something I've grown to love about Laravel: [local query scopes](https://laravel.com/docs/10.x/eloquent#local-scopes). Here's how those work:

**A user model:**
```php
namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include popular users.
     */
    public function scopePopular(Builder $query): void
    {
        $query->where('votes', '>', 100);
    }
}
```

## Querying with a scope:
```php
use App\Models\User;

$users = User::popular()->get();
```

Now we have a tidy, reusable scope we can call from PHP, a Blade template, a [Livewire](https://livewire.laravel.com/) component, etc.

## Setting it up in Craft

[You'll need a module](https://craftcms.com/docs/4.x/extend/module-guide.html) to set this functionality up. I won't go into those setup details, but here's approximately what your module code might look like.

You'll notice we're extending **EntryQuery**, but this could be **AssetQuery**, **UserQuery**, (or others) depending on what you're querying.

See [Element Queries](https://craftcms.com/docs/4.x/element-queries.html) for more details and the full API at your disposal when creating your scope.

### The module class:

<Notice>
    **Update 2023-09-10**: The code I posted has been updated with recommendations from [Brandon Kelly](https://github.com/craftcms/cms/discussions/13636#discussioncomment-6960289).
</Notice>

```php
namespace modules\foo;

use yii\base\Event;
use craft\elements\db\EntryQuery;
use craft\events\DefineBehaviorsEvent;

// This is the class we'll be storing our scopes in
use modules\foo\behaviors\QueryBehavior;

class Module extends \yii\base\Module
{
    public function init()
    {
        // Setup an event that will bind our behavior to Craft's EntryQuery class
        // You might substitute EntryQuery for AssetQuery if you're working with assets, for example.
        Event::on(
            EntryQuery::class,
            EntryQuery::EVENT_DEFINE_BEHAVIORS,
            function (DefineBehaviorsEvent $event) {
                $event->behaviors[] = QueryBehavior::class;
            }
        );
    }
}
```

### The behavior class:

<Notice>
    **Update 2023-09-10**: The code I posted has been updated with recommendations from [wsydney76](https://github.com/craftcms/cms/discussions/13636#discussioncomment-6905927).
</Notice>

```php
namespace modules\foo\behaviors;

use craft\elements\db\EntryQuery;

class QueryBehavior extends \yii\base\Behavior
{
    /**
     * Scope the query to entries that have a rating of 4 or higher.
     */
    public function topRated(): EntryQuery
    {
        /**
         * This is an instance of EntryQuery, allowing you full access
         * to that API for your filtering
         *
         * @var EntryQuery $query
         */
        $query = $this->owner;

        // Use ->andWhere() to ensure preceeding ->where() clauses are not overwritten
        return $query->andWhere(['>=', 'field_rating', 4]);
    }
}
```

Now we can begin using our behavior to create commonly-used scopes when query entries. For instance:

### With Twig:

```twig
{% set entries = craft.entries.section('posts').topRated().all() %}
```

### With PHP:

```php
use craft\elements\Entry;

Entry::find()->section('posts')->topRated()->all();
```

And because we return the instance of the EntryQuery from within our scope, you can continue to chain these scopes:

```php
use craft\elements\Entry;

Entry::find()
  ->section('posts')
  ->topRated()
  ->limit(5)
  ->orderBy('title')
  ->all();
```

## Be careful, though!

While this is a nice way to centralize your commonly scoped queries, it might be ambiguous which queries these work for. For instance, tacking on our ->topRated() scope might not work for entries within the "pages" section.

It's also not entirely apparent where these are defined at first glance. Keep that in mind when you're working with a team or handing off your project to someone!
