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

Yiiframework 21 июня 2009 г., 15:34

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

Я не ошибусь если предположу, что большинство современных фреймворков для разработки 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:

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

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:

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

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-кодинга!

Юпи! — CMS на Yii – http://yupe.ru

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

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



Комментарии 6

xoma
xoma
Отредактировал код примеров из статьи — стало лучше!
Bethrezen
Bethrezen
Думаю надо под кат загнать
xoma
xoma
я тебя не понял )))
xoma
xoma
разобрался )))
DARX
DARX
Ну 10-15 миллисекунд — это же мелочи совсем =) Модульный подход очень упрощает жизнь после месяца тестирования приложения.
xoma
xoma
вот и я про тоже говорю!
Пожалуйста, авторизуйтесь или зарегистрируйтесь для комментирования!