Польза и принцип работы Interception Filters

slego

Новичок
Польза и принцип работы Interception Filters

Всем привет.

Все надеялся, что тема Interception Filters (кстати, как все-таки правильней interceptiON
или interceptiNG?) меня обойдет стороной.... не обошла...

Вот. С первого взгляда, вроде как симпатичная и довольно-таки полезная штука.
Но со второго и с третьего возникают сомнения.
Ну, во-первых, варианты фильтров, которые я встречал:
1. ExecutingTimeFilter - время выполнения всего скрипта
2. AuthFilter - фильтр аутентификации
3. OutputBufferingFilter - чувствую, что жутко полезный фильтр,
но смысл никак не могу понять :) (использование функций ob_start,
ob_end_flash и т.д. Возможно, добрые люди объяснят на пальцах для ЧЕГО это надо?
Кеширование, или только для устранения проблем, связанных с преждевременной отсылкой headers?
Только к мануалу не отсылайте, пожалуйста).
4. LoggingFilter
5. DebugFilter
6. и т.д.
n. MainExecutionFilter - Основной фильтр, в котором реализуется логика приложения
(маппинг комманд, форварды, выбор вью и т.д. Много чего, короче).

Далее цепочка фильтров реализуется, как правило, на основе паттерна Decorator
(ну, или какой-нибудь его модификации). Где-то так:
PHP:
$fc = new ExecutionTimeFilter(new DebugFilter(new AuthFilter
    (new SessionFilter(new MainExecutionFilter()));
$fc->process();
Каждый фильтр имеет preProcess и postProcess методы для запуска до и после запуска
следующего фильтра. Ну или не имеет :), а есть просто метод process().

Теперь, собственно, что меня смущает. Весь код - условный.

ПРИМЕР 1. Фильтр времени исполнения
PHP:
$fc = new ExecutionTimeFilter(new MainExecutionFilter());
$fc->process();

class ExecutionTimeFilter extends Filter
{
    public function process()
    {
        $action = new Request::getAction();
        $action->execute();

        $view = new View($action->getData())
        Response::setContent($view->render());
    }
}

class ExecutionTimeFilter extends Filter
{
    public function preProcess()
    {
        $this->_time = microtime();
    }

    public function postProcess()
    {
        return microtime() - $this->_time;
    }

    public function process()
    {
        $this->preProcess();

        $this->next_filter->process();

        $this->postProcess();
    }
}
Т.е. имеем некоторые данные (в частности, время выполнения), которые мы получаем
ПОСЛЕ того, как отработал основной фильтр, реализующий логику приложения, который
уже все, что он должен был, собрал и поместил в Response.

К тому же, мне почему-то казалось, что подобный фильтр нужен не столько для спортивного
интереса, сколько для того, чтобы при достижении максимального времени выполнения,
прерывать скрипт. Это как такое можно реализовать? Нужен, наверное, какой-нибудь
Observer или что там еще.


ПРИМЕР 2. Фильтр аутентификации:
PHP:
$fc = new AuthFilter(new MainExecutionFilter());
$fc->process();

class AuthFilter extends Filter
{
    public function process()
    {
        if ($this->user->isAuth)
                $this->next_filter->process();
        else echo "хацкерам - НАЙН!";
        //else exit();
    }
}
Тут тоже ерунда какая-то получается. Мы даже не доходим до основной логики, а хорошо
было бы посмотреть маппинг-файл, поискать там
действие, которое отвечает за провал фильтра, опять подключить необходимое вью...
Некузяво как-то.

ПРИМЕР 3. Фильтр дебага или логирования.
Наш фильтр, получается, просто служит для "включения" опции, на которую будет ссылаться
остальной код
PHP:
$fc = new DebugFilter(new MainExecutionFilter());
$fc->process();

class DebugFilter extends Filter
{
    public function process()
    {
        Application::setDebugMode("on");

        $this->next_filter->process();
    }
}

class ExecutionTimeFilter extends Filter
{
    public function process()
    {
        //...
        if (Application::getDebugMode == "on")
        {
            echo Request::getAction();
        }
        //...
    }
}
С одной стороны, красиво конечно, но стоит ли на включение одной опции
создавать целый объект?


Итак, как прокомментирует все это достопочтеннейшая публика?
В чем я заблуждаюсь и что пытаюсь делать категорически неправильно?
И стоит ли вообще таким заморачиваться?
На sitepont.com встречал мнение, что Interception Filters - это АНТИпаттерн для
php. Что он ориентирован на приложения, которые резидентно висят в памяти.
Загрузил все фильтры в самом начале работы приложения и все.
В случае php эти фильтры загружаются каждый раз... хотя тут еще много чего
каждый раз загружается :)

В общем, any critics, suggests and propositions are welcome.

Спасибо всем, кто таки осилил. :)
 

WMix

герр M:)ller
Партнер клуба
использование функций ob_start,
ob_end_flash

твой ответ вероятно правильный
Кеширование, и/или только для устранения проблем, связанных с преждевременной отсылкой headers?

после ob_start() пхп собирает все данные вывода отправленные функциями print echo итд...

те дойдя до конца даже самого кривого (рабочего) скрипта, на экран не выйдет ничего, можно отправить и хидер, и посадить куки и тд...
и уже в нужном месте вывети то что насобиралось, с помощью ob_end_flash()

в остальном (в обшем в важном) я несилен пока, написанное показалось мне ужасно интересным. сейчас почитаю что книги пишут об этом...
 

slego

Новичок
Ну наконец-то! Хоть кто-то ответил.
С ob_.... - стало понятней, спасибо.

Мне тоже, к сожалению, показалось интересным.. теперь мучаюсь и плохо сплю :)
 

syfisher

TDD infected!!
Intercepting Filters:

1) Используются для того, чтобы производить предварительную или последующую обработку запроса пользователя
2) Фильтры не должны ничего знать о доменных области приложения. В идеале подключение/отключение фильтров не должно сказываться на работоспособности приложения.
3) Фильтры совершенно не обязательно реализовывать через декоратор. Вполне подходит такой подход:

Тест на цепочку:
PHP:
require_once(dirname(__FILE__) . '/InterceptingFilter.interface.php');
require_once(dirname(__FILE__) . '/FilterChain.class.php');
 
Mock :: generate('InterceptingFilter');
 
class PositiveTestingFilter extends MockInterceptingFilter{
   public function run($filters_chain){
     parent :: run($filters_chain);
 
     $filters_chain->next();
   }
}
 
function NegativeTestingFilter extends MockInterceptingFilter{}
 
class FilterChainTest extends UnitTestCase
{
  function testRun()
  {
    $filter1 = new PositiveTestingFilter($this);
    $filter2 = new NegativeTestingFilter($this);
    $filter3 = new PositiveTestingFilter($this);
 
    $chain = new FiltersChain();
    $chain->registerFilter($filter1);
    $chain->registerFilter($filter2);
    $chain->registerFilter($filter3);
 
    $filter1->expectOnce('run', array($chain));
    $filter2->expectOnce('run', array($chain));
    $filter3->expectNever('run', array($chain));
 
    $chain->run();
 
    $filter1->tally();
    $filter2->tally();
    $filter3->tally();
  }
}
Код цепочки:
PHP:
class FiltersChain
{
  protected $filters = array();
  protected $counter = -1;
 
  public function registerFilter($filter){
    $this->filters[] = $filter;
  }
 
  public function next(){
    $this->counter++;
     if(isset($this->filters[$this->counter])){
      $this->filters[$this->counter]->run($this);
    }
  }
 
  public function run(){
    $this->counter = -1;
    $this->next();
  }
}
Пример фильтра:

<?php
PHP:
require_once(LIMB_DIR . '/src/filters/InterceptingFilter.interface.php');

class TimingFilter implements InterceptingFilter
{
  public function run($filter_chain)
  {
    $start_time = $this->getMicroTime();

    $filter_chain->next();

    $response = Context :: instance()->getResponse();
    $response->append('<small>' . round($this->getMicroTime() - $start_time, 2) . '</small>');
  }

  function getMicroTime()
  {
    list($usec, $sec) = explode(" ", microtime());
    return ((float)$usec + (float)$sec);
  }
}

?>
В данном случае цепочка будет представлять из себя матрешку, где каждый фильтр решает, прерывать цепочку или нет.

4) Хорошим дизайнерским решением является возможность использовать различные цепочки фильтров для различных типов запросов. Например, запрос для страницы центра администрирования должен включать AuthenticationFilter, а запрос для фронтальной страницы - уже нет.

5) Если использовать какую-либо реализацию хендла, то можно экономить на парсинке кода. Это очень полезно, когда, например, кеширующий фильтр прерывает цепочку. То цепочка формируется так:

PHP:
    $filter_chain = new FilterChain();
    $filter_chain->registerFilter(new Handle(LIMB_DIR . '/src/filters/ResponseProcessingFilter'));
    $filter_chain->registerFilter(new Handle(LIMB_DIR . '/src/filters/TimingFilter'));
[...]
Соответственно резолв хендла фильтра производится только тогда, когда до фильтра дошла очередь.

6) Нам фильтры очень нравятся :)
 

slego

Новичок
1) согласен
2) не совсем понятно - пример с DebugFilter - это есть ЗЛО, фильтр знает что-то о доменных объектах или нет? Т.е. это пример как НЕ должно быть?
3) спорить не буду, потому что сам через декоратор не планирую использовать. А именно так, как ты реализуешь, т.к. собираюсь список фильтров считывать с конфиг-файла.
Пример фильтра:
<?php
PHP:
class TimingFilter implements InterceptingFilter
{
  public function run($filter_chain)
  {
    $start_time = $this->getMicroTime();

    $filter_chain->next();

    $response = Context :: instance()->getResponse();
    $response->append('<small>' . round($this->getMicroTime() - $start_time, 2) . '</small>');
  }
Вот момент с контекстом, с передачей из ФИЛЬТРА чего-то, ну я не знаю, наверх что ли, мне, честно говоря, не очень нравится... как-то я внутренне против этого.
В данном случае цепочка будет представлять из себя матрешку, где каждый фильтр решает, прерывать цепочку или нет.
А что происходит, если цепочка прерывается? Происходит целый комплект MVC c выбором комманды, отвечающей за прекращение работы фильтра и соответствующих вьюшек?

4) Хорошим дизайнерским решением является возможность использовать различные цепочки фильтров для различных типов запросов. Например, запрос для страницы центра администрирования должен включать AuthenticationFilter, а запрос для фронтальной страницы - уже нет.
Тоже думал о подобной структуре
PHP:
<action name="login">
    <filters>
         <filter name="AuthFilter"/>
         <filter name="LogFilter"/>
         ...
    </filters>
</action>
5) что-то пока для меня это сложно :(
6)
Нам фильтры очень нравятся :)
Мне тоже, правда еще не знаю почему :)
 

syfisher

TDD infected!!
Автор оригинала: slego
2) не совсем понятно - пример с DebugFilter - это есть ЗЛО, фильтр знает что-то о доменных объектах или нет? Т.е. это пример как НЕ должно быть?
Фильтр ничего о доменных объектах знать не должен - это правило. Об остальных частях контроллера - по желанию. В твоем случае - ничего страшного нет.

Автор оригинала: slego
Вот момент с контекстом, с передачей из ФИЛЬТРА чего-то, ну я не знаю, наверх что ли, мне, честно говоря, не очень нравится... как-то я внутренне против этого.
Это каждый для себя решает. Иногда без этого не обойтись. Я так и не придумал, как можно сделать проверку прав доступа и не передать что-либо наверх (другим фильтрам).

Автор оригинала: slego
А что происходит, если цепочка прерывается? Происходит целый комплект MVC c выбором комманды, отвечающей за прекращение работы фильтра и соответствующих вьюшек?
Нет. У нас контроль за моделью производится в одном из самых нижних уровней. Если цепочка прерывается, то дальше ничего не исполняется.

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

И еще цепочка не прерывает фатально. Лишь не передается управление следующему фильтру. То есть фильтр может содержать такую логику:

PHP:
$this->_doSomePreprocessing();
if($this->_someCondition())
  $chain->next();
$this->_doSomePostprocessing();
То есть после того, как цепочка "упрется в тупик" котроль все равно вернется в данный фильтр. Конечно, если не будет исключительной ситуации.

Автор оригинала: slego

5) что-то пока для меня это сложно :(
Посмотри WACT или Limb (если php5). Думаю, что быстро разберешься
 

slego

Новичок
Спасибо за столь подробрый ответ. Но, честно говоря, момент с прерыванием/разрывом цепочки и т.д для меня по-прежнему остается туманным :(
Посмотри WACT или Limb (если php5). Думаю, что быстро разберешься
Да, собственно, только после просмотра Limb меня и зацепили interception filters эти :)

Автор оригинала: syfisher
Вообще цепочка прерывается достаточно редко. На ум пока приходит только кеширование. Система прав только говорит нижнему фильтру выполнить команду, которая отобразить соответсвующий шаблон.
У вас работа с выводом вью, с установкой шаблонов, передачей в них параметров происходит ПОСЛЕ цепочки фильтров или все таки где-то ВНУТРИ цепочки?

Может быть, если не сложно, пример какой-нибудь законченный привести сможешь?

PHP:
class AuthFilter
{
    //....
    public function process($filter_chain)
    {
          if (this->user->ifAuthed())
          {
                 $filter_chain->processNext();
          }
          else
          {
Нет. У нас контроль за моделью производится в одном из самых нижних уровней. Если цепочка прерывается, то дальше ничего не исполняется.
И че тут - просто банальный exit() без объявления войны?

Или же происходит что-то подобное (я опять декоратором воспользуюсь, ок? уж больно он компактен и нагляден):

PHP:
$request = Request::getInstance();
$response = Response::getInstance();

$fc = new DebugFilter(new AuthFilter(new MainFilter($request, $response)));
//  внутри цепочки в $response сунется все, что
//  только возможно и нужно
$view = new View($response);
$view->render();
-~{}~ 30.08.05 11:45:

И в догонку еще вопрос. Так, а принцип работы с фильтрами Логирования и Дебага я правильно понял? Т.е. они, включают некий флаг, который потом проверяет по мере необходимости остальные модули?
 

syfisher

TDD infected!!
Автор оригинала: slego
У вас работа с выводом вью, с установкой шаблонов, передачей в них параметров происходит ПОСЛЕ цепочки фильтров или все таки где-то ВНУТРИ цепочки?
Внутри. Называется CommandProcessingFilter.

Может быть, если не сложно, пример какой-нибудь законченный привести сможешь?
Лучше посмотри в исходниках. Если не горит, дождись следующего релиза. Там концепция использования фильтров немного изменилась, в лучшую сторону конечно.

Еще посмотри эту страницу:
http://limb-project.com/wiki/doku.php?id=limb:ru:3_x:architecture:filter

Здесь все очень подробно описано, UML и примеры кода.

PHP:
class AuthFilter
{
    //....
    public function process($filter_chain)
    {
          if (this->user->ifAuthed())
          {
                 $filter_chain->processNext();
          }
          else
          {
И че тут - просто банальный exit() без объявления войны?
Нет, просто фильтр не передает управление дальше. Вот пример фильтра подсистемы кеширования (да простят мне большое количество кода :) ):
PHP:
class FullPageCacheFilter implements InterceptingFilter{
  public function run($filter_chain) {
    $ini_loader = new FullPageCacheIniSpecsLoader('full_page_cache.ini');
    $specs = new SpecificationList();
    $ini_loader->register($specs);

    $caching_policy = new FileBasedCacheWriter();
    $caching_policy->setStorageDirectory(VAR_DIR . '/full_page_cache/');

    $cache = new FullPageCache($caching_policy, $specs);

    $toolkit = LimbBaseToolkit :: instance();
    $request = $toolkit->getRequest();
    $uri = $request->getUri();
    $user = $toolkit->getUser();

    $cache_request = new FullPageCacheRequest($uri, $user);
    if(!$cache->openSession($cache_request))    {  
    $filter_chain->next();
      return;
    }

    $response = $toolkit->getResponse();
    if($content = $cache->get())  {
      $response->write($content);
    }
    else  {
      $filter_chain->next();
      $content = $response->getResponseString();
      $cache->save($content);
    }
  }
}
Если страницу закешировать нальзя, то управление сразу передается в следующий фильтр и управление в данный фильтр более не возвращается. Если закешировать можно, то проверяется, существует ли кеш. Если нет, то цепочка не прерывается, и вызывается следующий фильтр. Если кеш есть, то в Response кладется закешированный контент и управление следующему(считай в глубь) не передается.

Вот, например, наш вариант AuthFilter:

PHP:
class SimpleACLAccessFilter implements InterceptingFilter{
  function run($filter_chain) {
    $toolkit = LimbBaseToolkit :: instance();

    $dispatched = $toolkit->getDispatchedRequest();
    $service = $dispatched->getService();
    $service_name = $service->getName();
    $action = $dispatched->getAction();

    $request = $toolkit->getRequest();
    $uri = $request->getUri();

    $path = $uri->getPath();

    $acl_toolkit = SimpleACLBaseToolkit :: instance();
    $authorizer = $acl_toolkit->getAuthorizer();
    if($authorizer->canDo($action, $path, $service_name))  {
      $filter_chain->next();
      return;
    }

    $service403 = $toolkit->createService('403');
    $new_dispatched = new DispatchedRequest($service403, $service403->getDefaultAction());
    $toolkit->setDispatchedRequest($new_dispatched);
    $filter_chain->next();
  }
}
Кратко, мы просто заменяем так называемый DispatchedRequest - объект, хранящий информацию, что же должна сделать система.

В результате CommandProcessingFilter, который пользуется DispatchedRequest, чтобы узнать, какую команду выполнить, отобразит страницу с сообщением, что данный адрес не доступен.

И в догонку еще вопрос. Так, а принцип работы с фильтрами Логирования и Дебага я правильно понял? Т.е. они, включают некий флаг, который потом проверяет по мере необходимости остальные модули?
В Limb вообще нет концепции дебага. У нас есть для этого тесты :). Насчет логирования - другие подсистемы ничего про лог не знают.

LogginFilter схематично выглядит так:

PHP:
$chain->next();
 $this->_doSomeLoggingHere();
 

slego

Новичок
Кратко, мы просто заменяем так называемый DispatchedRequest - объект, хранящий информацию, что же должна сделать система.
В результате CommandProcessingFilter, который пользуется DispatchedRequest, чтобы узнать, какую команду выполнить, отобразит страницу с сообщением, что данный адрес не доступен.
т.е., если я правильно понял, то в самом упрощенном варианте имеем следующую схему:
У нас поступает запрос index.php?action=doSomth,
а в фильтре, если проверка не срабатывает, то мы просто этот action меняем на action=showErrorPage, к примеру?

Насчет логирования - другие подсистемы ничего про лог не знают.

LogginFilter схематично выглядит так:

PHP:
$chain->next();
 $this->_doSomeLoggingHere();
мммм.... Хорошо. Берем ваш CommandProcessingFilters - он ведь выполняется самым последним в цепочке фильтров? Но, даже это не так важно. Важно следующее: CommandProccessingFilters - суть, не слабый такой кусок кода, с кучей commands всяких, так ведь? Т.е там может быть под несколько десятков классов? Если мы сделаем так
PHP:
$fc = new LoggingFilter(new CommandProccessingFilter(...))
class LoggingFilter
{
  $chain->next();  // CommandProccesingFilter
  $this->_doSomeLoggingHere();
}
то что мы сможем занести в лог (кроме, например, "фильтр CommandProccessFilter отработал успешно"), если CommandProccesingFilter и все внутренние классы не будут о нем ничего знать?

Just in case, я не придираюсь, я просто хочу понять, в правильном ли я направлении размышляю :)
 

syfisher

TDD infected!!
CommandProcessingFilters не обязательно стоит самым последним в цепочке.

Как я уже говорил, тот лог, который обычно используется для дебага, у нас не используется. Я пока не встречал ситуации, когда нам это было нужно. Статистике достаточно тех данных, которые есть. То, что имеет место в CommandProcessingFilters, является частью доменной логики, поэтому я не вижу смысла лезть внутрь доменной логики с логами. Конечно, ситуации бывают разными. Но мы пока этого избегаем как можем.

Но ты все правильно понимаешь, так держать! И про логи, и про action... ;)
 

slego

Новичок
Дело ясное, что дело темное. :)
Огромное спасибо за уделенное внимание и за столь подробное разжевывание. Буду пытаться что-то писать. Если возникнут проблемы (а они возникнут), можно в приват попишу немного? ;)
 

syfisher

TDD infected!!
Пиши в форум Limb-а или сюда. Может еще кому пригодится или еще кто ответит.
 

slego

Новичок
ок

-~{}~ 30.08.05 17:31:

Все равно не могу понять одного.
Пример с TimingFilter - это фильтр для вычисления работы скрипта, ведь так?
Вопрос №1.
Если нужно знать время работы скрипта, то этот фильтр должен быть самым верхним, т.е. стартовать раньше всех и завершать свою работу самым последним. Во всяком случае, я именно так вижу логику работы этого фильтра.

А если он стартовал раньше всех, внутри отработали все дочерние фильтры, произошел response->commit(), то как вытащить время выполнения?

Еще раз посмотрел Limb. У вас там есть фильтр, называется ResponseProcessingFilter, в котором, собственно происходит вывод результата на экран, насколько я понял. Можно конечно сделать что-то типа
PHP:
$filter_chain = new ResponseProcessingFilter(new TimingFilter(new CommandProcessingFilter(...)))
тогда моя "проблема" исчезает, но возникает другая - TimingFilter будет безжалостно врать :)

Вопрос №2.
Я его уже задавал, правда он остался проигнорированным. Не думали ли вы о создании TiminigFilter, который именно бы следил за МАКСИМАЛЬНО ВОЗМОЖНЫМ временем работы. Т.е. задал в конфигах, что это запрос должен работать максимум 20 сек, если больше - значит, что-то не так, например, что-то с удаленным сервером => к следующему фильтру не переходим, а выводим страницу с ошибкой времени работы.

Как?
 

slego

Новичок
И что? Тупое прерывание скрипта?

Нужно "красиво" отработать такую ситуацию - переключить action=doSmth c запрашиваемого на, например, action=errorTimeExecution - собрать стандартный вид страницы, подключить необходимые вьюшки.
 

syfisher

TDD infected!!
Автор оригинала: slego
тогда моя "проблема" исчезает, но возникает другая - TimingFilter будет безжалостно врать :)
Так и есть. Врет нещадно :)

Автор оригинала: slego
Вопрос №2.
Я его уже задавал, правда он остался проигнорированным. Не думали ли вы о создании TiminigFilter, который именно бы следил за МАКСИМАЛЬНО ВОЗМОЖНЫМ временем работы. Т.е. задал в конфигах, что это запрос должен работать максимум 20 сек, если больше - значит, что-то не так, например, что-то с удаленным сервером => к следующему фильтру не переходим, а выводим страницу с ошибкой времени работы.
Как?
YAGNI, YAGNI - пока не было необходимости. Не забывай, что Limb - это только alpha. Реально на базе тройки еще не сделано ни одного серьезного проекта. Поэтому все может поменяться.

Как говорится, счастливо поэкспериментировать.
 

BeGe

Вождь Апачей, блин (c)
Автор оригинала: BeGe
set_timelimit(20)
А если ты повиснешь на текущей операции, ты не сможешь определить время... что прошло 20 секунд... у тебя должен быть внешний счётчик для реалзации отчёта времени выполнения программы.
 

slego

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

BeGe

Вождь Апачей, блин (c)
не существует. Внешний - это не пхп, это уже сама операционная система.
 
Сверху