DevGang
Авторизоваться

CakePHP и NamedScope для принципов DRY

Реализовано в CakePHP 2 версии (2.x)

Предыстория

Я наткнулся на этот форк и SimpleScope.

И что мы видим? Последний имеет недостаток избыточности в таких условиях объема, когда используется множество конфигурациях поиска. А первый был в принципе похож на то, как работают scopes в Rails. Но среди других мелких проблем мне не хватало возможности использования атрибутов модели для конфигурации. И эти обе проблемы не встречались в тестовых кейсах.

Поэтому я решил их объединить , досконально протестировать и попробовать извлечь максимальную пользу от основных идей реализации.

Основное использование

Более подробное объяснение поведения представлено в документации в вики.

А сейчас перейдем к краткому гайду.

Сначала установите/скачайте и загрузите плагин Tools, как описано в гайде или в файле readme.

Используйте это поведение в AppModel:

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

Затем определите области применения в вашей модели:

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

Потом вы сможете использовать эти области в любом из поисковых запросов:

$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')));

Расширенное использование

Используете scopedFind(), чтобы не использовать многочисленные обертки find вокруг тех областей, которые часто будут размещаться внутри моделей.

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);
}

Сейчас, вероятно, там будет метод getActiveUsers(), а так же еще несколько десятков, все из которых содержат одно и то же условие, которое на самом деле не DRY и может быть весьма подвержено ошибкам в случае корректировки и изменений (легко пропустить одно из множества вхождений в и из модели).

Так какой же более умный подход применить в данном случае?

Давайте попробуем использовать здесь вышеуказанные области , а также использовать метод одиночной обертки. 

Помимо всех вышеперечисленных областей, вам также необходимо определить некоторые scopedFinds в вашей модели:

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(
             ...
        )
    );
}

Сама область будет содержать active и конфигурация ключа области будет храниться в одном месте.Поэтому, если вокруг опубликованного листинга (>a & & < b&&!= c &&...) у вас есть очень сложное условие, эта реализация позволит сократить код от нескольких определений до одной строчки.

Чтож, давайте сделаем это: 

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

В случае, если нам нужно получить только список или число, мы можем настроить scopedFind:

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

Перезапишем параметры по умолчанию:

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

А еще,  вы можете получить список доступных областей поиска(scoped finds):

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

Области поиска (Scoped finds) :

  • требуют строку имени name
  • при желании вы можете использовать массив поиска find

Массивы поиска (find arrays):

  • опционально используйте type string (по умолчанию all)
  • при желании используйте массив options
  • при желании используйте virtualFields

Массивы опций options:

  • могут использовать свойство поведения scope 
  • могут поддерживать все остальные параметры поиска (включая contain, order, group, limit, …)

Тестирование

Вы должны проверить свои области scopes , пусть даже так просто:

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));
}

Вы бы сразу заметили недопустимый код SQL, отсутствующие поля и неправильные операторы.

При использовании scopedFinds, не забудьте выполнить также их модульное тестирование (проверить корректность кода SQL).

Теперь об этом можно забыть , так как у вас больше нет методов поиска оберток.

Если ленитесь просто добавьте этот пример в любой тест модели, в котором используются пользовательские scopedFinds:

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

В этом случае, по крайней мере, выполнится каждый поиск и если код SQL некоректный, программа выбросит ошибку.

Рекомендуется тщательно проработать тестовые кейсы для каждого ключа find, который утверждает возвращаемое значения.

В будущем:

Бьюсь об заклад, что когда будут часто использовать это поведение , придется довольно много править ошибки. Но, в целом, этот пример, кажется, уже охватывает большинство случаев использования.

С Cake3 и масштабируемыми кастомными finders большая часть возможностей, которые нам  нужны для этих задач войдут в основную функциональность. И это будет прекрасно!

И тем не менее, до тех пор используйте это решение для того, чтобы сохранить scopes и conditions DRY.

#PHP #CakePHP
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

Сделайте первый шаг к новой профессии

Получить скидку