Back-End

2 dez, 2014

Aplicando Post/Redirect/Get Pattern na prática com PHP

100 visualizações
Publicidade

Quantas vezes, navegando aleatoriamente na internet, não recebemos um alerta do tipo: “Tem certeza que deseja reenviar as informações para o servidor?” ou até mesmo uma página de erro dizendo que você deve recarregar a página para confirmar o reenvio do formulário?

Isso acontece porque tentamos acessar mais de uma vez uma URL que utiliza verbo HTTP como POST, por exemplo. É um problema comum quando usamos estes verbos HTTP que não sejam GET em uma URI e esta mesma URI fica responsável por processar a requisição e ainda exibir uma página de retorno para o cliente. Poder ser um simples cadastro, uma página de autenticação, processos que necessitam de um workflow ou divisões de um processo maior em pequenos steps, como por exemplo, ao fazer o processo de finalização de uma compra, onde comumente separamos os diversos passos:

  1. Identificação do cliente – onde o usuário deva logar ou se cadastrar antes de ir para o próximo passo;
  2. Endereço de entrega;
  3. Dados de pagamento;
  4. Confirmação do pedido;
  5. Página de sucesso com confirmação de compra realizada.

E o PRG Pattern (Post/Redirect/Get) é utilizado justamente para resolvermos este tipo de problema. Este padrão consiste basicamente em separar realmente o processamento das informações que chegam através do formulário, da página de resposta que será entregue ao cliente.

Devemos tomar muito cuidado, pois isso não é uma questão apenas de ter que confirmar o reenvio da requisição. Devemos prestar muita atenção neste tipo de reenvio de informação, principalmente com etapas delicadas do processo, pois um simples momento de desatenção, pode fazer com que o cliente seja cobrado duas vezes, por exemplo.

Gerando o erro de reenvio de informações

Vamos construir um simples exemplo usando Silex com engine de template Twig para reproduzir o problema e posteriormente arrumá-lo.

Teremos basicamente três rotas inicialmente:

GET /signup

POST /signup_confirmation

POST /success

Nosso arquivo web/index.php do Silex fica assim:

<?php
require_once __DIR__.'/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;

$app = new Silex\Application();
// Adds Twig service provider
$app->register(new Silex\Provider\TwigServiceProvider(), array(
    'twig.path' => __DIR__.'/../views',
));

// Routes

$app->run();

O próximo passo seria adicionar a implementação das rotas no espaço que deixei reservado no arquivo acima. Então, teríamos as seguintes implementações:

// Routes
// GET /signup
$app->get('/signup', function () use ($app) {
    return $app['twig']->render('signup.twig');
});

// POST /signup_confirmation
$app->post('/signup_confirmation', function (Request $request) use ($app) {
    return $app['twig']->render(
        'signup_confirmation.twig',
        [
            'username' => $request->get('username'),
            'password' => md5($request->get('password'))
        ]
    );
});

// POST /success
$app->post('/success', function (Request $request) use ($app) {
    return $app['twig']->render(
        'success.twig',
        ['username' => $request->get('username')]
    );
});

E nossas views:

views/signup.twig

<h1>Signup</h1>
<form action='/signup_confirmation' method='post'>
    <fieldset>
        <label for='username'>Username</label>
        <input id='username' name='username' type='text' />
        <label for='password'>Password</label>
        <input id='password' name='password' type='password' />
        <button>Save</button>
    </fieldset>
</form>

views/signup_confirmation.twig

<h1>Signup Confirmation</h1>
<form action='/success' method='post'>
    <fieldset>
        <legend>Do you really want complete your registration?</legend>
        <input name='username' type='hidden' value='{{ username }}' />
        <input name='password' type='hidden' value='{{ password }}' />
        <button>Confirm</button>
    </fieldset>
</form>

views/success.twig

<h1>Success</h1>
<p>Congratulations, {{ username }}, you were registered successfully!</p>

Pronto! Agora temos o problema reproduzido. Se acessarmos /signup, submetermos o formulário e tentarmos carregar as páginas seguintes mais de uma vez, receberemos um alerta, variando de browser para browser, mas que deve ser algo como no Firefox, por exemplo: “Para mostrar esta página, o Firefox deve enviar informações que irão repetir a ação (como uma busca ou confirmação de compra) que foi executada anterioremente”.

Agora vamos ver como podemos usar o PRG Pattern para fugirmos deste problema.

Fugindo do problema de reenvio de formulário

Primeiramente, como se trata de uma sequência de Post/Redirect/Get, fica claro que as rotas que nós temos não são capazes de resolver nosso problema. Precisamos criar pelo menos mais duas rotas, que serão responsáveis por processar o envio do formulário de cadastro e outra para o processamento da confirmação do cadastro. O que deixaria nosso cenário assim:

GET /signup

POST /signup

GET /signup_confirmation

POST /signup_confirmation

GET /success

Deixando um pouco a discussão sobre melhores práticas, só para facilitar nosso exemplo, vou utilizar Session para trafegar os dados de cadastro entre as páginas. Então, registramos mais um service provider ao web/index.php

$app->register(new Silex\Provider\SessionServiceProvider());

Depois, podemos alterar nosso arquivo web/index.php para atender estes novos requisitos, adicionando duas novas rotas – que basicamente irão receber os dados passados pelos dois formulários e guardar as informações do cadastro na sessão, além de fazer o redirecionamento (usando o código HTTP/1.1 303 See Other) para a rota correta que deve exibir a página de resposta:

// POST /signup
$app->post('/signup', function (Request $request) use ($app) {
    $app['session']->set('username', $request->get('username'));
    $app['session']->set('password', md5($request->get('password')));

    return $app->redirect('/signup_confirmation', 303);
});

// POST /signup_confirmation
$app->post('/signup_confirmation', function () use ($app) {
    $app['session']->set('confirmed', true);

    return $app->redirect('/success', 303);
});

Feito isso, precisaremos adaptar as rotas que já existiam para atender também esse novo formato. Alteramos as rotas /signup_confirmation e /success para GET e alteramos seu comportamento para ler as informações da sessão ao invés da variável $request.

$app->get('/signup_confirmation', function () use ($app) {
    return $app['twig']->render(
        'signup_confirmation.twig',
        [
            'username' => $app['session']->get('username'),
            'password' => $app['session']->get('password')
        ]
    );
});

$app->get('/success', function () use ($app) {
    return $app['twig']->render(
        'success.twig',
        [
            'username'  => $app['session']->get('username')
        ]
    );
});

Com isso, podemos navegar tranquilamente entre as páginas e recarregá-las sem o perigo de reenviar as informações novamente para serem processadas. Claro, algumas validações extras poderiam ser feitas em um caso de uso real para este processo, por exemplo, um cliente não poderá acessar a página de sucesso diretamente sem ter passado pelo processo de cadastro.

Para garantir isso, podemos fazer o seguinte na nossa página /success:

$app->get('/success', function (Request $request) use ($app) {
    if (! $app['session']->get('confirmed')) {
        return $app->redirect('/signup');
    }

    return $app['twig']->render(
        'success.twig',
        [
            'username'  => $app['session']->get('username')
        ]
    );
});

Assim temos um exemplo da aplicação funcionando perfeitamente rodando Post/Redirect/Get.

O código fonte deste exemplo pode ser visto no GitHub: PRG Sample.

Conclusão

Mais do que evitar o incômodo de receber alertas todas as vezes que tentarmos acessar uma página que envie dados na requisição, devemos levar em consideração quais impactos esta requisição duplicada pode causar para o usuário. Em muitos casos, esta requisição duplicada pode ser só perda de tempo, sem nenhum mal efetivamente causado a ele. Por exemplo, não há nada de perigoso um formulário de login ser enviado mais de uma vez. Isso não trará impacto algum para o usuário, apenas uma requisição desnecessária. Mas por outro lado, uma requisição duplicada na hora de fechar uma compra, talvez (leia-se: com certeza) seja uma dor de cabeça enorme. Por isso, devemos ficar sempre atentos às etapas que envolvam processos e informações delicadas.

E cá entre nós, não é nem um pouco difícil aplicar Post/Redirect/Get, vai?!