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.