Skip to content

Model Behavior NamedScope

Mark edited this page Feb 14, 2014 · 11 revisions

Basic Usage

Attach the behavior to your AppModel:

App::uses('Model', 'Model');

class AppModel extends Model {
    public $actsAs = array('Tools.NamedScope');
}

Then define some scopes in your model:

App::uses('AppModel', 'Model');

class User extends AppModel {
    public $scopes = array(
        'active' => array('User.active' => 1),
        'admin' => array('User.role LIKE' => '%admin%'),
    );
}

Then you can use those scopes in any of your find queries:

$activeUsers = $this->User->find('all', array('scope' => array('active')));
$activeAdmins = $this->User->find('all', array('scope' => array('active', 'admin')));
$activeAdminList = $this->User->find('list', array('scope' => array('active', 'admin')));

Advanced Usage

If you also want to use scopedFind(), you will also get rid of all the many find wrappers around those scopes that will often be placed inside the models.

An example:

public function getActiveAdmins() {
    $this->virtualFields['fullname'] = "CONCAT(User.firstname, ' ', User.lastname)";
    $options = array(
        'fields' => array('User.id', 'User.fullname'),
        'conditions' => array('User.role LIKE' => '%admin%'),
        'order' => array('User.fullname'),
    );
    return $this->find('all', $options);
}

Now there will maybe also be a getActiveUsers() method and maybe a few dozen more, which all contain the same condition - which is not really DRY and might be quite error-prone if the conditions have to be adjusted (easy to miss one of the many occurrences in and out of the model).

So what would be a smarter way to approach this? Let's try to use the above scopes here - and also use the single wrapper method. Besides the above scopes, you also need to define some scopedFinds in your model:

App::uses('AppModel', 'Model');

class User extends AppModel {
    public $scopes = array(
        'activeAdmins' => array(
            'name' => 'Active admin users',
            'find' => array(
                'type' => 'all',
                'virtualFields' => array(
                    'fullname' => "CONCAT(User.firstname, ' ', User.lastname)"
                ),
                'options' => array(
                    'fields' => array('User.id', 'User.fullname'),
                    'scope' => array('active', 'admin'),
                    'order' => array('User.fullname'),
                ),
            ),
        ),
        'activeUsers' => array(
             ...
        )
    );
}

The scope itself will both contain active, and the config about this scope key will be stored in a single place. So if you have some very complex condition around published (> a && < b && != c && ...) this will take the overhead from multiple definitions and reduce it to a single location.

Let's execute it:

$activeAdmins = $this->User->scopedFind('activeAdmins');

In case we need to only get a list or the count, we can adjust the scopedFind:

$activeAdminList = $this->User->scopedFind('activeAdmins', array('type' => 'list'));
$activeAdminCount = $this->User->scopedFind('activeAdmins', array('type' => 'count'));

We can also overwrite the default options:

$config = array(
    'options' => array(
        'limit' => 2, 
        'order' => array('User.created' => 'DESC'))
);
$twoNewestActiveAdmins = $this->User->scopedFind('activeAdmins', $config);

You can also get a list of available scoped finds:

$scopedFinds = $this->User->scopedFinds();

Scoped finds:

  • require a name string
  • optionally use a find array

The find arrays:

  • optionally use a type string (defaults to all)
  • optionally use an options array
  • optionally use virtualFields

The options arrays:

  • can use the behaviors' scope property
  • support all other find options (including contain, order, group, limit, ...)

Tip: See the test cases for more complex examples.

Testing

You should test your scopes, even if it's just something like this:

public function testScopes() {
    $scopes = $this->User->scopes;
    // Each on its own
    foreach ($scopes as $scope) {
        $this->User->find('first', array('scope' => $scope));
    }
    // All together
    $this->User->find('first', array('scope' => $scopes));
}

In case there was invalid SQL, missing fields, wrong contain statements, it would be noticeable right away.

If you use scopedFinds, don't forget to also unit test them (regarding valid SQL). This can easily be forgotten now as you don't have the find wrapper methods anymore. In case you are lazy, add this test case to any model test that uses custom scopedFinds:

public function testScopedFinds() {
    $scopedFinds = $this->User->scopedFinds();
    foreach ($scopedFinds as $key) {
        $this->User->scopedFind($key);
    }
}

This will at least execute each find and throw an error if the SQL is invalid. It is advisable to have a more thorough test case for each find key, though, that includes the assert of the return value.

Clone this wiki locally