Criando seu próprio framework PHP

O componente HttpFoundation

/**
* Este é o segundo de uma série de artigos traduzidos e adaptados
* a partir dos originais: "Create your own PHP Framework -
* http://symfony.com/doc/current/create_framework/index.html
* by Fabien Potencier.
*/


Antes de mergulhar de cabeça no processo de criação de um framework, vamos dar um passo para trás e analisar porque você iria querer utilizar um framework ao invés de conjunto de scripts PHP como se fazia antigamente. E ainda por que utilizar um framework é uma boa ideia, mesmo para pequenos projetos e também por que criar seu framework utilizando os componentes do Symfony é melhor do que fazê-lo do zero.

Nota:
Não vamos falar sobre os benefícios óbvios e tradicionais de se utilizar um framework quando trabalhando em grandes aplicações com pouco mais do que dois ou três desenvolvedores; a Internet já possui uma ampla gama de materiais sobre este tópico.

Até mesmo uma "aplicação" simples como a foi desenvolvida no artigo anterior possui alguns problemas:

Primeiro, se o parâmetro name não estiver definido na query string da URL, você vai receber um PHP Warning. Portanto, vamos corrigir isso:

De qualquer forma esta aplicação não é segura. Dá pra acreditar? Mesmo esse simples trecho de código PHP está vulnerável a uma das mais difundidas falhas de segurança da Internet, o XSS ou Cross-Site Scripting. Eis uma versão mais segura:

Nota:
Você deve ter notado que deixar seu código mais seguro através da função htmlspecialchars é tedioso e propenso a erros. Essa é uma das razões pela qual usar um template engine como o Twig, onde o scaping está ativo por padrão, pode ser uma boa ideia (além disso fazer o scaping explicitamente é menos penoso, basta utilizar o filtro e).

Observe que se quisermos evitar as mensagens de warning/notice do PHP e deixar nosso código mais seguro, aquele simples código que foi escrito no início não será tão simples assim.

Além da segurança, esse código não é facil de testar. Mesmo que não haja muito para se testar. Escrever testes para este simples trecho de código não é natural e parece horrível, pelo menos pra mim. Eis uma tentativa de escrever um teste unitário para o código acima:

Nota:
Se nossa aplicações fosse um pouco maior, seríamos capazes de encontrar ainda mais problemas. Caso esteja curioso sobre esses problemas, leia Symfony versus Flat PHP.

Se até agora você ainda não estiver convencido de que a questão da segurança e dos testes são dois bons motivos para você parar des escrever código PHP do jeito antigo e adotar um framework, pode parar de ler meus artigos e voltar a trabalhar no código que você estava trabalhando.

Nota:
É claro que adotar um framework irá te proporcionar mais do que somente segurança e testabilidade, no entanto é mais importante ter em mente que o framework que você escolher deve permitir que você escreva códigos melhores e de forma mais rápida.

Orientação a Objetos com o componente HttpFoundation

Programar para a web é interagir com o protocolo HTTP. Dessa forma nosso framework deve estar focado na especificação HTTP.

A especificação HTTP descreve como um cliente (como um browser), interage com um servidor (nossa aplicação, através de um servidor web). O diálogo entre o cliente e o servidor é especificado por um conjunto de mensagens bem definidas, requisições e respostas (requests/responses).

O cliente envia uma requisição ao servidor que, baseado nessa requisição, envia uma resposta ao cliente.

No PHP a requisição é representada por variáveis globais ($_GET, $_POST, $_FILE, $_COOKIE, $_SESSION) e a resposta é gerada por funções (echo, header, setcookie, …).

O primeiro passo para melhorar nosso código é utilizar uma abordagem Orientada Objetos, este é o principal objetivo do componente HttpFoundation: substituir as variáveis globais padrões do PHP e suas funções por uma camada Orientada a Objetos.

Para utilizar este componente, temos que adicioná-lo como uma dependência do nosso framework:

composer require symfony/http-foundation

Rodando este comando o composer irá baixar e instalar o componente HttpFoundation na pasta vendor/. Além disso os arquivos composer.json e composer.lock serão criados ou atualizados para refletir este novo requisito do nosso framework.

Class Autoloading
Quando você instala uma dependência pelo composer, ele gera ou atualiza o vendor/autoload.php. Este script possibilita que qualquer classe seja automaticamente carregada (autoload). Sem este recurso você teria que incluir o arquivo onde a classe foi definida antes de poder utilizá-la. Mas graças ao PSR-4, podemos confiar no composer e no PHP para fazer o duro por nós.

Agora vamos reescrever nossa aplicação tirando proveito das classesRequest e Response:

O método createFromGlobals() cria um objeto Request a partir no conteúdo atual das variáveis globais do PHP.

O método send() envia o objeto Response de volta ao cliente (ele primeiro produz os cabeçalhos HTTP seguidos pelo corpo da resposta).

Dica:
Antes de chamar o método send() nós deveríamos ter adicionado uma chamada ao método prepare(): $response->prepare($request); para garantir que nossa resposta esteja de acordo com a especificação HTTP. Por exemplo, se tivéssemos acessado a página através do método HEAD, o prepare() teria removido o conteúdo da resposta.

A principal diferença desse novo código é que você tem total controle das mensagens HTTP. Você pode criar a requisição que quiser e enviar a resposta quando achar apropriado.

Nota:
Nós não definimos explicitamente o cabeçalho Content-Type na resposta porque o charset padrão do objeto Response já é UTF-8.

Através da classe Request, você tem na ponta dos dedos qualquer informação da requisição HTTP, graças a uma simples e agradável API:

Também podemos simular uma requisição:

$request = Request::create('/index.php?name=Fabien');

Com a classe Response, você pode ajustar facilmente a resposta:

Dica:
Para debugar uma resposta, faça o cast dela para string. Será devolvido a representação HTTP da resposta (cabeçalhos e corpo).

Por último, mas não menos importante, essa classe, assim como qualquer outra classe no código do Symfony, foi auditada por uma empresa independente em busca de falhas de segurança. Além disso, sendo um projeto Open Source, significa que diversos programadores do mundo todo também leram o código e já corrigiram possíveis falhas de segurança. Quando foi a última vez que você contratou uma equipe de profissionais para auditar o código fonte do seu próprio framework?

Até mesmo um procedimento simples como pegar o endereço IP do cliente pode ser inseguro:

if ($myIp === $_SERVER['REMOTE_ADDR']) {
// o cliente é conhecido, então daremos a ele mais privilégios
}

Isso funciona sem nenhum problema até você colocar um proxy reverso na frente dos seus servidores web. Fazendo isso você terá que modificar seu código para que ele funcione em seu ambiente de desenvolvimento, onde você provavelmente não tem um proxy reverso, e também no seu ambiente de produção:

if ($myIp === $_SERVER['HTTP_X_FORWARDED_FOR'] || 
$myIp === $_SERVER['REMOTE_ADDR']) {
// o cliente é conhecido, então daremos a ele mais privilégios
}

O método Request::getClientIp() proporcionaria o comportamento esperado desde o início, cobrindo até os casos em que você tenha uma cadeia de proxies reversos:

$request = Request::createFromGlobals();

if ($myIp === $request->getClientIp()) {
// o cliente é conhecido, então daremos a ele mais privilégios
}

Além disso ainda há um benefício extra: esse método é seguro por padrão. O que isso significa? Você não pode confiar no valor de $_SERVER['HTTP_X_FORWARDED_FOR'] pois ele pode ser manipulado pelo usuário quando não há um proxy na frente dos seus servidores web. Por isso, se você estiver utilizando aquele código que faz referência direta ao array $_SERVER em produção e sem ter um proxy, seria trivial abusar do seu sistema. No entanto este não é o caso com o método getClientIp(), onde você deve explicitamente determinar quais são os IPs dos seus proxies reversos chamando o métodosetTrustedProxies():

Request::setTrustedProxies(array('10.0.0.1'));

if ($myIp === $request->getClientIp()) {
// o cliente é conhecido, então daremos a ele mais privilégios
}

O método getClientIp() funcionará de forma segura em qualquer circunstância. Você pode utilizá-lo em todos os seus projetos, qualquer que sejam as configurações, ele vai se comportar de forma correta e confiável.

Esse é só mais um dos benefícios de se utilizar um framework. Se você tivesse que escrever um framework por conta própria, você teria que pensar em todos esses cenários por conta própria. Então por que não utilizar uma tecnologia que já funciona?

Nota:
Se você quiser aprender mais sobre o componente HttpFoundation, você pode dar uma olhada na sua API ou ler sua documentação.

Acredite ou não, nós já temos nosso primeiro framework. Você pode parar por aqui se você quiser. Somente o componente HttpFoundation do Symfony já possibilita que você escreva códigos melhores e mais testáveis. Além disso ele possibilita que você programe mais rápido, já que muitos dos problemas do dia-a-dia já foram resolvidos para você.

Projetos como Drupal e Laravel adotaram o componente HttpFoundation; se funciona para eles, provavelmente vai funcionar para você. Não reinvente a roda.

Quase esqueci de mencionar outro benefício: usar o componente HttpFoundation é o ponto de partida para uma melhor interoperabilidade entre todos os frameworks e aplicações que o utilizam, como Symfony, Drupal 8, phpBB 3, ezPublish 5, Laravel, Silex e outros.


Você também pode acompanhar o desenvolvimento através deste repositório no GitHub.