Criando um FrameWork PHP do Zero Usando Padrões de Projeto (Parte 1)

out 01, 2008 I PHP.

Hoje em dia, para um bom desenvolvimento de softwares em PHP é imprescindível o uso de um Framework. Existe vários disponíveis, dentre eles podemos citar: Symfony; Prado; Zend; Cake, etc. Mas mesmo com tantos frameworks disponíveis atualmente, será que vale a pena o desenvolvimento de mais um framework? Realmente não compensa, já que quase todos os frameworks já possui uma quantidade grande de usuários e de sistemas desenvolvidos, fazendo com que esses frameworks sejam confiáveis e robustos;

Mas o interessante de se desenvolver seu próprio framework é conhecer o funcionamento do mesmo, e aprender os mecanismos por trás dele para que o nivel de programação do seu código possa ser elevado; Portanto o objetivo principal deste artigo é desenvolver um Framework, para que possa, através da codificação do mesmo compreender os princípios básicos dos frameworks em geral, e conhecer um pouco mais de padrões de Projeto;

Como a maioria dos Frameworks da atualidade, vamos o implementar através do padrão MVC, (Model-View-Controller), que visa separar as camadas de Dados, de Visão e de Controle de forma intercambiável. Na primeira parte vamos desenvolver a parte inicial do Controller que é o responsável pelo fluxo das informações dentro do sistema.

Para muitas pessoas que programam em PHP, a codificação se da em um único arquivo, onde a ação a ser executada era definida a partir de uma variável GET e de IF, que direciona a execução do código dependendo do valor dessa variável; Se for analisar esse código, podemos considerar isso como um pequeno controller, sendo que é a partir dessa idéia iremos desenvolver o Framework;

Sendo assim, a seguir temos nossa primeira versão do nosso Controller, que está “controlando” as actions de um mini-sistema de noticia:

Arquivo: noticias.php

<?php
$acao = $_ GET['action'];
if ($acao == 'ler') {
     $id_da_noticia = $_GET['id'];
     echo 'Lendo a noticia de id '.$id_da_noticia;
} else if ($acao == 'formulario') {
     echo 'Formulário de Cadastro de Noticia';
} else {
     echo 'Home das Noticias';
}
?>

A idéia aqui é intuitiva: Para cadastrar uma nova noticia, invoca-se a URL: noticia.php?action=formulario. Caso o parâmetro action for omitido, ou tiver um valor inválido será exibida uma página listando todas as Noticias cadastradas, que é sera Home do sistema de noticias;

O código é funcional, mantém tudo em um único lugar, porém durante as manutenções, o caos aparece. Com o aumento de tamanho ficará complicado se localizar dentro do arquivo, tanto para encontrar o local para uma correção como para adicionar uma nova action. A fim de conseguir uma melhor organização, vamos começar separando as ações em funções, e em seguida, um switch é utilizado para direcionar o fluxo da aplicação para a função desejada:

Arquivo: noticias.php

<?php
 
function home() {
     echo 'Home das Noticias';
}
 
function ler() {
     $id_da_noticia = $_GET['id'];
     echo 'Lendo a noticia de id '.$id_da_noticia;
}
 
function formulario() {
     echo 'Formulário de Cadastro de Noticia';
}
 
switch($_GET['action']) {
     case 'formulario': formulario();break;
     case 'ler': ler(); break;
     default: home(); break;
}
 
?>

Aparentemente nosso código está significativamente mais legível. Porém o problema da manutenção ainda permanece, o arquivo ficará maior que o da versão anterior, devido a todas as functions que deverão ser criadas; Para melhorar essa organização, vamos separar as funções em arquivos, e incluí-los conforme a action requisitada:

<?php
switch($_ GET['action']) {
     case 'formulario':
          include('formulario.php');
          formulario();
          break;
     case 'ler':
          include('ler.php');
          ler();
          break;
     default:
          include('home.php');
          home();
          break;
}
?>

O código acima aumentou a ainda mais a legibilidade porém a manutenção ainda é trabalhosa pois necessitamos realizar manualmente os includes dos arquivos das funções, o que não é nada empolgante, e caso o nome do arquivo incluído esteja errado, aparecerá um erro, para melhorar esse cenário, vamos adicionar orientação a objetos no nosso código, e também utilizar o padrão de projeto Command;

Esse padrão de projeto, como o próprio nome diz, trata-se de criar classes de comandos, que podem ser executados independentemente ou agrupadas, fornecendo a flexibilidade de serem alternadas em tempo de execução sem muitas mudanças; Cada classe Command implementa uma única interface, que contem um único método, como descrito a seguir:

arquivo: Command.php

<?php
interface Command {
     public function execute ( );
}
?>

Para não nos preocuparmos com os includes, utilizaremos a função __autoload, que tem por responsabilidade, carregar todas as classes que ainda não foram carregadas dentro do script atual; Para maiores informações, veja no site do PHP; Para podermos utiliza-la, o nome do arquivo deve ser padronizado com o nome da Classe. Sendo assim, definimos que o nome da classe sera o nome do arquivo, seguido da string ‘Command’;

Assim teremos:
Formulario.php = FormularioCommand
LerNoticia.php = LerNoticiaCommand
HomeNoticia.php = HomeNoticiaCommand

Como exemplo, vamos criar o Command para a ação de leitura:

LerNoticia.php

<?php
class LerNoticiaCommand implements Command {
     public function execute ( ) {
          $id_da_noticia = $_GET['id'];
          echo 'Lendo a noticia de id '.$id_da_noticia;
     }
}
?>

Simples não? Outra vantagem dessa abordagem é que, se algum curioso tentar acessar o arquivo LerNoticia.php diretamente, ele não irá ver nada, ja que a classe LerNoticiaCommand necessita ser invocada para ser executada;

Assim nosso Controller fica mais simplificado:

arquivo: noticia.php

<?php
 
//Incluindo a interface command, necessária para os command das Action
require_once 'command.php';
 
function __autoload($nomeClasse) {
     $nomeArquivo = substr($nomeClasse,0,-7).'.php';
     require_once $nomeArquivo;
}
 
switch($_ GET['action']) {
     case 'formulario': $command = new FormularioCommand();break;
     case 'ler' : $command = new LerNoticiaCommand();break;
     default : $command = new HomeNoticiaCommand();break;
}
 
$command->execute();
 
?>

O controller ja está bastante prático no momento. Para criarmos uma nova action, basta:

  1. Criar um arquivo com uma classe que implemente a interface Command;
  2. Adicioná-lo no switch para que ele seja encontrado.

Mas é necessário ainda umas mudanças na organização interna para adaptá-lo nos moldes da orientação a objetos, e um dos principios básico desta abordagem é que devemos separar o que muda do que é estático, e no nosso caso, a parte do código que está sempre em alteração, no período de desenvolvimento, é o switch que diz qual command vai ser executado em qual action; e para separar essa funcionalidade, vamos utilizar mais um padrão de projeto, chamado Factory, que é uma classe cujo papel é instanciar e retornar um Command para a execução, dependendo dos parâmetros informados pela URL. Mais detalhes sobre o padrão factory você pode obter aqui;

Vamos tambem padronizar o nome da factory para que o __autoload possa encontrá-lo, fazendo com que o nome da classe seja o caminho de diretórios dela, separados pelo caractere underline (’_'), além disso vamos separá-las em um diretório específico do nosso framework, que chamaremos de lib; e tambem separaremos as classes de Controller, adicionando-as em um diretório específo, chamado ‘command’;

Nossa Factory terá o seguinte formato:

arquivo: lib/Command/Factory.php

<?php
class Command_Factory {
     public function createCommand() {
          switch($_ GET['action']) {
               case 'formulario': return new FormularioCommand();
               case 'ler' : return new LerNoticiaCommand();
               default : return new HomeNoticiaCommand();
          }
     }
}
?>

e o nosso novo __autoload, juntamente com o novo controller, ficará assim

arquivo: noticia.php

<?php
 
function __autoload($nomeClasse) {
     //Se a classe terminar com Command, carrega o arquivo da maneira 'antiga';
     if (substr($nomeClasse,-7) === 'Command') {
          $nomeArquivo = 'command/'.substr($nomeClasse,0,-7).'.php';
     } else {
          $nomeArquivo = 'lib/'.implode('/',explode('_',$nomeClasse)).'.php';
     }
     require_once $nomeArquivo;
}
 
$factory = new Command_Factory();
$factory->createCommand()->execute();
 
?>

E se quisermos ter, alem das noticias, um sistema para exibição de galeria de fotos no site? teremos que criar outro Factory? outro Controller?
R.: Não! podemos mudar nossa factory para que possa adicionar mais tipos de ’subsistemas’ no framework;

Para tanto necessitaremos de parametro adicional na URL que será chamado de ‘module’; Nossa Factory terá que ler esses dois parâmetros para direcionar o fluxo de informações ao Command correto; Sendo assim:

arquivo: lib/Command/Factory.php

<?php
 
class Command_Factory {
     public function createCommand() {
          switch($_GET['module']) {
               case 'noticia':
                    switch($_ GET['action']) {
                         case 'formulario': return new FormularioCommand();
                         case 'ler' : return new LerNoticiaCommand();
                         default : return new HomeNoticiaCommand();
                    }
               case 'galeria':
                    switch($_ GET['action']) {
                         case 'vergaleria': return new VerGaleriaCommand();
                         case 'verfoto' : return new VerFotoCommand();
                         default : return new HomeGaleriaCommand();
                    }
               default: 'index': return new IndexCommand();
          }
     }
}
?>

Agora que o nosso controller não é mais específico para noticias, podemos renomea-lo para index.php, fazendo com que seja a pagina principal do sistema;

Poderiamos finalizar nosso controller por aqui, porém ele pode ser melhorado na parte da configuração dos Command, substituindo o switch por um arquivo de configuração (XML, YML, properties), ou podemos simplismente suprimir o switch, fazendo com que a factory encontre a classe Command conforme a estrutura de diretórios. Assim, vamos fazer com que a classe do Command esteja armazenada em um diretório com o nome do modulo a qual ela pertence;

Esse tipo de configuração é chamado de configuração por convenção, que é a que será utilizada no Controller que estamos desenvolvendo; e é preferível, pois a configuração das actions da-se na existência de um único arquivo, que é o próprio arquivo do Command; Vejamos como fica o Command_Factory com esse comportamento:

arquivo: /lib/Command/Factory.php

<?php
class Command_Factory {
     public function createCommand() {
          //Se o module não for informado, ele passa a ser considerado o index;
          $module = (isset($_GET['module'])) ? $_GET['module'] : 'index';
          //A mesma coisa para a action
          $action = (isset($_GET['action'])) ? $_GET['action'] : 'index';
 
          //Deixa a primeira letra em Maiusculo e o resto da string em minusculo
          //Conforme dica de André Caldas, foi adicionado o comando htmlspecialchars para evitar
          //SQL Injection
          $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 existir, carrega-o, caso contrário gera erro
          if (file_exists($fileName)) {
               require_once $fileName;
               //O nome da classe é a Action seguida da String Command
               $className = $action.'Command';
               //Carrega e retorna a Classe
               return new $className();
          } else {
               //Não vou me preocupar com o tratamento de erros agora, isso será abordado nos           proximos artigos
               die('Erro 404 - Página não encontrada');
          }
     }
}
?>

Tambem é necessário mudar a função __autoload, ja que o carregamento da classe do Command fica agora por responsabilidade da Factory.

arquivo: index.php

<?php
function __autoload($nomeClasse) {
require_once 'lib/'.implode('/',explode('_',$nomeClasse)).'.php';
}
 
$factory = new Command_Factory();
$factory->createCommand()->execute();
 
?>

Arvore de diretório final:

  • /
  • index.php
  • lib/
    • Command.php
    • Command/
      • Factory.php
  • Command/
    • Noticia/
      • Lernoticia.php
      • Index.php
      • Listar.php
    • Galeria/
      • Index.php
      • Vergaleria.php
      • Verfoto.php

Com esse recurso, a manutenção do código fica somente por conta dos Commands. Para adicionar um novo, basta apenas criar o arquivo com a classe implementando a interface Command e adicionar no diretório do módulo específico. Muito prático, e rápido; Com esses artifícios ja da para começar a usar o Controller, mas ainda temos muito a fazer;

Editado: Conforme dica de André Caldas, foi adicionado o comando htmlspecialchars no Factory, afim de evitar códigos maliciosos vindos via URL

Clique aqui para fazer o Download do código gerado nessa primeira parte;

31 responses so far, say something?

  1. Igor de Paula Says:

    Adorei o artigo, pois sempre fiz sites em dreamweaver, e to com um projeto que resolvi fazer em classes, mas depois resolvi fazer em mvc, e me chegou por um amigo esse seu blog….abraços

  2. skoolfobia Says:

    Muito bom. Esse tipo de artigo realmente vale a pena. Cheers

  3. Carlos Torre Says:

    Muito bom post, gostei muito dessa iniciativa, acredito que em breve o php irá evoluir e ser uma linguagem comercial, equiparando-se com Java e .Net, pra isso nos programadores devemos deixar e sermos preguiçosos e programar php de forma correta, como demostra esse post.
    Parabéns

  4. Lucas Fernando Amorim Says:

    MVC e Smarty estão sendo supervalorizados.

  5. Wryel Says:

    Excelente aritgo, não o li por completo, mas chegando em casa eu vo dar uma lida, e do meu feedveck por completo :D

  6. ezidio Says:

    Valew galera! Aguarde a próxima parte, onde será abordado o tratamento de erros encima dessa arquitetura.
    Abraço!

  7. André Gustavo Says:

    Pow cara, exatamente na semana que comecei a criar um framework para estudo fiquei sabendo deste artigo pelo BR-LINUX, só vou esperar o CONAPHP que vai ter uma palestra exatamente sobre isso pra por a mão na massa

    muito bom
    parabéns

  8. jfr Says:

    um otimo exemplo de boas praticas e programação
    com tecnicas que tornam o negocio mais funcional
    :)

  9. Anderson Says:

    Muito bom o artigo, explicou muito bem sobre o padrão Command.

  10. Ribamar FS Says:

    Rapaz, realmente importante.
    Como você disse, já temos muitos e bons frameworks, mas saber como fazer um deles e usando os bons padrões aí é outra coisa.
    Sempre procurei e tive vontade de criar o meu mas nunca encontrei um artigo assim como o seu.
    Tem mais, se eu conseguir fazer um, posso criar um pequeno e funcional que resolva muitos dos meus problemas sem as complicações dos grandes frameworks existentes.
    No mínimo eu saberei como funciona um bichinho destes.
    Muito obrigado e concordo com o colega que é importante melhorar o nível técnico dos programadores de PHP, para valorizar a linguagem.

  11. Ribamar FS Says:

    O link dos fontes está quebrado.

  12. ezidio Says:

    Link dos fontes corrigido…

  13. André Caldas Says:

    Sei que o objetivo do artigo é educacional… mas tá cheio de falhas de segurança.

    Nenhuma informação passada pelo usuário em uma URL é confiável. Não se deve fazer echo( “$id_da_noticia” ). Deve-se usar htmlspecialchars.

    Pior ainda é:
    $fileName = ‘command/’.$module.’/’.$action.’.php’;

    André Caldas.

  14. Thiago A. Says:

    Parabéns pelo artigo. Eu uso um framework parecido, que eu mesmo fui desenvolvendo, inclusive com um esquema de include automático para os modules, e condensando as rotinas de inserir/alterar/excluir, funcionalidades do banco de dados em geral e comandos SELECT nas classes, retornando-os em um array pra ficar legal a manipulação.

    A diferença é que condenso o controle de exibição da aplicação (as actions do module) em apenas um arquivo (que é incluido automaticamente, ele e a classe correspondente), e pra ir à ação desejada, no caso de manutenção, utilizo um simples CTRL + F… e claro, abuso da identação, então fica tudo bem legível e fácil de achar.

    Também gosto de serializar as classes correspondentes dos modules na Session, assim a aplicação lembra onde o usuário estava dentro de um module, ou qual pesquisa ele fez por último. E cadastro também os modules no banco de dados, assim limito a exibição das áreas válidas e implemento um sistema de permissões. Mas achei bem legal esse esquema, e como leva o padrão de orientação a objetos mais à risca e faz uma separação mais rigorosa, penso em adotá-lo.

    Abraços

  15. João F. Melo Says:

    Muito show mesmo, como “iniciante” em php ainda não conhecia como fazer frameworks, usa muitos arquivos a acabava ficando com um “site” enorme e tendo o trabalho de lembrar onde fica isso ou aquilo.

    Estarei acompanhando o artigo e se precisar de ajuda na tradução de algum doc pode contar comigo.

  16. Guilherme Garnier Says:

    Ótimo artigo, parabéns. Muito bom para aprender na prática como aplicar padrões de projeto.

  17. Igor de Paula Says:

    uma duvida nesse caso, aqui nessa funcao, eu poderia colocar todo o script da minha pagina? incluindo css, consulta ao banco…

    eis a funcao…

    public function execute ( ) {
    $id_da_noticia = $_GET['id'];
    echo ‘Lendo a noticia de id ‘.$id_da_noticia;
    }

  18. Everton Tavares Says:

    Igor… poder pode… mas não é a melhor solução para o problema; O ideal é separar a parte de banco de dados, a parte de codificação e a parte visual; Nos próximos artigos vamos estar abordando esse assunto;
    =)

  19. Igor Says:

    Estou ansioso para terminar a serie….

  20. KaizenWeb » Criando um FrameWork PHP do Zero Usando Padrões de Projeto (Parte 2) Says:

    [...] a abordagem que melhor se encaixa no FrameWork; Se você perdeu a primeira parte, pode ler ela clicando aqui; Como foi observado, a nossa factory gera um erro quando não encontra o command solicitado, e [...]

  21. Ribamar FS Says:

    Olá!

    Quando executo a primeira parte aparece o erro:

    Erro 404 - Página náo encontrada

    O que será que está havendo?

  22. Everton Tavares Says:

    Provavelmente porque não existe o Command Index/IndexCommand.php, que é o command que é executado caso não seja informado o module e a action;

  23. Kaléu P. D. Caminha » Blog Archive » Padrões de Projeto em PHP Says:

    [...] links: Padrão Adapter (exemplo 1 PHP); Construindo um framework PHP com Padrões de Projeto (parte 1, parte [...]

  24. Helio Says:

    Senhor, muito boa a série. Vou ler todas!!!!

    - Só 2 comentários e 2 dúvidas:
    1º - Tem 2 parágrafos duplicados no texto. “Poderíamos finalizar nosso come…”
    2º - Faltou o link para o Padrão Factory? Você está querendo dizer “Abstract Factory” ou “Factory Method” ou é algum outro padrão novo que não tá no GoF?
    3º - Cuidado com o que você chama de “padrões de projeto” e “padrões de design”. MVC é um padrão de projetos que utiliza vários “padrões de design” como o Command.

    Aprender padrões é MUITO bom… hehehe

  25. Everton Tavares Says:

    Realmente tinha um paragrafo duplicado… ja foi removido, obrigado!!
    =)

    O Padrão Factory a qual me refiro é o Factory Method; Em vários artigos o Factory Method é chamado apenas de factory, como vc pode conferir nesse link: http://java.sys-con.com/node/46239.
    Vale a pena ressaltar que não existe apenas os padrões catalogados pelo GoF, a Sun por exemplo tem seu próprio catálogo de Padrões que tem por objetivo complementar os padrões do GoF; Veja mais aqui: http://www.corej2eepatterns.com/Patterns2ndEd/index.htm

    Quanto ao terceiro comentário, o termo Design Patterns tem duas traduções: Padrões de Design e Padrões de Projeto; Portanto, os dois termos significam a mesma coisa! =)

  26. Everton Tavares Says:

    Tambem para constar, alguns autores também chamam o Factory Method como Simple Factory (Fábrica Simples);

  27. ian Says:

    Muito bom!
    Prático, Simples e Funcional.

  28. Leonardo Says:

    Everton, primeiramente parabens pelo artigo! Muito explicativo e prático!

    Agora vou levantar um probleminha que estou enfrentando e uma possível solução:
    Estou usando o seu exemplo, e a IDE Eclipse + DW para fazer as paginas.
    No Eclipse, tudo perfeito! Ele mapeia as classes, disponibiliza a documentação, etc, etc.
    Agora no DW que está o problema: Um colega que faz a parte visual do site (ele não programa) usando o DW e eu temos enfrentado problemas quanto ao ‘endereçamento’ dos elementos. Usamos endereçamento relativo para imagens, links, etc, e na hora de publicar o problema ocorre, pois as páginas pensam que estão na pasta command/*module*, mas na verdade estão no root, devido ao include do Factory no index.php.
    Bom, eu estava pensando em usar header(location:xxx) no lugar do include. O problema do design estaria resolvido, mas as páginas não seriam mais classes.
    O que você acha disso?

    Agradeço a atenção!

  29. roberto Says:

    Olá parabens pelo artigo, estou tendo um problema na parte de acessar os Commands, qual seria a sintaxe para acessa-los?
    ja tem tentei localhost/[site]/[controller]/[action] e só da pagina nao encontrada. só funciona quando localhost/[site] q vai para a pagina Inicial.

  30. Leonardo Says:

    Roberto, verifique o nome das pastas.
    A pasta ‘command’ é diferente da pasta ‘Command’.
    Pelo que me lembro, tive que fazer algumas correções desse tipo no projeto, mas no mais está funcionando.

  31. Ricardo Says:

    Amigo so um problema no arquivo Factory.php a variavel $module = index, mas nao se tem a pasta index isso não levaria a um erro? pois ele vai tentar acessar algo que nao existe.

Deixe um comentário