Como fazer um motor de templates HTML em PHP

Um motor de templates é usado para separar a apresentação da lógica de negócio. Um bom programador sabe como isto é importante - não só permite a separação de responsabilidades (enquanto o designer trabalha na parte da apresentação, o programador trabalha na parte da lógica de negócio) como também torna a manutenção muito mais fácil.

Existem muitos (e quando digo muitos quero dizer mesmo MUITOS) motores de templates para PHP. Um exemplo dos mais populares é o Smarty. A maior parte destes motores têm imensas funcionalidades avançadas e requerem que quem as use tenha de aprender uma nova sintaxe para poder saber como construir os ficheiros de template.

E se apenas necessitamos de um motor de templates simples de usar e de perceber? Porque não construirmos o nosso próprio motor? É mesmo isso que faremos neste tutorial - vamos criar um simples motor de templates em PHP, que qualquer pessoa será capaz de usar sem ter que perder tempo a ler manuais.

Os nossos ficheiros de template serão escritos em puro HTML com umas etiquetas extra para fácil substituição. Colocaremos estas etiquetas onde queremos que o nosso conteúdo fique - o motor funcionará basicamente como uma ferramenta de substituição, mas poderá ser actualizada com funcionalidades mais avançadas.

Podem ver uma descrição geral deste simples motor de templates em PHP na imagem seguinte.

Simples template HTML

Primeiro, vamos começar por definir o nosso template HTML. Temos de tomar uma decisão quanto ao formato de etiquetas que vamos usar. A maior parte dos templates usa chavetas, e.g. {etiqueta}, mas eu gosto de usar uma sintaxe diferente: [@etiqueta]. Escolha a convenção que achar mais adequada.

Imaginemos um caso típico em que queremos construir uma página de perfil de um utilizador. Vamos assumir que precisamos de mostrar a foto, nome de utilizador, nome verdadeiro, idade e local do utilizador. Um HTML para conseguir isto encontra-se a seguir:

<h1>[@username] profile</h1>
<img src="[@photoURL]" class="photo" alt="[@name]" />
<b>Name:</b> [@name]<br />
<b>Age:</b> [@age]<br />
<b>Location:</b> [@location]<br />

Vamos criar este ficheiro de template e guardá-lo. Eu costumo usar a extensão tpl para os templates. Neste caso, vamos chamar a este ficheiro user_profile.tpl.

Agora precisamos de carregar este template no nosso script PHP e substituir as etiquetas por valores verdadeiros.

Classe para o motor de templates

Para podermos usar mais facilmente esta solução vamos criar uma classe - chamemos-lhe Template. Esta classe apenas precisa de 2 variáveis membros - uma para guardar o nome do ficheiro com o template e a outra para guardar os valores que serão usados para substituir as etiquetas.

Comecemos por definir a nossa classe e o seu construtor. Vejam o código de seguida.

class Template {
    protected $file;
    protected $values = array();
 
    public function __construct($file) {
        $this->file = $file;
    }
}

Não colocarei aqui comentários no código (pois o tutorial já o explica), mas o código fonte deste tutorial encontra-se bem documentado com comentários.

Precisamos de pelo menos dois métodos: um para definir o valor de cada etiqueta e outro para obter o resultado final.

Guardaremos os valores num vector, onde cada valor será guardado com uma chave que corresponde à etiqueta a substituir. O método para definir os pares valor/etiqueta será chamado set.

O método para obter o resultado final é um pouco mais avançado (chamado output). Começamos por verificar se o ficheiro template existe. Se este não existe devolvemos uma mensagem de erro. Se existir então carregamos os conteúdos do ficheiro para uma variável. Depois precorremos o novo vector com os valores e substituímos cada etiqueta com o seu valor respectivo.

O código para estes 2 métodos é fornecido de seguida. Coloque estes métodos na classe Template.

public function set($key, $value) {
    $this->values[$key] = $value;
}
 
public function output() {
    if (!file_exists($this->file)) {
        return "Error loading template file ($this->file).<br />";
    }
    $output = file_get_contents($this->file);
 
    foreach ($this->values as $key => $value) {
        $tagToReplace = "[@$key]";
        $output = str_replace($tagToReplace, $value, $output);
    }
 
    return $output;
}

Colocar o motor em funcionamento

Agora já podemos testar a nossa primeira iteração do motor de templates. Vamos criar um simples ficheiro PHP chamado user_profile.php, que irá carregar o template com dados de teste e mostrar o resultado final.

Começamos por incluir o ficheiro com a definição da nossa classe Template (chamei a este ficheiro template.class.php). Depois criamos um novo objecto Template e definimos cada valor no template. No final mostramos o seu resultado.

include("template.class.php");
 
$profile = new Template("user_profile.tpl");
$profile->set("username", "monk3y");
$profile->set("photoURL", "photo.jpg");
$profile->set("name", "Monkey man");
$profile->set("age", "23");
$profile->set("location", "Portugal");
 
echo $profile->output();

Como podem ver é muito simples de usar. Num exemplo do mundo real estes valores seriam previamente carregados de uma base de dados ou ficheiro.

No entanto, há algo em falta. Se fossemos usar o resultado final do código acima estaríamos a produzir código HTML inválido, pois apenas definimos a parte principal do nosso conteúdo no nosso ficheiro de template (não há etiqueta <html>, etc.). Falta-nos um layout. Então vamos fazer um template para o layout!

O código seguinte representa um layout de um possível site. Contém um cabeçalho, menu, área de conteúdo e um rodapé. Podemos definir várias etiquetas ao longo do ficheiro (lembrem-se - é apenas mais um template!). Neste caso usei uma etiqueta title para poder definir o título da página e uma etiqueta content onde colocaremos o conteúdo principal de cada página.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>[@title]</title>
<link rel="stylesheet" type="text/css" href="stylesheet.css" />
</head>
<body>
    <div id="header">
        <a href="http://www.broculos.net"><img src="broculo_small.gif" class="logo" alt="Broculos.net" /></a>
        <h1><a href="http://www.broculos.net">Broculos.net</a></h1>
        <h2>Simple PHP Template Engine</h2>
    </div>  
    <div id="menu">
        <h1>Navigation</h1>
        <ul>
            <li><a href="user_profile.php">User profile</a> - example of a user profile page</li>
            <li><a href="list_users.php">List users</a> - example table with listing of users</li>
        </ul>
    </div>
    <div id="content">
        [@content]
    </div>
    <div id="footer">
        Example usage of a simple PHP Template Engine.<br />
        Search <a href="http://www.broculos.net">Broculos.net</a> for more tutorials.
    </div>
</body>
</html>

Todos estes ficheiros estão incluidos nos ficheiros deste tutorial.

Agora vamos usar o nosso template do perfil de utilizador para substituir a etiqueta content no template do layout. Este encadeamento de templates fornece-nos a flexibilidade necessária.

A nossa página user_profile.php deverá agora estar assim:

include("template.class.php");
 
$profile = new Template("user_profile.tpl");
$profile->set("username", "monk3y");
$profile->set("photoURL", "photo.jpg");
$profile->set("name", "Monkey man");
$profile->set("age", "23");
$profile->set("location", "Portugal");
 
$layout = new Template("layout.tpl");
$layout->set("title", "User profile");
$layout->set("content", $profile->output());
 
echo $layout->output();

Primeiro carregamos o template do perfil de utilizador. Depois carregamos o template do layout e colocamos o conteúdo do perfil nele. Finalmente mostramos o HTML.

Manipulação mais avançada de templates

Se olhou com atenção para a área de navegação do template do layout reparou uma ligação para a página lists_users.php. Isto será o tópico para este capítulo.

Queremos uma página que liste os utilizadores. Como poderemos fazer isto? A minha abordagem é dividir isto em 2 templates: um para o conteúdo principal (i.e. a descrição/título e a definição da tabela) e outro para a listagem de cada utilizador (cada linha da tabela).

Comecemos por criar o template list_users.tpl (o template principal da página de listagem de utilizadores).

<h1>Users</h1>
<table>
    <thead>
        <tr>
            <th>Username</th>
            <th>Location</th>
        </tr>
    </thead>
    <tbody>
        [@users]
    </tbody>
</table>

Apenas um título e a definição da tabela dos utilizadores. Agora precisamos de um template para cada linha da tabela (list_users_row.tpl).

    <tr>
        <td>[@username]</td>
        <td>[@location]</td>
    </tr>

Precisamos de combinar estes dois num só. Como cada utilizador/linha será representado por um template diferente vamos fazer um método que nos permite juntar vários templates. Isto porque precisamos do valor conjunto destes templates para depois podermo-lo usar para substituir a etiqueta users no template principal list_users.tpl.

static public function merge($templates, $separator = "n") {
    $output = "";
 
    foreach ($templates as $template) {
        $content = (get_class($template) !== "Template")
            ? "Error, incorrect type - expected Template."
            : $template->output();
        $output .= $content . $separator;
    }
 
    return $output;
}

Adicionem o método acima à classe Template. Este método precorre o vector de objectos Template, concatena o seu resultado e coloca um separador entre eles (o separador por omissão é uma quebra de linha para produzir código HTML mais fácil de ler).

O método merge necessita de um vector de objectos Template. Se existir um valor de outro tipo incluído no vector será concatenada uma mensagem de erro ao resultado (penso que isto é preferível a terminar o script abruptamente com um erro).

Agora só precisamos da nossa página list_users.php. Nesta página vamos criar alguns utilizadores fictícios, precorreremos o vector de utilizadores e criamos um template para cada um deles. Depois juntamos os templates num só valor para inclusão no template list_users.tpl. Vamos novamente usar o layout definido anteriormente.

include("template.class.php");
 
$users = array(
    array("username" => "monk3y", "location" => "Portugal")
    , array("username" => "Sailor", "location" => "Moon")
    , array("username" => "Treix!", "location" => "Caribbean Islands")
);
 
foreach ($users as $user) {
    $row = new Template("list_users_row.tpl");
 
    foreach ($user as $key => $value) {
        $row->set($key, $value);
    }
    $usersTemplates[] = $row;
}
$usersContents = Template::merge($usersTemplates);
 
$usersList  = new Template("list_users.tpl");
$usersList->set("users", $usersContents);
 
$layout = new Template("layout.tpl");
$layout->set("title", "Users");
$layout->set("content", $usersList->output());
 
echo $layout->output();

E é isto! Finalmente terminamos.

Conclusões - Limitações e ideias finais

Isto é apenas uma possível abordagem para estruturarem os vossos ficheiros. Ainda é possível uma separação mais avançada entre a apresentação e a camada de negócio: os nossos ficheiros ainda têm muito código cujo único propósito é apenas preparar a apresentação. Uma abordagem é incluir mais complexidade nos templates: ou através de uma sintaxe definida propositadamente para os templates ou então através de uma mistura de PHP com HTML.

Pessoalmente gosto que os meus ficheiros HTML estejam limpos de outro código qualquer. Uma alternativa é usar classes para a apresentação. Estas classes poderiam ser descendentes da classe Template. Vamos assumir que um utilizador é definido através da classe User e que a classe VUser é usada para representar um template de utilizador.

class User {
    var $username;
    // etc.
}
 
class VUser extends Template {
    public function __construct(User $user) {
        $this->file = "user_profile.tpl";
        $this->values["username"] = $user->username;
        // etc.
    }
}

Na nossa página PHP apenas precisamos do seguinte:

$user = loadUser();
$tpl = new VUser($user);
echo $tpl->output();

Muito mais limpo, não é?

O nosso motor de templates é ainda muito limitado. Poderíamos usar funcionalidades mais avançadas como caching e outras técnicas para melhorar a performance.

Recursos

Nuno Freitas
Publicado por Nuno Freitas em 15 março, 2008

Artigos relacionados