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.