Output Buffer do PHP e os mistérios do echo (parte 2)

Artigo que apresenta alguns conceitos mais avançados do Output Buffer do PHP e algumas de suas aplicações práticas.

Introdução

No artigo anterior, vimos uma introdução sobre o Output Buffer do PHP, com seus conceitos básicos e uma aplicação prática do recurso. Neste artigo, veremos a continuação do assunto, nos aprofundando em conceitos mais complexos e algumas aplicações práticas mais avançadas.

O Output Buffer como Callback

No exemplo mostrado no post anterior, vimos como abrir e fechar um bloco para ser controlado com o output buffer, que são as funções ob_start e ob_get_clean (ou obter o conteúdo com ob_get_content e depois fechar o bloco com ob_end_clean).

Porém, a função ob_start pode funcionar com uma função de callback, que é executada ao encerrar o buffer ou ao realizar o flush. Este evento pode se dar de forma explícita, com a chamada de alguma função (como ob_flush ou ob_end_clean) ou, simplesmente, quando o script terminar a execução e o buffer é automaticamente fechado e jogado para flush.

A função de callback recebe por parâmetro o conteúdo do buffer (uma string) e deve retornar esta string com as modificações desejadas ou retornar false para que o conteúdo original seja usado sem modificações.

A linguagem PHP já oferece algumas funções que podem ser usadas como callback para ob_start, veja algumas:

  • ob_ gzhandler - Para comprimir o conteúdo antes de enviar para o cliente.
  • ob_iconv_handler - Para trocar a codificação do documento antes de enviar para o cliente (usando a extensão "iconv").
  • mb_output_handler - Para trocar a codificação do documento antes de enviar para o cliente (usando a extensão "multibyte string").

Podemos usar uma função pronta do PHP, mas também podemos fazer nossa própria função. Por exemplo, vamos fazer uma função que troca a letra "a" pela letra "b" em todo o documento, antes de enviá-lo para o cliente.

<?php

ob_start('trocar_a_por_b');

echo 'atenção para a alteração';
exit(0);

function trocar_a_por_b($documento) {
    return str_replace('a', 'b', $documento);
}

Note que não foi preciso fechar o controle de buffer. Quando chega-se na instrução exit, o PHP sabe que precisa chamar a função callback para o buffer armazenado.

Para enviar um documento comprimido para o cliente, basta usar o callback de ob_gzhandler:

<?php

ob_start('ob_gzhandler');

echo 'Conteúdo do documento';
exit(0);

Você pode notar que o documento foi trafegado de forma compactada usando, por exemplo, o plugin firebug do Firefox. Com ele, você vê que na aba "Rede", o cabeçalho de resposta do servidor veio com a diretiva "Content-Encoding: gzip".

E para trocar a codificação de um arquivo, vamos ver como fazer usando a extensão "iconv":

<?php

iconv_set_encoding('internal_encoding', 'UTF-8');
iconv_set_encoding('input_encoding', 'UTF-8');
iconv_set_encoding('output_encoding', 'ISO-8859-1');

ob_start('ob_iconv_handler');

header('Content-type: text/html; charset=UTF-8');
echo 'Mudando a codificação';
exit(0);

Neste caso o meu arquivo foi gerado em UTF-8, mas o documento enviado para o cliente será convertido para ISO-8859-1. Observe que é necessário configurar estas codificações antes de usar o controle de buffer.

Observação: a própria função de callback modifica o cabeçalho HTTP "Content-type" para ajustar a codificação. Porém, se você especificar o charset usando meta tag, a função não é capaz de mudar.


O funcionamento do flush

O flush é um mecanismo para descarregar um conteúdo do buffer e aplicar o tratamento especial, caso necessário. Por exemplo, queremos trocar a letra "a" por "b" em todo documento, mas queremos fazer isso de tempos em tempos ao invés de fazer tudo de uma vez. Veja como ficaria o exemplo inicial:

ob_start('trocar_a_por_b');

echo 'atenção '; 

ob_flush();

echo 'para a ';

ob_flush();

echo 'alteração';

exit(0);

function trocar_a_por_b($documento) {
    return str_replace('a', 'b', $documento);
}

Neste caso, a cada vez que chamamos a função ob_flush, estamos dizendo para o PHP processar o que está em buffer e já descarregar para o pacote HTTP que será entregue ao cliente. Isso pode ser útil caso a sua função precise de muita memória para realizar a operação.

Uma outra forma de executar a função de callback aos poucos, seria especificar um tamanho mínimo para processamento. Assim, quando o buffer atinge o tamanho definido, a função de callback é chamada para processar parte do documento. Isso é feito na própria chamada da função ob_start. Por exemplo, vamos chamar a função de callback quando o buffer atingir o tamanho mínimo de 15 bytes:

<?php
ob_start('trocar_a_por_b', 15);

echo 'atenção ';
echo 'para a ';
echo 'alteração';
exit(0);

function trocar_a_por_b($documento) {
    return str_replace('a', 'b', $documento);
}

Ao realizar o primeiro echo o tamanho do buffer ainda não chegou a 15, mas depois do segundo echo, o buffer passa deste tamanho, então faz o processamento do buffer, o descarrega para o pacote HTTP e limpa o buffer. Depois do terceiro echo o buffer ainda não chega a ter 15 bytes, mas como chega ao final do script (chamada do exit), então o buffer é processado com o "resto" do documento.


Controle de Output Buffer em Cascata

O controle de output buffer em cascata consiste em um determinado controle fazer seu processamento sobre o resultado do processamento de outro controle. Ou seja, são definidos níveis de processamento e um nível mais externo precisa esperar pelo resultado . Por exemplo, podemos querer converter a codificação de UTF-8 para ISO-8859-1 e, depois, compactar o documento com o gzip:

<?php
iconv_set_encoding('internal_encoding', 'UTF-8');
iconv_set_encoding('input_encoding', 'UTF-8');
iconv_set_encoding('output_encoding', 'ISO-8859-1');

ob_start('ob_gzhandler');

ob_start('ob_iconv_handler');

header('Content-type: text/html; charset=UTF-8');
echo 'Mudando a codificação';
exit(0);

Observe que o controle de compactação com gzip foi chamado antes do controle de conversão de UTF-8 para ISO-8859-1. Isso significa que ele está no nível mais externo ou, em outras palavras, o controle de conversão de codificação está "dentro" do controle de compressão. Talvez se incluirmos algumas chaves e indentação, a o funcionamento fique um pouco mais claro:


<?php
iconv_set_encoding('internal_encoding', 'UTF-8');
iconv_set_encoding('input_encoding', 'UTF-8');
iconv_set_encoding('output_encoding', 'ISO-8859-1');

ob_start('ob_gzhandler');
{
    ob_start('ob_iconv_handler');
    {
        header('Content-type: text/html; charset=UTF-8');
        echo 'Mudando a codificação';
    }
}
exit(0);

Com isso, você pode incluir quantos níveis de controle de output buffer desejar. Mas tome cuidado pois sua aplicação pode ter a performance comprometida se você incluir muitos controles em cascata, além de eles poderem gastar mais memória que o esperado.

Para saber em que nível de controle de output você está em determinado momento, você pode usar a função ob_get_level, sendo que o nível "0" (zero) é o mais abrangente (processado por último).


Configurando o nível de controle de output buffer global

A linguagem PHP também oferece uma forma de se definir um nível global de controle de output buffer. Isso é definido via diretiva de configuração. Por exemplo, se configurarmos a diretiva output_handler do php.ini com o valor "ob_gzhandler", significa que todos os scripts PHP terão um nível de controle de output buffer global automaticamente, que fará a compressão do documento gerado. Ou seja, corresponde a incluir a linha abaixo automaticamente em todos os scripts PHP:

ob_start('ob_gzhandler');

No caso do callback "ob_iconv_handler", seria necessário configurar as diretivas de configuração "iconv.input_encoding", "iconv.internal_encoding" e "iconv.output_encoding", no próprio arquivo php.ini, que faz presumir que todos arquivos PHP terão a mesma codificação.

A diretiva output_buffering serve para definir se haverá um controle de buffer global antes de despachar o conteúdo para o pacote HTTP. Ela pode assumir os valores:

  • on: para habilitar o buffer de forma ilimitada (primeiro todo conteúdo vai para o buffer, depois é gerado o pacote HTTP de resposta, depois o pacote é enviado para o cliente.
  • off: para não ter controle de buffer global (toda vez que você chama a função flush, o conteúdo é enviado para o pacote HTTP e enviado o pedaço para o cliente.
  • um número de bytes: para habilitar o buffer que é despachado ao atingir o número de bytes especificado.

Observação: a diretiva output_buffering não pode ser definida em php-cli.


Transferindo o documento em partes

No post anterior, vimos que um pacote HTTP pode ser enviado de uma vez só para o cliente ou ser enviado em pequenos pacotinhos, que são unidos no cliente para formar o documento completo (veja o cabeçalho HTTP "Transfer-Encoding").

Agora que conhecemos os principais conceitos acerca do output buffer, podemos conhecer como enviar um documento aos poucos.

Primeiramente, não é necessário especificar o cabeçalho HTTP "Transfer-Encoding: chunked" pois isso será feito automaticamente pelo PHP.

Basicamente, para transferir o documento em partes, precisamos estar no nível mais global do output, ou seja, não estar usando nenhum output buffer. Neste ponto, se utilizarmos a função flush, o conteúdo desde o último flush será enviado para o cliente em um pacotinho de dados. Vamos ver um exemplo:

// Considerando que a diretiva "output_buffering" esteja desabilitada (off)

echo 'a';
flush();
sleep(5);

echo 'b';
flush();
sleep(5);

echo 'c';
exit(0);

Note que este script só vai funcionar se o output buffer não estiver definido no php.ini, ou seja, a diretiva output_buffering deve valer "off". Ou então, se estiver habilitada, o script precisa incluir a linha ob_end_clean(); logo no início do script, pois ele irá cancelar o output buffer mais global.

Se acessarmos este script no firefox, por exemplo, veremos o navegador exibir a letra "a", depois de 5 segundos exibir a letra "b" e depois de 5 segundos exibir a letra "c". O sleep foi colocado apenas para ficar notável que o conteúdo é recebido aos poucos.

Caso o buffer esteja habilitado, o documento será enviado em pedaços, mas eles serão enviados todos de uma só vez (quando todo o documento estiver pronto).

Se o buffer estiver habilitado, ainda há uma solução para enviar o documento em partes e as partes serem enviadas assim que chamarmos o flush. Basta chamarmos ob_flush para despachar o conteúdo do buffer e depois flush para despachar o conteúdo para o cliente:

// Considerando que a diretiva "output_buffering" esteja habilitada

echo 'a';
ob_flush();
flush();
sleep(5);

echo 'b';
ob_flush();
flush();
sleep(5);

echo 'c';
exit(0);

2 comentários

Anônimo disse...

ÓTIMO ARTIGO, está e uma parte muito importante do desenvolvimento web, que muitas vezes passa despercebida..