Статьи

Шаблоны для гибкой обработки представлений, часть 1 — работа с композитами

Я бы осмелился сказать, что взгляды во многих формах, которые они могут принимать, являются недооцененными сущностями в поисках истинной идентичности. С самого первого момента, когда MVC начал проникать в сообщество PHP, «V» акронима была несправедливо уменьшена до уровня простого шаблона, ответственность которого ограничена отображением нескольких HTML-страниц.

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

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

В некотором смысле, это может быть. Как обычно, казалось бы, сложные проблемы можно решить, обратившись к более простым подходам. Более того, поскольку во многих случаях представления концептуально моделируются как скелетные объекты PHP, совместно использующие некоторый общий интерфейс рендеринга (таким образом, учитывающий полиморфизм), относительно просто обрабатывать их как по отдельности, так и совместно через узды унифицированного API.

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

Звучит как приманка для ботаников? Ну, это не так. Чтобы преодолеть ваш скептицизм, в этом уроке, состоящем из двух частей, я покажу вам, как реализовать с нуля пару настраиваемых модулей обработки представлений, погрузившись в лакомство шаблонов Composite и Decorator.

Первый шаг — реализация базового модуля представления

Включенный в классический репертуар GoF, шаблон Composite является одним из самых ярких примеров старой мантры «Favor Composition over Inheritance» в действии. Проще говоря, силы шаблона позволяют вам управлять одним и несколькими экземплярами данного компонента, используя один и тот же API. Эта способность особенно привлекательна для обхода древовидных структур, состоящих из объектов листьев и ветвей (да, объектов, снабженных другими объектами) без необходимости загромождать ваш код условными выражениями, поэтому развертывается элегантное решение, которое опирается на столпы полиморфизма.

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

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

<?php namespace LibraryView; interface ViewInterface { public function setTemplate($template); public function getTemplate(); public function __set($field, $value); public function __get($field); public function __isset($field); public function __unset($field); public function render(); } 
 <?php namespace LibraryView; class View implements ViewInterface { const DEFAULT_TEMPLATE = "default.php"; protected $template = self::DEFAULT_TEMPLATE; protected $fields = array(); public function __construct($template = null, array $fields = array()) { if ($template !== null) { $this->setTemplate($template); } if (!empty($fields)) { foreach ($fields as $name => $value) { $this->$name = $value; } } } public function setTemplate($template) { $template = $template . ".php"; if (!is_file($template) || !is_readable($template)) { throw new InvalidArgumentException( "The template '$template' is invalid."); } $this->template = $template; return $this; } public function getTemplate() { return $this->template; } public function __set($name, $value) { $this->fields[$name] = $value; return $this; } public function __get($name) { if (!isset($this->fields[$name])) { throw new InvalidArgumentException( "Unable to get the field '$field'."); } $field = $this->fields[$name]; return $field instanceof Closure ? $field($this) : $field; } public function __isset($name) { return isset($this->fields[$name]); } public function __unset($name) { if (!isset($this->fields[$name])) { throw new InvalidArgumentException( "Unable to unset the field '$field'."); } unset($this->fields[$name]); return $this; } public function render() { extract($this->fields); ob_start(); include $this->template; return ob_get_clean(); } } с <?php namespace LibraryView; class View implements ViewInterface { const DEFAULT_TEMPLATE = "default.php"; protected $template = self::DEFAULT_TEMPLATE; protected $fields = array(); public function __construct($template = null, array $fields = array()) { if ($template !== null) { $this->setTemplate($template); } if (!empty($fields)) { foreach ($fields as $name => $value) { $this->$name = $value; } } } public function setTemplate($template) { $template = $template . ".php"; if (!is_file($template) || !is_readable($template)) { throw new InvalidArgumentException( "The template '$template' is invalid."); } $this->template = $template; return $this; } public function getTemplate() { return $this->template; } public function __set($name, $value) { $this->fields[$name] = $value; return $this; } public function __get($name) { if (!isset($this->fields[$name])) { throw new InvalidArgumentException( "Unable to get the field '$field'."); } $field = $this->fields[$name]; return $field instanceof Closure ? $field($this) : $field; } public function __isset($name) { return isset($this->fields[$name]); } public function __unset($name) { if (!isset($this->fields[$name])) { throw new InvalidArgumentException( "Unable to unset the field '$field'."); } unset($this->fields[$name]); return $this; } public function render() { extract($this->fields); ob_start(); include $this->template; return ob_get_clean(); } } 

Как отмечалось ранее, взгляды являются привлекательными кандидатами, которые усердно молятся за то, чтобы их смоделировали как ПОПО, и, безусловно, этот класс View соблюдает эту концепцию. Его функциональность сводится к тому, чтобы просто использовать некое волшебство PHP за кулисами, чтобы назначать и удалять поля из представления, которые, в свою очередь, анализируются его методом render() .

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

 <!doctype html> <head> <meta charset="utf-8"> <title>The Default Template</title> </head> <body> <header> <h1>You are viewing the default page!</h1> <?php echo $this->header;?> </header> <section> <?php echo $this->body;?> </section> <footer> <?php echo $this->footer;?> </footer> </body> </html> 

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

 <?php use LibraryLoaderAutoloader, LibraryViewView; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $view = new View(); $view->header = "This is my fancy header section"; $view->body = "This is my fancy body section"; $view->footer = "This is my fancy footer section"; echo $view->render(); 

Совсем неплохо, верно? И хотя я должен признать, что представление реализует немного больше волшебства, чем то, с чем мне обычно удобно, оно работает довольно прилично, когда дело доходит до создания визуализируемых объектов представления. Более того, учитывая, что класс способен анализировать замыкания, добавленные в шаблон, мы могли бы быть немного смелее и использовать его для создания двухэтапных представлений. Для этого нам нужно создать хотя бы один минималистичный частичный файл с partial.php или что-то в этом роде, похожее на этот:

 <p><?php echo $this->content;?></p> 

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

 <?php $view = new View(); $view->header = function () { $header = new View("partial"); $header->content = "This is my fancy header section"; return $header->render(); }; $view->body = function () { $body = new View("partial"); $body->content = "This is my fancy body section"; return $body->render(); }; $view->footer = function () { $footer = new View("partial"); $footer->content = "This is my fancy footer section"; return $footer->render(); }; echo $view->render(); 

Нам удалось реализовать довольно гибкий модуль представления, который можно использовать для рендеринга изолированных объектов и даже фиктивных частичек. Однако, несмотря на все эти достоинства, модуль все еще не работает, когда дело доходит до рендеринга деревьев представлений. Это может быть сделано с помощью вложенных замыканий, что, по крайней мере, на мой вкус, будет довольно небрежным решением. Как насчет обращения к шаблону Composite? В конце концов, он предоставляет эту функциональность прямо из коробки, сохраняя при этом суть ООП.

Построение составного вида — листья и ветви как равные

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

 <?php namespace LibraryView; interface TemplateInterface { public function setTemplate($template); public function getTemplate(); } 
 <?php namespace LibraryView; interface ContainerInterface { public function __set($field, $value); public function __get($field); public function __isset($field); public function __unset($field); } 
 <?php namespace LibraryView; interface ViewInterface { public function render(); } 

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

 <?php class View implements TemplateInterface, ContainerInterface, ViewInterface { // the same implementation goes here } 

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

 <?php namespace LibraryView; class CompositeView implements ViewInterface { protected $views = array(); public function attachView(ViewInterface $view) { if (!in_array($view, $this->views, true)) { $this->views[] = $view; } return $this; } public function detachView(ViewInterface $view) { $this->views = array_filter($this->views, function ($value) use ($view) { return $value !== $view; }); return $this; } public function render() { $output = ""; foreach ($this->views as $view) { $output .= $view->render(); } return $output; } } 

Класс CompositeView — это не что иное, как контейнер простого представления, замаскированный под причудливым именем, хотя его attachView() / detachView() позволяет нам добавлять и удалять представления по желанию, включая другие составные представления. Эта изящная рекурсивная способность, часто присутствующая в типичных реализациях шаблона Composite, дает большую гибкость бесплатно. Например, рассмотрим общий сценарий, когда HTML-страница состоит из классического верхнего и нижнего колонтитула. Чтобы сделать пример еще проще, допустим, к разделам привязаны footer.php шаблоны header.php , body.php и footer.php :

 <!doctype html> <head> <meta charset="utf-8"> <title>A Sample Template</title> </head> <body> <header> <h1>Welcome to this page, which you're accessing from <?php echo $this->ip;?></h1> <p><?php echo $this->content;?></p> </header> 
 <section> <p><?php echo $this->content;?></p> </section> 
 <footer> <p><?php echo $this->content;?></p> </footer> </body> </html> 

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

 <?php use LibraryLoaderAutoloader, LibraryViewView, LibraryViewCompositeView; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $header = new View("header"); $header->content = "This is my fancy header section"; $header->ip = function () { return $_SERVER["REMOTE_ADDR"]; }; $body = new View("body"); $body->content = "This is my fancy body section"; $footer = new View("footer"); $footer->content = "This is my fancy footer section"; $compositeView = new CompositeView; echo $compositeView->attachView($header) ->attachView($body) ->attachView($footer) ->render(); 

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

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

Заключительные замечания

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

Это не означает, что нужно строго придерживаться достоинств шаблона всякий раз, когда необходимо развернуть где-нибудь механизм обработки представления, поскольку догматичность довольно бессмысленна. Более того, если Composites просто не соответствуют вашему вкусу, возможно пойти по альтернативному пути и реализовать гибкий модуль представления, обращаясь к функциональности, предоставляемой Decorators. В дальнейшем я расскажу о внутренних принципах именно этого подхода, так что не пропустите!

Изображение через Fotolia