Streams do PHP

Artigo que apresenta os streams de PHP e os strems padrão (stdout, stderr e stdin).

Introdução

Streams são canais por onde passam fluxos de dados (bytes). É comum se falar em "streaming de vídeo", que é um canal por onde é trafegado um fluxo de dados do vídeo em pedaços (chunks).

Segundo a definição do php.net, streams são recursos por onde se lê ou escreve dados de forma linear, e que também permite se posicionar em uma posição arbritária do stream.

Neste artigo veremos como a linguagem PHP trabalha com streams e também como manipulá-los no terminal de comandos.

Stream Wrappers do PHP

No PHP, são oferecidos "stream wrappers", que são formas de generalizar as operações sobre um stream. Os streams são criados a partir de referências, na forma de string, que se assemelham a uma URI, pois possuem um schema e um valor álvo. Veja alguns exemplos:

  • file:///home/rubens/arquivo.txt Stream para um arquivo físico.
  • https://rubsphp.blogspot.com/2020/08/streams-do-php.html Stream para um arquivo remoto, em outro servidor.
  • php://stdout Stream para a saída de dados padrão.
  • php://stderr Stream para a saída de erros padrão.
  • php://stdin Stream para a entrada de dados padrão.

Ao utilizar funções como fopen, file_get_contents, file_put_contents, é possível informar a referência para um stream. Caso o schema não seja informado, o PHP assume que o álvo é um arquivo físico do servidor. Os dois blocos abaixo são equivalentes

$h = fopen('/home/rubens/arquivo.txt', 'r');

$h = fopen('file:///home/rubens/arquivo.txt', 'r');

Porém, nem todos os tipos de streams são suportados a todo momento. Por exemplo, para abrir um stream de uma URL (arquivo em um servidor externo), é necessário que o PHP esteja configurado com a opção allow_url_fopen ativada.

Um outro detalhe é que essas funções que aceitam streams costumam aceitar um parâmetro para especificar opções do contexto do stream. Por exemplo, ao abrir um stream de uma URL (arquivo remoto), é utilizado o protocolo HTTP para trafegar os dados. Portanto, você pode escolher os detalhes sobre a requisição HTTP que será feita ao servidor. Esse contexto é construído com a função stream_context_create, que deve ter um array de opções e um array de parâmetros. Veja um exemplo abaixo de como realizar uma requisição HTTP POST com file_get_contents e obter o conteúdo de resposta (que poderia ser feito utilizando curl ou alguma lib como o Guzzle):

$context = stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => [
            'Content-Type: application/json',
        ],
        'content' => json_encode(['oi' => 'tenta']),
        'protocol_version' => 1.1,
    ]
]);

$body = file_get_contents('http://algum.servidor/algum/path', false, $context);
var_dump($http_response_header, $body);

No exemplo acima, a variável $body terá o valor textual da resposta HTTP. Já a variável reservada do PHP $http_response_header terá um array com os cabeçalhos HTTP da resposta.

Tendo uma visão geral sobre os streams, você deve consultar a documentação do PHP para conhecer os tipos de streams wrappers suportados no PHP e os contextos e parâmetros aceitos para cada stream wrapper.

Os streams de entrada/saída padrão

Os streams de entrada e saída padrão são stdin, stdout e stderr. Um script de PHP rodando na sapi FPM (fpm-fcgi, que é uma sapi para lidar com requisições HTTP) pode escrever dados no stream da saída padrão (stdout) ou de erro dessa forma:

$out = fopen('php://stdout', 'w');
fwrite($out, 'Exemplo de texto na saida padrao');

$err = fopen('php://stderr', 'w');
fwrite($err, 'Exemplo de texto na saida de erro');

O exemplo acima irá produzir a seguinte mensagem no log do fpm:

[27-Aug-2020 00:16:49] WARNING: [pool www] child 83 said into stdout: "Exemplo de texto na saida padraoExemplo de texto na saida de erro"

Ou seja, escrever algum conteúdo na saída padrão é diferente de chamar o comando echo ou print, já que o stdout/stderr dizem respeito às saídas padrão do processo que está rodando o php fpm (processo que fica em loop infinito aguardando requisições HTTP). Já o echo envia para um buffer que irá compor o body da resposta HTTP para a requisição recebida, e que foi bastante discutido no artigo Output buffer e os mistérios do echo.

Já na SAPI CLI, ou seja, quando se chama um script php pela linha de comando (terminal), a saída padrão é idêntica ao stream onde são despachadas as mensagens enviadas pelo echo, ou qualquer outra função que "imprima" algo. Além disso, a sapi CLI oferece algumas constantes que são file handlers abertos para os streams de entrada/saída padrão: STDOUT, STDERR e STDIN. Veja um exemplo com algumas formas de se exibir algo no terminal ao executar um script PHP via linha de comando:

echo "oi\n"; // Imprime "oi" no terminal

fwrite(STDOUT, "oi\n"); // Também imprime "oi" no terminal

$out = fopen('php://stdout', 'w');
fwrite($out, "oi\n"); // Também imprime "oi" no terminal

Ou seja, a constante STDOUT é apenas um handler já criado do stream php://stdout, assim como STDERR é um handler de php://stderr e STDIN é um handler de php://stdin.

Diferença do STDOUT para o STDERR

Caso você não conheça a diferença entre o STDOUT e o STDERR, basta saber que o primeiro é destinado para a saída de texto padrão, enquanto o segundo é destinado para a saída de erros padrão.

Ao criar um script PHP para ser rodado na linha de comando, você consegue transferir o conteúdo da saída padrão para algum arquivo, assim como transferir o conteúdo da saída de erros para outro arquivo.

Supondo que teste.php seja um arquivo com esse conteúdo:

<?php

fwrite(STDOUT, 'oi');
fwrite(STDERR, 'tenta');

Caso você rode esse comando em um terminal bash (no Linux) desta forma:

$ php teste.php > out.txt 2> err.txt

Então você está dizendo que quer que o conteúdo impresso na saída padrão do comando (o texto "oi") vá para o arquivo out.txt, enquanto o conteúdo impresso na saída de erro (o texto "tenta") vá para o arquivo err.txt. Além disso, a execução desse comando no terminal não vai imprimir nada no terminal, pois você pediu para que o texto fosse para os arquivos. Isso pode ser muito útil para ser usado, por exemplo, em scripts usados para cron. Também é possível fazer só a saída padrão ir para um arquivo, ou apenas a saída de erro ir pra um arquivo, ou então ambas as saídas irem para o mesmo arquivo.

Criando um stream wrapper personalizado

Além dos stream wrappers oferecidos pelo PHP, é possível criar seu próprio stream wrapper. Para fazer isso, basta implementar uma classe que implemente os métodos previstos para funcionar como um stream wrapper. No link a seguir, você pode conferir os métodos que você pode implementar: assinatura dos métodos de uma classe de stream. E no link a seguir, você pode ver um exemplo de implementação de classe de stream.

Após implementar sua classe, é preciso registrá-la sob algum nome de protocolo (schema), através da função stream_wrapper_register, onde você deve passar o nome do seu protocolo personalizado e o nome da classe que irá lidar com esse protocolo. Ou seja, se você registrar um protocolo com o nome eita, você pode usar a função fopen informando a referência pro seu stream da segunte forma:

$h = fopen('eita://alguma-coisa', 'r');

Note que o "alguma-coisa" será algo que você irá lidar na sua classe. Não precisa ser exatamente um caminho de arquivo. Pode ser uma string qualquer e você é que estabelecerá o significado dela.

Isso quer dizer que você pode, por exemplo, criar uma classe para lidar com arquivos remotos armazenados em um S3 da AWS. Neste caso, você poderia criar um arquivo remotamente de forma tão simples quanto essa:

file_put_contents('s3://meu-bucket/minha-chave', 'conteudo do arquivo');

Simples, né? Mas espere! Já que você chegou até aqui no artigo, segura essa dica: essa classe já existe! A SDK oficial da AWS para PHP já possui um recurso que permite registrar o stream wrapper s3://. Nem tudo que se faz com um arquivo local é possível com esse wrapper oferecido pela SDK, mas é uma forma bem prática de lidar com os arquivos remotos na nuvem da Amazon. Para mais detalhes sobre uso e limitações, veja esse artigo com as instruções para usar o stream wrapper do S3 para PHP.

4 comentários

Anônimo disse...

Bom artigo! Acho que vc digitou errado no exemplo "fwrite(STDOUT, 'tenta');". Não deveria ser STDERR?