Используем CAction, время = деньги

Обсуждение документации. Переводы Cookbook и авторские рецепты.
Ответить
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Используем CAction, время = деньги

Сообщение slavcodev »

Повесив на шею табличку: «не стреляйте в писателя, он пишет как может», расскажу, а точнее напомню, Вам об одной из возможностей YiiFramework. Речь пойдет о действиях. Из документации можно узнать, что действия в фреймворке могут быть двух видов:
  • Метод в классе контроллера с префиксом action.
  • Класс, наследованный от CAction или его предков.
Первый метод не представляет особого интереса, опустим его. А вот второй — другое дело. Даже авторы фреймворка называют его более продвинутым. Ну что же, разберем чем же он продвинут :)

Начнем с минусов:
  • Дополнительные файлы, растяпы могут легко потерять при переносе проекта.
Вот и все минусы, пришедшие мне в голову (подскажите ещё, если найдёте).

Плюсы:
  • Один и тот же код не придётся писать несколько раз.
  • Совершенствуя действие не придётся помнить о всех контроллерах.
Как видите, плюсы довольно таки весомые.

Ладно, рекламная пауза закончилась, приступим к более интересному. Приведём пример, чтоб закрепить у себя в памяти прочитанное.

Кратко о задаче:
Думаю, 9 из 10 сайтов имеют раздел новости. Конечно же могут быть частные случаи, но, в основном, новости — это список. Список новостей, отсортированных по дате создания, список новостей с определённым тегом, список определённого автора, список определённого статуса или просто новость с определенным ID (я её тоже отнесу к списку, хотя конечно же, список этот из одного элемента).

Приступим
Предположим, у нас есть модель News (не будем её описывать, так как атрибуты модели, правила валидации и всё остальное не так важно).
Так же есть представления: newsItem — для отдельной новости, newsList — для списка новостей.
Наше действие будет искать нужные записи, разбивать записи на страницы и выводить представление.

appliaction.controllers.News.ListAction.php

Код: Выделить всё

class ListAction extends CAction {
    /**
     * @var string Атрибут модели для фильтра поиска записей.
     */
    public $findAttr = NULL;
    /**
     * @var string Имя параметра $_GET для фильтра поиска записей.
     */
    public $findParam = NULL;
    /**
     * @var int Количество записей на странице.
     */
    public $postsPerPage = 10;
    /**
     * @var string Имя представления.
     */
    public $view = 'list';
    /**
     * @var CDBCriteria Критерий поиска.
     */
    public $criteria = NULL;
    /**
     * @var array Параметры представления.
     * Индекс элемента массива будет использован как имя переменной передаваемой в представление,
     * а сам элемент либо имя метода, либо callback-функция возвращающая значение переменной.
     */
    public $data = array();
    /**
     * @var CActiveRecord.
     */
    public $model = NULL;

    /**
     * Рендер представления.
     */
    public function run() {
        // Проверяем обязательный параметр
        if (is_null($this->model)) {
            throw new CException(Yii::t('yii', 'Property "{class}.{property}" is not defined.',
                array('{class}' => get_class($this), '{property}' => 'model')));
        }
        $data = array();
        // Обходим массив параметров представления и получаем значения параметра
        foreach ($this->data as $key => $var) {
            // Если элемент массива является валидной callback-функцией выполняем функция, получая значение.
            if (is_string($key) && is_callable($var)) {
                $data[$key] = call_user_func($var, $this);
            }
            // Если элемент строка, проверяем наличие метода в данном классе.
            elseif (is_string($var) && method_exists($this, 'get' . ucfirst($var))) {
                $data[is_string($key) ? $key : $var] = call_user_func(array(
                    $this,
                    'get' . ucfirst($var)
                ));
            }
        }
        $this->getController()->render($this->view, $data);
    }
    /**
     * Устанавливаем параметры представления.
     *
     * @param array $data
     */
    public function setData(array $data) {
        $this->data = is_array($data) ? $data : array();
    }
    /**
     * Установка имени атрибута модели.
     *
     * @param string $findAttr
     */
    protected function setFindAttr(string $findAttr) {
        $findAttr = trim($findAttr);
        $this->findAttr = empty($findAttr) ? NULL : $findAttr;
    }
    /**
     * Установка параметра.
     *
     * @param string $findParam
     */
    protected function setFindParam(string $findParam) {
        $findParam = trim($findParam);
        $this->findParam = empty($findParam) ? NULL : $findParam;
    }
    /**
     * Получаем значение для фильтра записей.
     *
     * @return string
     */
    public function getFindString() {
        return Yii::app()->request->getParam($this->findParam, NULL);
    }
    /**
     * Возвращает критерий поиска.
     *
     * @return CDbCriteria
     */
    public function getCriteria() {
        if (is_null($this->criteria)) $this->criteria = new CDbCriteria;
        return $this->criteria;
    }
    /**
     * Получаем модели.
     *
     * @return CActiveRecord
     */
    /*public function getModel() {
        return is_object($this->model) ? $this->model : NULL;
    }*/
    /**
     * Добавляет условие поиска.
     */
    public function addCondition() {
        $findString = $this->getFindString();
        if (!is_null($this->findAttr) && !empty($findString)) {
            $this->getCriteria()->addColumnCondition(array($this->findAttr => $findString));
        }
    }
    /**
     * Возвращает массив записей.
     *
     * @return array
     */
    public function getRecords() {
        $this->addCondition();
        return $this->model->findAll($this->getCriteria());
    }
    /**
     * Возвращает искомую запись.
     *
     * @return CActiveRecord
     */
    public function getRecord() {
        $this->addCondition();
        return $this->model->find($this->getCriteria());
    }
    /**
     * Возвращает количество записей.
     *
     * @return int
     */
    public function getCount() {
        return $this->model->count($this->getCriteria());
    }
    /**
     * Возвращает CPagination для списка.
     *
     * @return CPagination
     */
    public function getPages() {
        $pages = new CPagination($this->getCount());
        $pages->pageSize = $this->postsPerPage;
        $pages->applyLimit($this->getCriteria());
        return $pages;
    }
} 
Теперь научим наш контроллер выполнять нужные действия:

Код: Выделить всё

class NewsController extends CController {
    public function actions() {
        // Общий критерий, сортируем по дате добавления
        $criteria = new CDbCriteria;
        $criteria->order = 'сreatedTime DESC';

        // Путь до действия
        $path = 'appliaction.controllers.News.ListAction';

        return array(
            // Действия для показа списка новостей
            'list' => array(
                'class' => $path,
                'model' => News::model(),
                'postsPerPage' => 5,
                'criteria' => $criteria,
                'data' => array('pages', 'records'),
            ),
            // Покажем список, но теперь уже с другим представлением — таблицей
            'table' => array(
                'class' => $path,
                'model' => News::model(),
                'view' => 'table',
                'criteria' => $criteria,
                'data' => array('pages', 'records'),
            ),
            // Список новостей с заданным тегом
            'tag' => array(
                'class' => $path,
                'model' => News::model(),
                'criteria' => $criteria,
                'findParam' => 'tag',
                'data' => array(
                    'pages' => array($this, 'getpagesByTags'),
                    'records' => array($this, 'getRecordsByTags'),
                ),
            ),
            // Покажем новость с определенным ID
            'id' => array(
                'class' => $path,
                'model' => News::model(),
                'findParam' => 'id',
                'findAttr' => 'id',
                'criteria' => $criteria,
                'view' => 'show',
                'data' => array('record'),
            ),
            // Список новостей с заданным алиасом
            'show' => array(
                'class' => $path,
                'model' => News::model(),
                'findParam' => 'alias',
                'findAttr' => 'alias',
                'criteria' => $criteria,
                'view' => 'show',
                'data' => array('record'),
            ),
            // Действие для отображения списка новостей заданного автора через адресную строку
            'author' => array(
                'class' => $path,
                'model' => News::model(),
                'findParam' => 'author',
                'findAttr' => 'author',
                'postsPerPage' => 20,
                'criteria' => $criteria,
                'data' => array('pages', 'records'),
            ),
        );
    }

    // Для списка новостей с заданным тегом нам понадобятся callback-функции
    // Теги в модели реализованы с помощью CTagableBehavior (ищите на форуме)
    public function getCountByTags(&$action) {
        return News::model()
            ->withTags($action->getFindString())
            ->count($action->getCriteria());
    }

    public function getRecordsByTags(&$action) {
        return News::model()
            ->withTags($action->getFindString())
            ->findAll($action->getCriteria());
    }
    public function getPagesByTags(&$action) {
        $pages = new CPagination($this->getCountByTags($action));
        $pages->pageSize = $action->postsPerPage;
        $pages->applyLimit($action->getCriteria());
        return $pages;
    }
} 
Проверяем в браузере:
http://localhost/news/list/
http://localhost/news/tag/tag/yii/
http://localhost/news/id/id/1/
http://localhost/news/author/author/mc-bear/

И, напоследок, бонус, в наше действие я неспроста внёс модель. Если, вдруг, у нас будет другая модель, например User, и в контроллере UserController нам нужно будет вывести список пользователей, можно использовать написанное заранее действие:

Код: Выделить всё

class UserController extends CController {
    public function actions() {
        // Общий критерий, сортируем по имени пользователя
        $criteria = new CDbCriteria;
        $criteria->order = 'name DESC';

        // Путь до действия
        $path = 'appliaction.controllers.News.ListAction';

        return array(
            // Действие для отображения списка пользователей
            'list' => array(
                'class' => $path,
                'model' => User::model(),
                'postsPerPage' => 20,
                'criteria' => $criteria,
                'data' => array('pages', 'records'),
            ),
            // Действия для отображения списка пользователей определенного статуса
            'status' => array(
                'class' => $path,
                'model' => User::model(),
                'postsPerPage' => 20,
                'criteria' => $criteria,
                'findParam' => 'status',
                'findAttr' => 'status',
                'data' => array('pages', 'records'),
            ),
        );
    }
} 
Проверяем в браузере:
http://localhost/user/list/
http://localhost/user/status/status/admin/
Вложения
protected.zip
Пример использования
(4.97 КБ) 554 скачивания
Жду Yii 3!
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Используем CAction, время = деньги

Сообщение samdark »

Таким же образом, по идее, можно реализовать административный интерфейс для модели на основе мета-данных и правил валидации.
Да и вообще любой однотипный код контроллера таким образом можно использовать повторно.
Ekstazi
Сообщения: 1428
Зарегистрирован: 2009.08.20, 22:54
Откуда: Молдова, Бельцы
Контактная информация:

Re: Используем CAction, время = деньги

Сообщение Ekstazi »

Хы, почти круд получился.
Dreammaker
Сообщения: 139
Зарегистрирован: 2009.09.02, 16:21
Откуда: Черкассы, Украина

Re: Используем CAction, время = деньги

Сообщение Dreammaker »

Сюда бы ещё create, update с валидацией прикрутить :)
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Используем CAction, время = деньги

Сообщение samdark »

Это уже не для рецепта задача. Но идея хорошая.
pirrat
Сообщения: 193
Зарегистрирован: 2009.04.03, 09:41

Re: Используем CAction, время = деньги

Сообщение pirrat »

Спасибо, интересные мысли, берем на вооружение.
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Используем CAction, время = деньги

Сообщение slavcodev »

Dreammaker писал(а):Сюда бы ещё create, update с валидацией прикрутить :)
Это совсем другой экшн :) Мне не нравится идея мешать все в одну кашу, тем более фреймворк позволяет легко строить из кирпичиков.
Жду Yii 3!
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Используем CAction, время = деньги

Сообщение samdark »

А если CRUD-наборчик? ;)
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Используем CAction, время = деньги

Сообщение slavcodev »

Не одним экшеном, это уж точно.
Жду Yii 3!
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Используем CAction, время = деньги

Сообщение slavcodev »

По просьбе в личке, приатачил пример. Включает контроллер, модель, представление, само действие и схемы для БД.
Жду Yii 3!
Аватара пользователя
radamir
Сообщения: 142
Зарегистрирован: 2009.08.10, 08:02
Откуда: Новосибирск

Re: Используем CAction, время = деньги

Сообщение radamir »

Ради такого простого действия в 3-5 строчек кода в зависимости от сложности выборки, прикручивать такой сложный аппарат. Жуть. Хотя как учебный пример иллюстрирующий применения внешнего действия очень хорошо.
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Используем CAction, время = деньги

Сообщение slavcodev »

Подниму тему, т.к. почему-то обсуждение пошло не по теме :)
Писал рецепт в надежде что найдутся разработчики чей опыт сможет помочь решить,
стоит ли выносить действия из контроллера в отдельные классы.

Теперь когда на форуме больше людей общаются и работы с Yii больше опыта, может обсудим? Да, нет, почему?
Жду Yii 3!
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Используем CAction, время = деньги

Сообщение samdark »

Если действие типовое и отличается только параметрами — да, определённо стоит. Хороший пример — отдача JSON для поля автокомплита.
tiron
Сообщения: 1
Зарегистрирован: 2012.10.16, 13:27

Re: Используем CAction, время = деньги

Сообщение tiron »

Огромное спасибо за рецепт! Как раз то, что искал ;)
Ответить