Yii контроллеры, экшены, фильтры и немного о производительности

Начнем с основ.

Я не ошибусь если предположу, что большинство современных фреймворков для разработки web-приложений основаны на паттерне MVC (модель-представление-контроллер). Не буду углубляться в суть этого подхода, интересующимся можно прочитать об этом тут. Yii не исключение. Для обработки запроса пользователя, необходимо создать контроллер, который может содержать несколько, так называемых «экшенов». Рассмотрим пример. Пусть, нам необходимо разработать функционал для работы с постами (допустим мы разрабатываем блог). Под «работой» я понимаю, то, что нам необходимо обеспечить возможность выполнения следующих операций — создание поста, изменение и удаление. Как правило конкретную работу выполняют как раз экшены, а контроллер служит своего рода объединяющим контейнером для них. В Yii контроллером является класс производный от CController, таким образом простейший контроллер может быть иметь следующий вид:

  class PostController extends CController
  {
   ...
  }

Yii накладывает несколько ограничений на объявление класса контроллера:

1. Наименование класса должно быть быть следующим <название_контроллера>Controller — окончание «Controller» — является обязательным. Пример: PostController.

2. Название файла, содержащего контроллер должно совпадать с наименованием класса. Пример: PostController.php.

Как говорилось выше, основные действия выполняют именно экшены, рассмотрим их реализацию в Yii.

Экшеном (action) может быть как метод контроллера, так и отдельный класс.

В простейшем и самом распространенном варианте, экшеном является метод контроллера. Здесь Yii так же накладывает свои правила:

1. Название метода должно содержать префикс «action».

Пример:

// создаем action - create
public function actionCreate()
{
   ....
}

И так мы имеем простейший контроллер, который имеет следующий вид:

class PostController extends CController
{
 public function actionCreate()
 {
  echo 'Вызван экшн create!';
 }
}

Сохраним данный код в файле PostController.php, и поместим его в каталог Controllers приложения.

Теперь мы можем обратиться к нашему экшену, используя следующий url:

http://localhost/testDrive/index.php?r=post/create

(тут я предполагаю что Yii-приложение расположено в каталоге testDrive, в корне web-сервера)

Параметр r, который имеет значение post/create, в терминах Yii, называется роутом. Как несложно догадаться, первая часть роута (post) — обозначает идентификатор контроллера (PostController), вторая часть (create) — идентификатор экшена (в данном случае мы вызываем экшн actionCreate, в контроллере PostController).

Кроме стандартного способа создания экшенов (как методов контроллера), Yii позволяет создавать экшены в виде отдельных классов (В Codeigniter и Kohana такого делать нельзя, Symfony, кажется, умеет так).

Экшеном является класс, унаследованный от CAction.

Пример:

// экшн выполненный в виде отдельного класса
class ActionShow extends CAction
{
  public function run()
  {
    //логика экшена располагается здесь
    echo "Экшн в виде отдельного класса!"
    .....
  }
}

Сохраним данный код в файле ActionShow.php и поместим его в каталог

/testDrive/protected/controllers/post/ (его необходимо создать).

Так как мы «вынесли» экшн в отдельный класс, необходимо как-то сообщить нашему контроллеру где искать класс экшена. Для этого необходимо переопределить метод actions контроллера, который должен вернуть массив, следующего вида:

‘actionName’ => ‘path.to.action.class’

Пример:

array(
'show' => 'application.controllers.post.ActionShow'
);

‘show’ — имя вызываемого экшена;

‘application.controllers.post.ActionShow’ — алиас пути, иными словами путь к файлу экшена в структуре приложения Yii (физически это путь к файлу /testDrive/protected/controllers/post/ActionShow.php)

Определив метод actions, наш контроллер будет иметь следующий вид:

class PostController extends CController
{
   public function actions()
  {
     return array(
                  'show' => 'application.controllers.post.ActionShow'
                );
  }
    public function actionCreate()
   {
       echo 'Вызван экшн create!';
   }
}

Теперь при обращении к url

http://localhost/testDrive/index.php?r=post/show

мы должны увидеть вызов метода run, класса ActionShow.

Возникает вопрос: «для чего выносить экшены в отдельные классы?»

Как отмечается в документации Yii, это позволяет организовать приложение по модульному принципу, таким образом один и тот же экшн, реализованный в виде отдельного класса, может быть использован несколькими контроллерами. Лично мне, тоже, гораздо удобнее работать с php файлом, в котором расположено минимум php-кода. Вынести экшн в отдельный класс это конечно же хорошо и удобно — но есть другая сторона вопроса — производительность. Как мне кажется (внутренности Yii я не смотрел), при рализации экшена как отдельного класса, на плечи Yii ложится несколько дополнительных задач, а именно:

1. Необходимо преобразовать alias пути в реальный путь до класса экшена.

2. Необходимо подключить файл экшена.

3. Необходимо создать экземпляр класса экшена.

4. Необходимо выполнить метод run().

Мне было интересно какой и способов реализации экшенов работает быстрее и насколько. Для этого я провел небольшое тестирование.

Был написан следующий контроллер:

/**
* Description of TestActionController
*
* @author andrey
*/
class TestActionController extends CController
{
   //put your code here
   public function actions()
    {
        return array(
                   'outer' => 'application.controllers.outer.OuterAction'
               );
    }
    public function actionInline()
   {
       print "inline action...";
   }
}

Внешний экшн имеет вот такой вид:

/**
* Description of OuterAction
*
* @author andrey
*/
class OuterAction extends CAction
{
    //put your code here
    public function run()
   {
      print "outer action...";
   }
}

Для тестирования, утилита ab (apache bench) обращалась к следущим url:

http://localhost/yii/index.php/TestAction/outer  — для тестирования экшена, выполненного в виде отдельного класса

http://localhost/yii/index.php/TestAction/inline — для тестирование экшена, выполнненого в виде метода контроллера

Тестирование проводилось на моей домашней машине: core2duo 2.2, 2GB — ОП, Ubuntu 8.10, Apache 2.2.9, php 5.2.6.

Команда для тестирования ab -t 10 -c 10 url.

И вот такие получились результаты.

экшн как метод контроллера ~ 92-95 rps

экшн как отдельный класс ~ 87-90 rps

Как видно из результатов, производительность отличается, примерно на 2-5 запросов в секунду, в пользу «встроенных» экшенов. Конечно, для очень больших и нагруженных проектов — даже эти 5 запросов очень важны, однако для меня же удобнее использовать «внешние» экшены, несмотря на «мизирный» проигрыш в скорости работы.

Хотелось бы сказать еще несколько слов о фильтрах. Согласно документации фильтр — это код, который может выполняться до и/или после выполнения любого экшена. Как и сами экшены — фильтры могут быть реализованы двумя способами:

  • как метод контролера
  • как отдельный класс

При реализации фильтра как метода контроллера, необходимо создать метод с префиксом «filter», пример:

// фильтр как метод контроллера
public function filterInAction($filterChain)
{
  // код фильтра
  .......
  $filterChain->run();
}

Особенности:

1. Префикс «filter» — обязателен.

2. Модификатор доступа для метода фильтра — public.

3. Строка $filterChain->run() — обязательна для выполнения цепочки фильтров и запрошенного экшена.
4. Встроенные фильтры могут выполняться только перед выполнением экшена (я не нашел способа выполнить встроенный фильтр после экшена)

Для того что-бы контроллер «знал» о необходимости выполнения фильтра — необходимо переопределить его метод «filters», пример:

public function filters()
{
   return array(
       'InAction'  // указываем название метода фильтра БЕЗ префикса "filter"
   );
}

Данный код говорит о том, что фильтр «InAction» — будет выполнен при обращеннии к любому экшену контроллера. Для того что бы исключить некоторые экшены, необходимо переписать метод вот так:

 public function filters()
 {
     return array(
         'InAction - inline'  // указываем название метода фильтра БЕЗ префикса "filter"
     );
 }

фрагмент строки «- inline» — говорит о том, что экшн «inline» не будет обработан фильтром.

Второй способ реализации фильтров — в виде отдельного класса. Для этого необходимо создать класс, производный от CFilter и переопределить его методы.

Пример:

 class OuterFilter extends CFilter
 {
    public function preFilter($filterChain)
    {
       // код выполняемый перед экшеном
       .....
       return true;
   }
   public function postFilter($filterChain)
  {
    // код выполняемый после экшена
    .....
  }
}

Особенности:

1. Оба метода должны принимать один параметр — $filterChain

2. Для того что бы продолжилось выполнение экшена или следующего в цепочке фильтра, метод preFilter  должен вернуть true (если вернуть false — выполнение прекращается…)

Для использования внешнего фильтра, так как же и при использовании «встроенного» — необходимо переопределить метод filters контроллера.

Пример:

 public function filters()
 {
     return array(
       'InAction - inline',
       array('application.filters.OuterFilter')
     );
 }

Обратите внимание на то, что  при указании «внешнего» фильтра, элементом массива фильтров, является так же массив, а не строка, как при объявлении «внутренних» фильтров. Используя такой подход можно передавать дополнительные параметры в фильтр, например вот так:

public function filters()
{
    return array(
    'InAction - inline',
     array(
          'application.filters.OuterFilter',
          'clean' => 'all'
     )
 );
}

В этом примере, мы передаем параметр «clean» со значением «all».

Так же как и при работе со «встроенными» фильтрами, можно указывать какие

экшены контроллера стоит обрабатывать, а какие нет.

// пример - исключаем экшн "inline" из обработки фильтром "OuterFilter"
public function filters()
 {
   return array(
     'InAction - inline',
     array(
         'application.filters.OuterFilter - inline',
         'clean' => 'all'
    )
 );
}

При использовании внешних фильтров, так же как и при использовании внешних экшенов, Yii должен проделать некоторую дополнительную работу.

1. Необходимо преобразовать alias пути в реальный путь до класса фильтра.

2. Необходимо подключить файл фильтра.

3. Необходимо создать экземпляр класса фильтра.

4. Необходимо выполнить методы preFilter() и postFiltrer().

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

Встроенный экшн + встроенный фильтр ~  89 — 90 rps;

Встроенный экшн + внешний фильтр       ~  87 — 89 rps;

Внешний экшн + встроенный фильр         ~  84 — 88 rps;

Внешний экшн + внешний фильр             ~ 87 — 90 rps;

Применение внешних фильтров дает следующие преимущества:

1. Код становится более модульным, упрощается повторное использование

2. Внешние фильтры, в отличии от встроенных, позволяют выполнять действия как до, так и после выполнения экшена.

Выводы.

Лично для себя я решил следующее:

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

Каждый выбирает свой способ реализации, надеюсь, эта статья поможет Вам сделать выбор!

Удачного yii-кодинга!

Читайте так же:

Yii, пишем фильтр для предотвращения XSS атак

Обсудить на форуме

Основной сайт Юпи! — http://yupe.ru

Исходный код — https://github.com/yupe/yupe

Присоединяйтесь!

Читайте еще:

  • Alexx

    Прежде всего спасибо за статью.
    Предложение по небольшому её улучшению. Несколько туманно объяснено вот в этой части:
    «Согласно документации фильтр — это код, который может выполняться до и/или после выполнения любого экшена».
    Хотелось бы подробней, фильтр как метод контролера всё таки выполняется до или после?
    Несколько проясняет ситуацию только вот эта фраза в конце:
    «Внешние фильтры, в отличии от встроенных, позволяют выполнять действия как до, так и после выполнения экшена.»
    По инерции пытался найти где-же указывать post/pre выполнение для встроенных фильтров.

  • Нормальная статья только мой совет на будущее не стараться обьяснить столько материала в одной статье. Постарайтесь большие статьи разбивать на более мелкие по отдельным темам, так намного удобнее и для понимания и для продолжения чтения. к примеру удобнее читать серию из трёх маленький статей и дочитав одну из них знать на чем остановился, чем искать в огромной статье участок кода на котором ты закончил читать прошлый раз.

    А вообще всё хорошо, в будущем жду продолжения по статьям 🙂

  • @Alexx
    В ближайшее время обязательно поправлю!

    @Ozzy
    Обязательно учту в следующих постах!

    Спасибо всем за отзывы.

  • xoma

    Отформатировал запись — листинги кода стали более читаемы )

  • mc-bear

    Отличная статья. Спасибо.
    Не останавливайтесь на этом.

  • Объяснил бы кто как сделать заргрузку файлов при регистрации пользователя. не видит файл впиртык.
    если что — в долгу не останусь

  • Опишите проблемку подробнее — все решаемо =)

  • есть форма. в форме текстовые поля и поле для загрузки файла. если для форм прописано enctype=»multipart/form-data» то в базу не передается имя файла и выводится ошибка что поле путое. тут кстати и еще один вопрос)
    собственно если не прописывать правило что все поля обязательны, то в БД они не передаются. а если прописать их обязательность — передаются.

  • а какой смысл передавать в базу то что написано в поле типа «файл» ?? В базе нужно хранить — путь до файла в файловой системе сервера, а для этого нужно обработать загружаемый файл и переместить его в каталог для хранения…
    сделайте print_r($_FILES) и посмотрите — передается ли файл на сервер.

  • в базу передаваться должно название файла.
    а где вызвать print_r ($_FILES)? в контролере или всеравно?

  • Лучше в контроллере — ведь именно там происходит обработка формы…

  • теперь проблема в том что «failed to open stream: No such file or directory». как бы его заставить перенести файл-то?

  • Ну, наверное как-то вот так
    http://www.php.ru/manual/features.file-upload.html

  • ужо разобрался 🙂 абсолютный путь указать надо было 🙂