Dla osób, które przyzwyczajone są do Smartów lub po prostu nie lubią znaczników phpowch w widokach, istnieje kilka sposobów na zastąpienie domyślnych phtmli na tple. W poprzednim wpisie pokazałem prostą implementację struktury modułowej - teraz do tej struktury załaduję Smarty. W założeniach chce by każdy moduł dysponował swoim, odrębnym layoutem. W przypadku małych aplikacji wystarczą dwa proste moduły: default po stronie klienta i admin dla administratora, gdzie każda moduł dysponuje innym, odrębnym szablonem strony. Dodatkowo jeśli potrzebowalibyśmy np: modułu bloga, to ten jako 3 dołączylibyśmy do struktury katalogów.
Przykładowa struktura aplikacji - 3 moduły:
index.php
application/
assistant/
modules/
default/
controllers/
IndexController.php
models/
views/
layout.tpl
index/
action.tpl
blog/
controllers/
IndexController.php
models/
views/
layout.tpl
index/
action.tpl
admin/
controllers/
IndexController.php
models/
views/
layout.tpl
index/
action.tpl
library/
Zend/
public/
images/
js/
css/
cache/
templates_c
smarty_cache
By móc jednak korzystać z takiej struktury niezbędne są zmiany w kodzie. Należy zastąpić domyślny widok Zenda - Zend_View - naszym widokiem - np. Smarty_View, a także nanieść drobne poprawki do Zend_Layout (wystarczy przeciążyć metody).
Aby tego dokonać utworzymy 2 nowe katalogi: “core” (dla naszych klas) i “smarty” (dla biblioteki Smarty, którÄ… można pobrać ze strony projektu), które umieÅ›cimy w “library”:
library/
Zend/
Core/
Controller/
View/
Smarty/
Chcąc zwiększyć możliwości i funkcjonalność naszej aplikacji stworzymy własny kontroler frontowy. Umieścimy w nim wspólne dla wszystkich kontrolerów elementy, podmienimy domyślny widok i utworzymy Layout.
<?php
abstract class Core_Controller_Action extends Zend_Controller_Action {
protected $config;
public function init() {
}
}
?>
Do podmiany domyślnego widoku będziemy potrzebowali własnej klasy widoku implementującej Zend_View_Interface. Wzór klasy znajduje się w dokumentacji Zenda. Kod pokazany poniżej zawiera drobne, niezbędne poprawki. Plik należy umieścić w library/Core/View/ pod nazwą Smarty.php :
?php
require_once 'Zend/View/Interface.php';
require_once 'Smarty/Smarty.class.php';
class Core_View_Smarty implements Zend_View_Interface
{
/**
* Smarty object
* @var Smarty
*/
protected $_smarty;
/**
* Constructor
*
* @param string $tmplPath
* @param array $extraParams
* @return void
*/
public function __construct($tmplPath = null, $extraParams = array())
{
$this->_smarty = new Smarty;
if (null !== $tmplPath) {
$this->setScriptPath($tmplPath);
}
foreach ($extraParams as $key => $value) {
$this->_smarty->$key = $value;
}
}
/**
* Return the template engine object
*
* @return Smarty
*/
public function getEngine()
{
return $this->_smarty;
}
/**
* Set the path to the templates
*
* @param string $path The directory to set as the path.
* @return void
*/
public function setScriptPath($path)
{
if (is_readable($path)) {
$this->_smarty->template_dir = $path;
return;
}
throw new Exception('Invalid path provided :' . $path);
}
/**
* Set the path to the copile directory
*
* @param string $path The directory to set as the path.
* @return void
*/
public function setCompilePath($path)
{
if (is_readable($path)) {
$this->_smarty->compile_dir = $path;
return;
}
throw new Exception('Invalid path provided :' . $path);
}
/**
* Set the path to the smarty cache directory
*
* @param string $path The directory to set as the path.
* @return void
*/
public function setCachePath($path)
{
if (is_readable($path)) {
$this->_smarty->cache_dir = $path;
return;
}
throw new Exception('Invalid path provided :' . $path);
}
/**
* Set the path to the smarty configs directory
*
* @param string $path The directory to set as the path.
* @return void
*/
public function setConfigPath($path)
{
if (is_readable($path)) {
$this->_smarty->config_dir = $path;
return;
}
throw new Exception('Invalid path provided :' . $path);
}
/**
* Retrieve the current template directory
*
* @return string
*/
public function getScriptPaths()
{
return array($this->_smarty->template_dir);
}
/**
* Alias for setScriptPath
*
* @param string $path
* @param string $prefix Unused
* @return void
*/
public function setBasePath($path, $prefix = 'Zend_View')
{
return $this->setScriptPath($path);
}
/**
* Alias for setScriptPath
*
* @param string $path
* @param string $prefix Unused
* @return void
*/
public function addBasePath($path, $prefix = 'Zend_View')
{
return $this->setScriptPath($path);
}
/**
* Assign a variable to the template
*
* @param string $key The variable name.
* @param mixed $val The variable value.
* @return void
*/
public function __set($key, $val)
{
$this->_smarty->assign($key, $val);
}
/**
* Retrieve an assigned variable
*
* @param string $key The variable name.
* @return mixed The variable value.
*/
public function __get($key)
{
return $this->_smarty->get_template_vars($key);
}
/**
* Allows testing with empty() and isset() to work
*
* @param string $key
* @return boolean
*/
public function __isset($key)
{
return (null !== $this->_smarty->get_template_vars($key));
}
/**
* Allows unset() on object properties to work
*
* @param string $key
* @return void
*/
public function __unset($key)
{
$this->_smarty->clear_assign($key);
}
/**
* Assign variables to the template
*
* Allows setting a specific key to the specified value, OR passing an array
* of key => value pairs to set en masse.
*
* @see __set()
* @param string|array $spec The assignment strategy to use (key or array of key
* => value pairs)
* @param mixed $value (Optional) If assigning a named variable, use this
* as the value.
* @return void
*/
public function assign($spec, $value = null)
{
if (is_array($spec)) {
$this->_smarty->assign($spec);
return;
}
$this->_smarty->assign($spec, $value);
}
/**
* Clear all assigned variables
*
* Clears all variables assigned to Zend_View either via {@link assign()} or
* property overloading ({@link __get()}/{@link __set()}).
*
* @return void
*/
public function clearVars()
{
$this->_smarty->clear_all_assign();
}
/**
* Processes a template and returns the output.
*
* @param string $name The template to process.
* @return string The output.
*/
public function render($name)
{
return $this->_smarty->fetch($name);
}
}
?>
Abyśmy mogli dodatkowo dysponować osobnym szablonem widoku, w każdym module o takiej samej nazwie (layout.tpl), należy rozszerzyć klasę Zend_Layout i przeciążyc odpowiednie metody (Operacja ta jest niezbędna w momencie, gdy chcemy bo framework sam wywoływał odpowiedni layout w zależności od modułu, w tym przypadku będzie to layout.tpl, znajdujący się w każdym module w katalogu view. Zend_Layout domyślnie renderuje layout na podstawie jego nazwy, poprawiona wersja będzie renderowała szablon na podstawie jego kompletnej ścieżki). Następujący kod należy umieścić w pliku Layout.php w library/Core/ :
<?php
class Core_Layout extends Zend_Layout {
public function __construct($options = null, $initMvc = false){
return parent::__construct($options, $initMvc);
}
public static function startMvc($options = null)
{
if (null === self::$_mvcInstance) {
self::$_mvcInstance = new self($options, true);
} else {
self::$_mvcInstance->setOptions($options);
}
return self::$_mvcInstance;
}
public function render($name = null)
{
if (null === $name) {
$name = $this->getLayout();
}
if ($this->inflectorEnabled() && (null !== ($inflector = $this->getInflector())))
{
$name = $this->_inflector->filter(array('script' => $name));
}
$view = $this->getView();
if (null !== ($path = $this->getViewScriptPath())) {
if (method_exists($view, 'addScriptPath')) {
$view->addScriptPath($path);
} else {
$view->setScriptPath($path);
}
} elseif (null !== ($path = $this->getViewBasePath())) {
$view->addBasePath($path, $this->_viewBasePrefix);
}
/* renderuje layout na podstawie jego scieżki */
return $view->render($this->getLayoutPath() . $name);
}
}
?>
(Być może są prostsze sposoby o których nie wiem)
Teraz należy zebrać to wszystko do kupy i odpowiednią konfigurację zawrzeć w metodzie init() naszego nowego Front kontrolera (metoda init jest wywoływana na samym początku procesu, nim zostanie wykonana jakakolwiek akcja kontrolera):
<?php
abstract class Core_Controller_Action extends Zend_Controller_Action {
protected $config;
public function init() {
$this->config = new Zend_Config_Ini('application/config.ini', null);
/* Zapisanie konfiguracji w rejestrze */
Zend_Registry::set('config',$config);
/* nasz nowy widok Smarty View */
$view = new Core_View_Smarty();
/* ustawiamy niezbędne ścieżki */
$view->setCompilePath('./cache/templates_c/');
$view->setCachePath('./cache/cache_smarty/');
$this->view = $view;
/* viewRenderer */
$viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
/*ustawiamy ścieżki dla widoków dla każdego wywoływanego modułu */
$viewRenderer
->setView($view)
->setViewScriptPathSpec(getcwd() . '/application/modules/:module/views/:controller/:action.:suffix')
->setViewSuffix('tpl');
/* konfiguracja Layoutu */
$options = array(
'viewSuffix' => 'tpl',
'layoutPath' => getcwd() . '/application/modules/' . $this->_request->getModuleName() . '/views/',
);
/* utworzenie obiektu Layoutu */
$this->layout = Core_Layout::startMvc($options);
/*przekazanie Layoutu do szablonu */
$view->assign( 'layout', $this->layout );
$view->assign( 'location', $this->config->globals->location);
}
}
?>
Aby wszystko działało poprawnie każdy z naszych kontrolerów musi teraz dziedziczyć po Core_Controller_Action zamiast po Zend_Controller_Action. Uzyskaliśmy dodatkowo prosty dostęp do konfiguracji ($this->config).
Przykład (plik: IndexController.php)
<?php
class IndexController extends Core_Controller_Action
{
public function indexAction(){
/* konfiguracja aplikacji */
print_r($this->config->toArray());
$this->view->hello = "Hello World";
}
}
?>
Teraz, by do szablonu konkretnego modułu, zwracany był odpowiedni widok z akcji, należy w nim umieścić następujący kod (plik: layout.tpl):
{$layout->content}
Wywołanie na przykład adresu: www.naszadomena.pl/admin/index/test spowoduje wywołanie IndexControllera z modułu admin, wyrenderowanie layoutu admina wraz z widokiem zwróconym przez akcje test.
W przyszłości postarm się przedstawić bardziej zaawansowane wykorzystanie obiektu Layout dla definiowania takich elementów jak nagłówek, stopka czy menu.
Aktualną strukturęnaszej aplikacji przedstawia poniższy schemat:
index.php
application/
assistant/
modules/
default/
controllers/
IndexController.php
models/
views/
layout.tpl
index/
action.tpl
blog/
controllers/
IndexController.php
models/
views/
layout.tpl
index/
action.tpl
admin/
controllers/
IndexController.php
models/
views/
layout.tpl
index/
action.tpl
library/
Zend/
Core/
Layout.php
Controller/
Action.php
View/
Smarty.php
Smarty/
public/
images/
js/
css/
cache/
templates_c
smarty_cache
Dodatkowo powinniśmy wyposażyć naszą aplikację w ErrorController, o czym szerzej napiszę w następnym wpisie.
Bardo fajne teksty, apopro powyższego to korzystanie ze Smarty wydaje mi się trochę przyrostem formy nad treścią. Zwykłe wykorzystanie PHP w szablonach wydaje mi się przyjemniejsze. Ale to oczywiście kwestia gustu.
Smarty znacznie poprawiają czytelność kodu. Chodzi mi tu głównie o webmasterów, w przypadku niektórych projektów wykorzystanie Smartów w widokach było koniecznością. Oczywiście wszystko ma swoje granice. W niewielkich projektach, o małym stopniu skomplikowania szablonów - sprawdzają się świetnie. Jednak gdy przychodzi do bardziej ambitnych zadań - Smarty po prostu utrudniają prace (oczywiście można kombinować, rejestrować własne funkcje czy modyfikatory, tylko pytanie czy naprawdę warto).
Szablony to już taki standard w aplikacjach opartych na php. Na pewno nie zrezygnował bym z nich.Ale pytanie brzmi czy rzeczywiście Smarty są najlepszym rozwiązaniem w aplikacji opartej na Zend Frameworku? Wg mnie dostosowywać Smarty do nowych funkcjonalności wprowadzanych przez Framework mija się z celem. Dlatego coraz bardziej przemawia do mnie Zend_View.
dzięki za podsunięcie rady z tym Layoutem, ale mam pytanie o treść configa (application/config.ini)
Nie wiem czy dokładnie chodzi Ci o tego configa. To jest config z aplikacji już z podpiętym Doctrine:
; Podstawowe dane konfiguracyjne
[globals]
location = “http://xxx.xx”
[meta]
title = “title”
description = “description”
keywords = “keywords”
;trasy routingu
[routes]
;trasa routingu dla strony głównej
default.route = “:controller/:action/*”
default.defaults.errorLayout = “default”
default.defaults.module = “default”
default.defaults.controller = “index”
default.defaults.action = “index”
default.defaults.web = “”
;trasa routingu dla panelu administracyjnego
admin.route = “admin/:controller/:action/*”
admin.defaults.errorLayout = “admin”
admin.defaults.module = “admin”
admin.defaults.controller = “index”
admin.defaults.action = “index”
;bazy danych
[database]
name = “db-name”
prefix = “”
type = “mysql”
dbname = “db=name”
host = “localhost”
username = “user”
password = “pass”
charset = “utf8″