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

by Everton Emilio Tavares in Sem categoria

· 12 Comments

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;

Next →

12 Comments

  1. [...] 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 [...]

  2. henrique disse:

    ola amigo
    deu erro para baixar o zip aqui

  3. henrique disse:

    Obrigado Everton pelo material , vou dar uma estudada , depois
    adicione no seu blog , o google friend connect ai eu acompanho o
    seu novos material

    um abraço e tenha uma boa semana :)

  4. henrique disse:

    Opa bom dia Everton
    adicione embaixo do friend connect o ‘twitter tambem”
    tem um plugin no wordpress que tudo que vc adicionar aqui
    ja vai direito la para o twitter
    ai eu pego tambem la qualquer novidade do blog

    um abração e bom final de semana para ti :)
    @kakarotodev

  5. Erick disse:

    BOa noite,baixei o tuto e rodei mas nãosai da index.Como q seria a URL para noticia por exeplo. Tente de varias maneiras
    http://localhost/frame/artigo_framework_parte_1/command/Noticia_Listar
    http://localhost/frame/artigo_framework_parte_1/Noticia_Listar
    http://localhost/frame/artigo_framework_parte_1/command/Noticia/Listar
    http://localhost/frame/artigo_framework_parte_1/Noticia/Listar
    Mas nenhuma entrou na pagina de noticia, so deu erro de ..was not found on this server.

  6. Erick disse:

    Consegui, a partir da parte 3, deve ser pq os outros 2 naum tinha o htacess.
    Uma pergunta, como eu faria em caso de um cadastro de cliente, por exemplo

    • Everton Emilio Tavares disse:

      Não entendi muito bem sua pergunta, mas acredito que em cadastro de cliente você deve ter um command de listagem, um para o formulário de edição, outro para receber os dados e gravar no banco, e assim por diante.
      =)

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

  8. Angelo disse:

    Cara, muito bom o artigo, to lendo e está tudo muito claro!
    Parabéns.

Leave a Comment