Todo bom FrameWork tem que nos dar a possibilidade de personalizá-lo, e um dos requisitos mais necessários de personalização é a URL, e nesse artigo será explicado como obter essa funcionalidade utilizando o padrão de projeto Strategy, que tem por objetivo trocar dinamicamente o comportamento da aplicação;
Como um exemplo para melhor entendimento do Padrão Strategy, considere uma classe Guerreiro, que possui um método chamado lutar, onde nele ele utiliza de uma arma golpear os adversários; Porém existem vários tipos de armas (Espada, Machado, Lança, Arco/Flecha), e para ganhar uma guerra, deve-se ter vários tipos de armamentos distribuídos entre os guerreiros;
Como primeira idéia de implementação, vamos criar uma implementação Guerreiro padrão, e para cada tipo de armamento vamos estender a classe padrão sobrescrevendo o método lutar para utilizar outro armamento; Sendo assim temos:
Como primeira idéia de implementação,
<?php class Guerreiro { // Outros métodos do Guerreiro ... public function lutar() { // Implementação da luta echo 'lutando com os punhos'; } } class Arqueiro extends Guerreiro { public function lutar() { // Implementação da luta echo 'Atirando com o Arco'; } } class Cavaleiro extends Guerreiro { public function lutar() { // Implementação da luta echo 'Lutando com espada'; } } ?>
De inicio parece uma boa solução, porém a luta não consiste apenas em golpear o adversário, mas sim de vários outros comportamentos que não depende da arma, e sendo assim, esse código ficará repetido em cada classe que estender a classe Guerreiro, o que não é uma boa solução, tendo em vista que na hora da alteração desse código, terá que passar em cada classe alterando o mesmo código;
Para melhorar solucionar esse problema, vamos aplicar o conceito do padrão Strategy, onde será mantida uma unica implementação de Guerreiro que receberá em seu construtor qualquer classe que implemente a interface Arma, que contem um método para realizar o golpe; Sendo assim:
<?php //Implementação de Guerreiro class Guerreiro { private $arma; public function __construct(Arma $arma) { $this->arma = $arma; } // Outros métodos do Guerreiro ... public function lutar() { // Implementação da luta $this->arma->golpear(); } } interface Arma { public function golpear(); } class ArcoFlecha implements Arma { public function golpear() { echo 'Atirando com o arco'; } } class Punho implements Arma { public function golpear() { echo 'Cabra macho luta com os punhos'; } } class Espada implements Arma { public function golpear() { echo 'Lutando com a Espada'; } } // Exemplo $arqueiro = new Guerreiro(new ArcoFlecha()); $cavaleiro = new Guerreiro(new Espada()); $cabraMacho = new Guerreiro(new Punho()); ?>
O padrão Strategy segue o padrão de Orientação a Objetos de encapsular o que varia, mantendo o codigo comum em um unico só lugar, ficando fácil a manutenção que, no nosso exemplo, basta implementar a interface Arma caso precisar de um novo tipo de arma na guerra; Outra vantagem vem da amplitude onde o problema pode ser expandido, ou seja, caso existam outro tipo de classe que lute com armas (um Pirata, por exemplo), não é necessário reimplementar todo o comportamento de luta com armas:
<?php class Pirata { private $arma; public function __construct(Arma $arma) { $this->arma = $arma; } public function invadirNavioInimigo() { $this->arma->golpear(); } } $pirata = new Pirata(new Espada()); ?>
Aplicando esse conceito no framework, vamos alterar a forma como é lido os parâmetros que definem qual Module/Action que será acessada;
Utilizar variáveis GET para determinar qual action deve ser executada não é uma boa prática, e tornam “feias” as URLs do Sistema; Para isso, vamos aplicar o conceito de URLs amigáveis, no lugar das variáveis GET; Assim, os valores do module e da action que será acessada no sistema, serão lidos diretamente da URL;
No Framework atual, a leitura dos parâmetros que decidem qual Command executar esta sendo feita atualmente dentro do Command_Factory juntamente com o código de carregar o Command; Nesse caso em particular, teremos que separar as duas funcionalidades, pois vai variar a forma que será recuperado os parâmetros para carregar os Command, e seguindo a definição do Strategy, vamos criar uma interface que irá descrever como que as implementações de leitura dos parâmetros deve fornecer os valores, assim o factory não irá se importar de qual implementação ele esta recuperando os parâmetros;
Com essa alteração, cada classe passa a ter sua responsabilidade bem definida, e sempre que se deseja mudar a forma como que os parâmetros devem ser lidos, basta criar uma classe que implementa a interface que realiza essa ação e passa-la pro Factory sem precisar mudar uma linha de código do Factory;
Nossa interface se chamará Command_Info, pois ela guardara as informações da URL de acesso, e terá dois métodos principais, que irá retornar o Modulo e a Action desejada, ficando da seguinte forma:
arquivo: /lib/Command/Info.php
interface Command_Info { //Método que retorna o Module public function getModule(); //Método que retorna a Action public function getAction(); }
Vamos criar a nossa implementação padrão, que irá recuperar os parâmetros da mesma forma que estava sendo feita anteriormente:
arquivo: /lib/Command/Info/Default.php
class Command_Info_Default implements Command_Info { // Retorna o valor do Module contido no GET public function getModule() { return $_GET['module']; } // Retorna o valor da Action contigo no GET public function getAction() { return $_GET['action']; } }
O Command_Factory receberá uma instância de alguma implementação de Command_Info por parâmetro, para então realizar a chamada ao Command correto;
arquivo: /lib/Command/Factory.php
<?php class Command_Factory { public function createCommand(Command_Info $info) { //Se o module não for informado, ele passa a ser considerado o index; $module = ($info->getModule() != null) ? $info->getModule() : 'index'; //A mesma coisa para a action $action = ($info->getAction() != null) ? $info->getAction() : 'index'; //Deixa a primeira letra em Maiusculo e o resto da string em minusculo $module = htmlspecialchars(ucfirst(strtolower($module))); $action = htmlspecialchars(ucfirst(strtolower($action))); //Cria o nome do arquivo da Action $fileName = 'command/'.$module.'/'.$action.'.php'; //Se o arquivo não existir, gera erro if (!file_exists($fileName)) { throw new Exception_Pagenotfound('Arquivo do Command não encontrado: '.$fileName); } //Carrega o arquivo require_once $fileName; //O nome da classe é a Action seguida da String Command $className = $action.'Command'; //Depois que o arquivo for adicionado, a classe deve existir, caso não existir, o arquivo carregado //não contem o Command, então deve gerar outro erro; o parâmetro false no class_exists //é informado para não carregar a classe utilizando o __autoload if (!class_exists($className,false)) { throw new Exception_Pagenotfound('Command não foi encontrado: '.$className); } //Carrega e retorna a Classe return new $className(); } } ?>
Notem que não é obrigatório a classe Strategy ser informadam no construtor, podendo ser informada como parâmetro do método; Vale tambem ressaltar que foi definido que o Command_Factory deve receber uma instância de Command_Info como parâmetro, e esse é um dos poucos casos onde o PHP trata o tipo da variável, que no nosso caso é essencial essa verificação de tipo, pois caso seja passado uma variável qualquer, o nosso Command_Factory não irá conseguir trabalhar corretamente;
Agora que temos toda a estrutura separada e flexível a ponto de podemos personaliza-la, precisamos de uma maneira setar a configuração; Geralmente se utiliza arquivo texto (XML, YAML, etc) para essa configuração, mas no nosso caso vamos utilizar uma classe PHP, que na minha opinião é a melhor escolha, pois pula a etapa de abrir e ler um arquivo para armazenar as configurações em variáveis do PHP, tornando o sistema mais rápido;
Obs.: A forma de configuração utilizada nesse artigo não é apenas um exemplo, e sua implementação não faz parte do foco; Existem outras formas de fazer essa configuração, podendo você escolher a que mais lhe agrada
arquivo: lib/Config.php
<?php //Arquivo de configuração do FrameWork class Config { private $commandInfo; public function configure() { //Configura a Classe que irá ler os parâmetros da URL $this->commandInfo = new Command_Info_Default(); } // Retorna o CommandInfo public function getCommandInfo() { return $this->commandInfo; } } ?>
arquivo: index.php
try { //Inicia a configuração $config = new Config(); $config->configure(); //Instancia o Factory e Recupera o Command $factory = new Command_Factory(); $factory->createCommand($config->getCommandInfo())->execute(); } catch (Exception_Pagenotfound $ep) { echo '<h1>ERRO 404 - Página não encontrada</h1>'; } catch (Exception $e) { echo '<h1>ERRO 500 - Erro na execução</h1>'; } ?>
Vamos agora a partir dessa nova estrutura, alterar o comportamento do FrameWork para utilizar URL amigáveis para acessar os command; Ja existem vários artigos disponíveis na web explicando passo a passo como criar Url’s amigáveis, e a implementação apresentada aqui será totalmente baseada no artigo disponível no fórum do iMasters e escrito por Perfect Lion;
Antes de tudo, é necessário que o modulo rewrite do Apache esteja habilitado dentro do arquivo httpd.conf; Ele é o modulo responsável por redirecionar as requisições, sendo que, a forma de redirecionamento é definida através do arquivo .htaccess, que deve estar localizado na raiz do site;
A Url que vamos utilizar terá o seguinte formato:
http://www.endereco.qualquer/MODULE/ACTION sendo que, caso nenhum dos dois for informado, será utilizado index como padrão;
A seguir, o código do .htaccess adaptado para utilizar no framework:
# << URLs Amigaveis
# verifica se o modulo rewrite está habilitado se sim executa o bloco dentro
# habilita o modulo Rewrite
RewriteEngine On
# Foi comentada essa linha para que o rewrite funcione somente nos arquivos que estão no diretorio do .htaccess
# RewriteBase /
# Verifica se a url digitada é um arquivo caso seja para aqui
RewriteCond %{REQUEST_FILENAME} !-f
# Verifica se a url digitada é uma pasta caso seja para aqui
RewriteCond %{REQUEST_FILENAME} !-d
# Redireciona para o arquivo index.php caso nenhum desses comandos assim seja valido vai para ele.
RewriteRule .(/)?$ index.php
# >> URLs Amigaveis
Vamos agora adaptar a implementação do Perfect Lion para a nossa estrutura, criando assim a classe Command_Info_Rewrite;
arquivo: lib/Command/Info/Rewrite.php
<?php /** * Command_Info utilizando URL Amigável * * Código Original por Perfect Lion em http://forum.imasters.uol.com.br/index.php?showtopic=203965 * * A unica alteração foi para que o script funcione quando estiver dentro de um diretório no servido, pois o * script original funciona na raiz do servidor * * @author Everton Emilio Tavares */ class Command_Info_Rewrite implements Command_Info { private $gets; public function __construct() { // Recupera o diretorio em que o sistema se encontra echo $diretorio = substr($_SERVER['SCRIPT_FILENAME'],strlen($_SERVER['DOCUMENT_ROOT']),-10); // Recupera a URI $uri = $_SERVER["REQUEST_URI"]; // Recupera os dados da URI. esta sendo utilizada a mesma variável do artigo para melhor entendimento $gets = explode("/",str_replace(strrchr($uri, "?"), "", $uri)); // Primeiro elemento sempre será em branco, então faz um array_shift para remove-lo array_shift($gets); // Caso o sistema esteja em um diretório, é necessário remover o caminho de pastas do mesmo $this->gets = array_slice($gets,sizeof(explode('/',$diretorio))); } // Retorna a primeira parte da URL como o Module public function getModule() { return $this->gets[0]; } // Retorna a Segunda parte da URL como Action public function getAction() { return $this->gets[1]; } } ?>
Basta agora alterarmos a configuração do framework para utilizarmos nossa nova classe:
arquivo: /config.php
<?php /** * Classe de Configuração do FrameWork * @author Everton Emilio Tavares */ class Config { private $commandInfo; public function configure() { //Configura a Classe que irá ler os parâmetros $this->commandInfo = new Command_Info_Rewrite(); } // Retorna o CommandInfo public function getCommandInfo() { return $this->commandInfo; } } ?>
Assim temos nosso Framework configurável e adaptável para qualquer situação e a qualquer tipo de URL sem modificar as classes do coração do FrameWork; É possível ainda possibilitar a configuração de outras partes do FrameWork, como a Factory de Commands, por exemplo;
[...] composite, vamos utilizar a classe guerreiro utilizada na parte 3 do artigo; Caso você perdeu, leia aqui; Veja novamente a nossa implementação de Guerreiro: //Implementação de Guerreiro class [...]