System szablonów PHPTAL jako widok w Zend Framework
Artykuł dodany: 28 maja 2009. Ostatnia modyfikacja: 05 listopada 2016.
Stopień trudności (1 - dla początkujących, 5 - dla ekspertów): 4
PHPTAL dla Zend Framework 3 i Zend Expressive
Poniższy artykuł opisuje kod dla frameworka Zenda w wersji 1. Przygotowałem dwa repozytoria które obsługują najnowsze projekty spod stajni Zenda.
- Zend Framework 3
- Zend Expressive
Wprowadzenie
PHPTAL to system szablonów wzorowany na pythonowym ZPT, w całości napisany w PHP5, znacznie lżejszy i wydajniejszy od innych projektów takich jak Smarty czy Open Power Template. Jego największą zaletą jest to, iż generuje poprawnie sformatowany (“well-formed”) dokument a w przypadku popełnienia przez nas błędu (złe zamknięcie tagów) wyświetla stosowną informację. Dodatkowo automatycznie zabezpiecza nas przed atakami XSS. Od pewnego czasu pieczę nad projektem sprawuje Polak, Kornel Lesiński, znany w sieci jako porneL tak więc możemy również liczyć na jego polskie wsparcie. Widać też wyraźny rozwój całego projektu który z każdym wydaniem jest jeszcze lepszy. Zend Framework zawiera w sobie komponent Zend_View jednak jego stosowanie jest średnio przyjemne ale również mało czytelne. W artykule pokażę jak zastąpić domyślny widok PHPTALem. Można również skorzystać z gotowego repozytorium zawierającego wszystkie niezbędne do pracy komponenty.
Dla potrzeb artykułu załóżmy konwencjonalną modułową strukturę katalogową.
/application /config (opcjonalnie) /app.config.ini #plik konfiguracji /(module 1) #rzeczywistą nazwą niech będzie `default` /config (opcjonalnie) /controllers /models /views /filters /helpers /scripts #tutaj umieszczamy pliki źródłowe, domyślnie z rozszerzeniem .xhtml /user /login.xhtml /listusers.xml /site.xhtml /bootstrap.php #plik odpowiedzialny za startowanie aplikacji /htdocs (dostępne z poziomu www) /images /scripts /styles index.php /library /Zend /Phptal #do katalogu wgrywamy zawartość rozpakowanego archiwum/svn PHPTALa /PHPTAL /PHPTAL.php /My /Controller /Plugin /SetPhptalView.php #wczytanie PHPTALa jako plugin kontrolera /View Phptal.php /Browser.php #opis w tekście /tmp /phptal_compiled #pliki przetworzone przez PHPTAL
Dla ułatwienia pliki źródłowe szablonów umieszczamy w katalogu sugerowanym również przez Zend_View. Biblioteki PHPTAL przegrywamy do katalogu /library/Phptal (struktura taka dla zachowania przejrzystości bibliotek). Z poziomu przeglądarki dostępny jest wyłącznie folder /htdocs, reszta poza ogólnie dostępną strukturą katalogową. /My/View/Phptal.php to główny plik odpowiedzialny za połączenie ZF z PHPTALem. Poniżej jego zawartość.
My/View/Phptal.php
<?php /** * PHPTAL as view in Zend Framework * Extends Zend_View_Abstract * NOTE: this class is supposed to work with Autoloader * Original concept by Matthew Ratzloff * http://framework.zend.com/wiki/display/ZFPROP/Zend_View_PhpTal+-+Matthew+Ratzloff * * @category My * @package My_View * @license http://framework.zend.com/license/new-bsd New BSD License */ class My_View_Phptal extends Zend_View_Abstract { /** * PHPTAL engine * * @var PHPTAL */ private $_engine = null; /** * Ignore HTML/XHTML comments on parsing * * @var bool */ private $_stripComments = false; /** * Config options * * @var array */ private $_configDirectives = array( 'compiledDir' => '../tmp/phptal_compiled/', 'compiledFilesExtension' => 'php', 'charset' => 'utf-8', 'gzipCompression' => false, 'gzipCompressionLevel' => 7 ); /** * Context variables * * @var array */ private $_variables = array(); /**#@+ * MIME type constants * Default: text/plain * 1 - Automatic selection: application/xhtml+xml, text/html * 2 - application/xhtml+xml * 3 - text/html * 4 - text/xml */ const MIME_XHTML_AUTO = 1; const MIME_XHTML = 2; const MIME_HTML = 3; const MIME_XML = 4; /**#@-*/ /** * Constructor * * @param array $config */ public function __construct(array $config = array()) { ini_set('include_path', ini_get('include_path').PATH_SEPARATOR.'../library/Phptal/'); $this->_engine = new PHPTAL(); $this->_loadConfig($config); $this->_setCompiledDir(); $this->setEncoding(); $this->_engine->setPhpCodeExtension($this->_configDirectives['compiledFilesExtension']); $this->_engine->set('helper', $this); parent::__construct(); } /** * Return the template engine object * * @return PHPTAL */ public function getEngine() { return $this->_engine; } /** * Set template variables * * @param string $key * @param mixed $value */ public function __set($key, $value) { $this->assign($key, $value); } /** * Get template variable * * @param string $key * @return null|mixed */ public function __get($key) { if ($this->__isset($key)) { return $this->_variables[$key]; } return null; } /** * Check if template variable is set * * @param string $key * @return bool */ public function __isset($key) { return array_key_exists($key, $this->_variables) and ($key[0] != '_'); } /** * Unset template variable * * @param string $key */ public function __unset($key) { if ($this->__isset($key)) { unset($this->_variables[$key]); } } /** * Clone engine object */ public function __clone() { $this->_engine = clone $this->_engine; } /** * Assigns variables to the view script via differing strategies. * * Zend_View::assign('name', $value) assigns a variable called 'name' * with the corresponding $value. * * Zend_View::assign($array) assigns the array keys as variable * names (with the corresponding array values). * * @see __set() * @param string|array The assignment strategy to use. * @param mixed (Optional) If assigning a named variable, use this * as the value. * @return My_View_Phptal Fluent interface * @throws Zend_View_Exception if $spec is neither a string nor an array, * or if an attempt to set a private or protected member is detected */ public function assign($spec, $value = null) { if (is_string($spec)) { if ('_' == substr($spec, 0, 1)) { throw new Zend_View_Exception('Setting private or protected class members is not allowed', $this); } $this->_variables[$spec] = $value; } elseif (is_array($spec)) { $error = false; foreach ($spec as $key => $val) { if ('_' == substr($key, 0, 1)) { $error = true; break; } $this->_variables[$key] = $val; } if ($error) { throw new Zend_View_Exception('Setting private or protected class members is not allowed', $this); } } else { throw new Zend_View_Exception('assign() expects a string or array, received ' . gettype($spec), $this); } return $this; } /** * Get all context variables * * @return array */ public function getVars() { return $this->_variables; } /** * Clear all context variables */ public function clearVars() { $this->_variables = array(); } /** * Set Content-Type header * * @param int $mimeType * @param string $encoding * @return My_View_Phptal */ public function setContentType($mimeType = null, $encoding = null){ if (null !== $encoding) { $this->setEncoding($encoding); } $encoding = $this->getEncoding(); $frontController = Zend_Controller_Front::getInstance(); if (Zend_Registry::isRegistered('browser')) { $browser = Zend_Registry::get('browser'); } else { $browser = new My_Browser(); Zend_Registry::set('browser', $browser); } switch ($mimeType) { case 1 : if ($browser->detectXhtmlMime()) { $contentType = 'application/xhtml+xml'; } else { $contentType = 'text/html'; } break; case 2 : $contentType = 'application/xhtml+xml'; break; case 3 : $contentType = 'text/html'; break; case 4 : $contentType = 'text/xml'; break; default: $contentType = 'text/plain'; break; } $frontController->getResponse()->setHeader('Content-Type', ''.$contentType.'; charset='.$encoding, true); return $this; } /** * Returns character encoding for output * * @return string Character encoding */ public function getEncoding() { return $this->_engine->getEncoding(); } /** * Set character encoding for output. * Must be executed before @see setContentType() * * @param string $encoding Character encoding * @return My_View_Phptal */ public function setEncoding($encoding = null) { $encoding = (is_null($encoding)) ? $this->_configDirectives['charset'] : $encoding; $this->_engine->setEncoding($encoding); return $this; } /** * Get output mode (XHTML or XML) * * @return int Constant value of PHPTAL_XHTML or PHPTAL_XML */ public function getOutputMode() { return $this->_engine->getOutputMode(); } /** * Set output mode (XHTML or XML) * * @param int $mode PHPTAL_XHTML or PHPTAL_XML * @return My_View_Phptal */ public function setOutputMode($mode) { $this->_engine->setOutputMode($mode); return $this; } /** * Get whether to ignore HTML comments when parsing * * @return bool */ public function getStripComments() { return $this->_stripComments; } /** * Ignore XML/XHTML comments on parsing * * @param bool $bool * @return My_View_Phptal */ public function setStripComments($bool = true) { $this->_engine->stripComments($bool); $this->_stripComments = $bool; return $this; } /** * Flags whether to ignore intermediate php files and to * reparse templates every time (if set to true). * Don't use in production - this makes PHPTAL significantly slower. * * @param bool $bool Forced reparse state. * @return My_View_Phptal */ public function setForceReparse($bool) { $this->_engine->setForceReparse($bool); return $this; } /** * Get whether to force a reparse or not * * @return bool */ public function getForceReparse() { return $this->_engine->getForceReparse(); } /** * Get the path to the PHP generated file * * @return string PHP generated file path */ public function getCodePath() { return $this->_engine->getCodePath(); } /** * Get the extension used for PHP files * * @return string PHP extension */ public function getCodeExtension() { $this->_engine->getPhpCodeExtension(); } /** * Load config options * * @param array $config */ private function _loadConfig($config){ if(!is_array($config)) { throw new My_View_Exception('Error loading configuration (must be an array)'); } foreach($config as $_optk=>$_optv){ if (!array_key_exists($_optk, $this->_configDirectives)) { throw new My_View_Exception("Configuration: Unknown option `$_optk`"); } $this->_configDirectives["$_optk"] = $_optv; } } /** * Set directory for compiled files */ private function _setCompiledDir(){ if (!is_dir($this->_configDirectives['compiledDir'])) { throw new My_View_Exception('Specified option `compiledDir` must be a directory'); } if(!is_writable($this->_configDirectives['compiledDir'])) { throw new My_View_Exception('Compiled files directory must be writable'); } $this->_engine->setPhpCodeDestination($this->_configDirectives['compiledDir']); } /** * Assign all variables to the PHPTAL engine * * @param array $variables Variables to assign */ private function _assignAll(array $variables = array()) { foreach ($variables as $key => $value) { $this->_engine->set($key, $value); } } /** * @see Zend_View_Abstract */ protected function _run() { $this->_engine->setTemplate(func_get_arg(0)); $this->_assignAll($this->_variables); try { if ($this->_configDirectives['gzipCompression'] && extension_loaded('zlib') && ini_get('zlib.output_compression') == 0 && stristr($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') ) { $frontController = Zend_Controller_Front::getInstance(); $frontController->getResponse()->setHeader('Content-Encoding', 'gzip', true); $frontController->getResponse()->setHeader('Vary', 'Accept-Encoding', true); $buffer = gzencode($this->_engine->execute(), $this->_configDirectives['gzipCompressionLevel'], FORCE_GZIP); $frontController->getResponse()->setHeader('Content-Length', strlen($buffer), true); echo $buffer; unset ($buffer); return; } echo $this->_engine->execute(); } catch (Zend_View_Exception $e) { throw new Zend_View_Exception($e); } } } /** * PHPTAL context modifier (helper:) * * @param string $src * @param boolean $nothrow * @return string */ function phptal_tales_helper($src, $nothrow) { $src = 'helper->'.trim($src); return PHPTAL_Php_Transformer::transform($src, '$ctx->'); }
Postanowiłem rozszerzyć klasę Zend_View_Abstract gdyż w ten sposób otrzymujemy pełną integrację z innymi komponentami jak Zend_Form. Sama implementacja Zend_View_Interface w niektórych przypadkach jest również bardzo korzystna ponieważ w mniejszym stopniu konsumuje zasoby. Drugiej metody nie będę omawiał ale sprowadza się głównie do przeniesienia kodu z metody _run() do render(). Należy również zadbać o wczytywanie helperów (metody add* z Zend_View_Abstract).
Klasa daje również możliwość gzipowanie zawartości i wysłanie odpowiednich nagłówków HTTP.
Do działania potrzebujemy jeszcze jednej klasy – /My/Browser.php z metody setContentType().
My/Browser.php
<?php class My_Browser { public $clientAcceptXhtml = false; /** * Check if client can accept MIME XHTML. * * @param void * @return bool true if client has MIME XHTML support, false if not detected. */ public function detectXhtmlMime(){ $this->clientAcceptXhtml = false; $sAccept = (isset($_SERVER['HTTP_ACCEPT'])) ? $_SERVER['HTTP_ACCEPT'] : ''; if (preg_match('/application\/xhtml\+xml(?![+a-z])(;q=(0\.\d{1,3}|[01]))?/i', $sAccept, $matches)){ $xhtmlQ = isset($matches[2])?($matches[2]+0.2):1; if (preg_match('/text\/html(;q=(0\d{1,3}|[01]))s?/i', $sAccept, $matches)){ $htmlQ = isset($matches[2]) ? $matches[2] : 1; $this->clientAcceptXhtml = ($xhtmlQ >= $htmlQ); return $this->clientAcceptXhtml; } else { $this->clientAcceptXhtml = true; return $this->clientAcceptXhtml; } } return false; } }
Sprawdza ona czy przeglądarka obsługuje typ MIME application/xhtml+xml. Opera i Firefox tak, IE nie obsługuje w związku z czym otrzyma text/html. Jeśli nie rozumiesz o czym mówię przeczytaj mój wcześniejszy artykuł.
Następnym krokiem będzie wskazanie PHPTAL jako widok. Dokonamy tego poprzez rejestrację pluginu. Utwórz plik /library/My/Controller/Plugin/SetPhptalView.php:
My/Controller/Plugin/SetPhptalView.php
<?php class My_Controller_Plugin_SetPhptalView extends Zend_Controller_Plugin_Abstract { public function preDispatch(Zend_Controller_Request_Abstract $request) { $config = Zend_Registry::get('config'); $frontController = Zend_Controller_Front::getInstance(); $dispatcher = $frontController->getDispatcher(); $moduleNameUnformatted = ($request->getModuleName()) ?: $dispatcher->getDefaultModule(); $moduleDir = $frontController->getModuleDirectory(); $moduleName = $dispatcher->formatModuleName($moduleNameUnformatted); $view = new My_View_Phptal($config->phptal->toArray()); $view->setContentType(My_View_Phptal::MIME_HTML) ->addHelperPath($moduleDir.'/views/helpers', $moduleName.'_View_Helper'); $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('ViewRenderer'); $viewRenderer->setView($view) ->setViewSuffix('xhtml'); } }
W pluginie automatycznie rejestrujemy ścieżkę do naszych własnych helperów (…/views/helpers), ustawiamy rozszerzenie szablonów na xhtml, oraz wysyłamy do klienta odpowiednie nagłówki (zobacz stałe My_View_Phptal::MIME*).
Następnie w swoim pliku startowym (bootstrap) zarejestruj plugin:
Bootstrap.php
[...] $frontController = Zend_Controller_Front::getInstance(); $frontController->registerPlugin(new My_Controller_Plugin_SetPhptalView());
W zaprezentowanym kodzie wykorzystywana jest również konfiguracja zarejestrowana w Zend_Registry pod kluczem ‘config’. Przyjmijmy, że korzystamy z Zend_Config_Ini wówczas przyjmie ona postać:
/application/config/app.config.ini
[production] phptal.compiledDir = "../tmp/phptal_compiled/" phptal.compiledFilesExtension = "php" phptal.charset = "UTF-8" phptal.gzipCompression = true phptal.gzipCompressionLevel = 7
Bootstrap.php
[...] $config = new Zend_Config_Ini('../application/config/app.config.ini', 'production'); Zend_Registry::set('config', $config);
Nota dla korzystających z Zend_Application
Zamiast rejestrować My_Controller_Plugin_SetPhptalView jako wtyczkę kontrolera należy stworzyć nowy obiekt – plugin zasobów (resource plugin).
My/Application/Resource/Phptal.php
<?php class My_Application_Resource_Phptal extends Zend_Application_Resource_ResourceAbstract { /** * View object * * @var My_View_Phptal */ protected $_view; /** * Defined by Zend_Application_Resource_Resource * * @return My_View_Phptal */ public function init() { $view = $this->getView(); $viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer(); $viewRenderer->setView($view) ->setViewSuffix('xhtml'); Zend_Controller_Action_HelperBroker::addHelper($viewRenderer); return $view; } /** * Retrieve view object * * @return My_View_Phptal */ public function getView() { if (null === $this->_view) { $frontController = $this->getBootstrap()->getResource('frontController'); $request = $frontController->getRequest(); $this->_view = new My_View_Phptal($this->getOptions()); $this->_view->setContentType(My_View_Phptal::MIME_HTML) ->addHelperPath('../application/modules/'.$request->getModuleName().'/views/helpers', $request->getModuleName().'_views_helpers_'); } return $this->_view; } }
Autoloader (Zend/Application/Module/Autoloader.php) automatycznie rejestruje przestrzeń nazw ‘View_Helper’.
Klucze konfiguracyjne dostępne będą dla opcji
[production] resources.My_Application_Resource_Phptal.compiledDir = APPLICATION_PATH "/../tmp/phptal_compiled/" resources.My_Application_Resource_Phptal. [...]
Kontroler i plik szablonu
PHPTAL powinien być gotowy do użycia. Przetestujmy. Przyjmijmy że mamy kontroler “user”, akcję “login” oraz wykorzystujemy makra i sloty. Specjalnie daję taki przykład aby pokazać jak świetnie taka koncepcja sprawdza się w Zend Framework. I to bez wykorzystywania dodatkowego kodu php. W sieci można znaleźć przykłady pełnej integracji PHPTAL z Zend_Layout gdzie stosujemy zwykłą metodę szablon główny -> include jednak według mnie neguje to całą ideę PHPTALa.
Zazwyczaj strona posiada jakiś główny plik nazwijmy go /application/modules/default/views/scripts/site.xhtml.
/application/default/views/scripts/site.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="pl" lang="pl" xmlns:tal="http://xml.zope.org/namespaces/tal" xmlns:metal="http://xml.zope.org/namespaces/metal" metal:define-macro="html"> <head> <tal:block metal:define-slot="head"/> </head> <body xml:lang="pl" lang="pl"> <div id="content"><tal:block metal:define-slot="content"/></div> </body> </html>
Przestrzenie nazw nie są wymagane ale dla poprawności dokumentu warto je dodawać (zostaną automatycznie usunięte z kodu wynikowego). W dokumencie widać również definicję makra “html” oraz slotów “head” i “content”. Wykorzystajmy je z naszym kontrolerem user/login. Utwórz plik /application/default/views/scripts/user/login.xhtml.
/application/default/views/scripts/user/login.xhtml
<tal:block metal:use-macro="../site.xhtml/html"> <tal:block metal:fill-slot="content"> <tal:block tal:content="structure form">form data</tal:block> <span tal:on-error="string:No username defined here" tal:content="user/name">the user name here</span> <div id="some" tal:content="structure helper:action('helper-test', 'user', null)"></div> </tal:block> <tal:block metal:fill-slot="head"> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <link href="stylesheet.css" rel="stylesheet" type="text/css"/> </tal:block> </tal:block>
Plik wypełniłem odrobinę losową zawartością aby zaprezentować łączenie z ZF. Zwróć uwagę na konstrukcję helper:action… W tym wypadku jest to mapowanie do helpera Zend_View_Helper_Action aczkolwiek możesz w ten sposób odwoływać się do dowolnych wbudowanych pluginów. Dodatkowo została zarejestrowana ścieżka …/views/helpers w którym to katalogu możesz umieszczać własne klasy. Formularz generujemy poprzez Zend_Form tak więc niezbędne jest użycie słowa “structure” aby PHPTAL wyświetlił kod HTML. Po kodzie widać, że jeżeli tylko chcesz możesz w zależności od kontrolera/akcji wczytywać dowolny plik główny, lub podstawiać różne dane do głównego szablonu. Daje to ogromne możliwości manipulacji dokumentem zwłaszcza w połączeniu z tal:condition.
Dane formularza oraz helper wypełniamy w kontrolerze.
/application/default/controllers/UserController.php
<?php class UserController extends Zend_Controller_Action { public function loginAction(){ //code from Zend_Form docs $form = new Zend_Form(); $form->setAction('/user/login') ->setMethod('post'); // Create and configure username element: $username = $form->createElement('text', 'username'); $username->addValidator('alnum') ->addValidator('regex', false, array('/^[a-z]+/')) ->addValidator('stringLength', false, array(6, 20)) ->setRequired(true) ->addFilter('StringToLower'); // Create and configure password element: $password = $form->createElement('password', 'password'); $password->addValidator('StringLength', false, array(6)) ->setRequired(true); // Add elements to form: $form->addElement($username) ->addElement($password) // use addElement() as a factory to create 'Login' button: ->addElement('submit', 'login', array('label' => 'Login')); $this->view->form = $form; // some todo code } public function helperTestAction() { $this->_helper->viewRenderer->setNoRender(); echo 'some test data'; }
Jeżeli formularz został utworzony poprawnie oraz zobaczyłeś tekst ‘some test data’ wszystko działa jak należy.
Zobaczmy jeszcze jak łatwe jest tworzenie plików XML. Do kontrolera możemy dopisać przykładową akcję “listusers”.
/application/default/controllers/UserController.php
public function listusersAction() { $this->view->users = $exampleDbTableUsers->fetchAll(); $this->_helper->viewRenderer->setViewSuffix('xml'); $this->view->setContentType(My_View_Phptal::MIME_XML); $this->view->setOutputMode(PHPTAL::XML); }
Przekazujemy do PHPTALa zmienną “users” zawierającą jakieś dane o użytkownikach pobrane z bazy. Zmieniamy też rozszerzenie plików na .xml oraz wysyłane nagłówki na ‘text/xml’ (MIME_XML). Tworzymy plik widoku:
/application/default/views/scripts/user/listusers.xml
<?xml version="1.0" encoding="UTF-8"?> <users> <user tal:repeat="user users"> <id>${user/id}</id> <name tal:content="string:text+ ${user/name}"></name> <rank tal:content="user/rank"></rank> </user> </users>
Plik jest bardzo czytelny (zaprezentowałem trzy różne metody odwoływania do zmiennych), możemy go podejrzeć w przeglądarce jako typowy xml. To kolejna zaleta PHPTALa.
Podsumowanie
PHPTAL to potężne narzędzie, do tego bardzo elastyczne i mam nadzieję że artykuł ten zachęci niezdecydowanych przynajmniej do pierwszych testów. Jako kontynuację zapraszam do lektury dokumentacji PHPTALa.
Dodaj komentarz
- php
- programowanie
- jquery
- xml
- xhtml
- http
- sieć
- internet
- javascript
- html
- mysql
- bazy danych
- zend framework
- css
- kodowanie
- postgresql
- apache
- zend framework 3
- sqlite
- phptal
Komentarze
Nie ma jeszcze żadnych komentarzy do wyświetlenia. Może chcesz zostać pierwszą osobą która podzieli się swoją opinią?