PHP

Aplicações em tempo real com PHP usando WebSockets

Neste artigo veremos uma introdução a WebSockets com a criação de um servidor em PHP e usando o navegador do usuário como cliente.

Antes, entretanto, recomendo que você assista o vídeo O que são WebSockets? gravado pelo Akira Hanashiro (aqui da TreinaWeb) que explica de uma forma bem sucinta.

E, se você tiver interesse, também recomendo a leitura do artigo Uma introdução a TCP, UDP e Sockets para se ter a base do que é um socket na arquitetura TCP/IP.

Desenvolvedor PHP Júnior
Formação: Desenvolvedor PHP Júnior
Nesta formação você aprenderá todos os fundamentos necessário para iniciar do modo correto com a linguagem PHP, uma das mais utilizadas no mercado. Além dos conceitos de base, você também conhecerá as características e a sintaxe da linguagem de forma prática.
CONHEÇA A FORMAÇÃO

Ok, mas o que é um WebSocket?

WebSocket é um protocolo que permite a criação de um canal de comunicação cliente-servidor com transmissão bidirecional onde ambos os lados (cliente e servidor) podem transmitir dados simultaneamente. WebSocket veio para suprir as deficiências do protocolo Http para esse objetivo. O protocolo Http, que por sinal, é unidirecional (a transmissão ocorre só de uma ponta para a outra), onde o cliente envia a requisição e o servidor retorna a resposta, finalizando ali a conexão. Ou seja, se o cliente envia 5 requisições Http para o servidor, 5 conexões TCP independentes são abertas, enquanto que com WebSocket uma única conexão TCP é aberta e ela fica disponível para troca de dados a qualquer momento (uma conexão persistente, até que um dos lados decida fechá-la).

É muito importante pontuar, também, que Socket e WebSocket são coisas diferentes. Um WebSocket é um protocolo que roda em cima de sockets TCP, enquanto que um socket é uma abstração, uma porta lógica de comunicação entre duas pontas numa rede.

Benefícios de usar WebSocket em detrimento a Http

Se existe a necessidade de uma conexão permanecer aberta por um longo tempo para uma troca de dados constante, WebSocket é uma ótima escolha. Uma conexão Http é relativamente pesada, ela transmite não só os dados, mas também cabeçalhos. Além disso, possui um curto tempo de vida e não mantém estado (stateless).

Já uma conexão WebSocket, depois do handshake entre o cliente e o servidor, ela permanece aberta até que uma das partes decida fechá-la. E foco dela é na transmissão dos dados, cabeçalhos não são transmitidos pra lá e pra cá.

Então, em resumo, os benefícios em relação ao Http são: baixa latência, conexão persistente e full-duplex (transmissão bidirecional).

Principais casos de uso para WebSockets

Home Brokers (onde cotações são atualizadas a todo instante), feeds de redes sociais, aplicativos de bate-papo, ferramentas de edição colaborativa, jogos multi-player etc.

Criando o primeiro servidor

Neste artigo usaremos a library Ratchet que nos permite criar um servidor WebSocket assíncrono, e para isso ela utiliza o event loop do ReactPHP. Outra opção seria utilizarmos o servidor de WebSocket do Swoole. Mas o bom de usar o ReactPHP é que não precisamos instalar nenhuma extensão específica na nossa instalação do PHP.

Crie uma pasta chamada websocket-demo e dentro dela crie o arquivo composer.json:

{
    "require": {
        "cboden/ratchet": "^0.4.1"
    }
}

Instale as dependências:

$ composer install

Agora, crie um arquivo server.php com a seguinte implementação:

<?php

require './vendor/autoload.php';

use Ratchet\Server\EchoServer;

$app = new Ratchet\App('localhost', 9980);
$app->route('/echo', new EchoServer, ['*']);
$app->run();

Esse é o servidor mais primitivo que existe, é um “Echo Server”, ele envia de volta tudo o que o cliente manda pra ele. Nem tivemos a necessidade de implementá-lo, pois ele já vem junto com o Ratchet.

Por fim, crie um arquivo index.html com a seguinte implementação:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket EchoServer</title>
</head>
<body>
<label for="input">Digite aqui: </label>
<input id="input" type="text" placeholder="Digite aqui"/>

<div id="response"></div>
<script>
    let input = document.getElementById('input');
    let response = document.getElementById('response');
    const socket = new WebSocket('ws://localhost:9980/echo');

    // Ao estabelecer a conexão enviamos uma mensagem pro servidor
    socket.addEventListener('open', function () {
        socket.send('Conexão estabelecida.');
    });

    // Callback disparado sempre que o servidor retornar uma mensagem
    socket.addEventListener('message', function (event) {
        response.insertAdjacentHTML('beforeend', "<p><b>Servidor diz: </b>" + event.data + "</p>");
    });

    input.addEventListener('keyup', function (event) {
        if (event.keyCode === 13) {
            socket.send(this.value);
            this.value = '';
        }
    });
</script>
</body>
</html>

Para testar, primeiro de tudo temos que ter o servidor rodando. Na raiz do projeto execute:

$ php server.php

Vai iniciar o servidor na porta 9980. Por fim, basta executar o arquivo index.html e interagir escrevendo mensagens e apertando enter para enviá-las. Tudo o que o servidor receber de input, ele retornará de volta para o cliente.

O objeto WebSocket é nativo e presente em todos os navegadores modernos. Iniciamos o servidor de forma “mágica” usando o Ratchet, mas a realidade é que por debaixo dos panos ele precisa abstrair algumas importantes coisas como o handshake inicial, as trocas das mensagens (Data Frames) que são criptografadas etc. Toda a dinâmica do funcionamento de um servidor de Websocket pode ser lida nesse documento: Escrevendo um servidor WebSocket.

Agora vamos criar o nosso próprio wrapper, nossa própria implementação de um servidor. Primeiro, na raiz do projeto, crie uma pasta chamada src. Em seguida, altere o composer.json para:

{
    "require": {
        "cboden/ratchet": "^0.4.1"
    },
    "autoload": {
        "psr-4": {
            "Chat\\": "src"
        }
    }
}

Agora crie um arquivo ChatServer.php dentro da pasta src com a seguinte implementação:

<?php

namespace Chat;

use Exception;
use SplObjectStorage;
use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface;

final class ChatServer implements MessageComponentInterface
{
    private $clients;

    public function __construct()
    {
        $this->clients = new SplObjectStorage();
    }

    public function onOpen(ConnectionInterface $conn): void
    {
        $this->clients->attach($conn);
    }

    public function onMessage(ConnectionInterface $from, $msg): void
    {
        foreach ($this->clients as $client) {
            $client->send($msg);
        }
    }

    public function onClose(ConnectionInterface $conn): void
    {
        $this->clients->detach($conn);
    }

    public function onError(ConnectionInterface $conn, Exception $exception): void
    {
        $conn->close();
    }
}

Estamos implementando a interface MessageComponentInterface. A diferença desse servidor pro EchoServer, é que neste estamos guardando as conexões que são estabelecidas e, quando uma mensagem é recebida, enviamos ela de volta para toda as conexões abertas (que é o que o fazemos no método onMessage).

Para testar a implementação, crie um arquivo chat.html no projeto:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket Simple Chat</title>
</head>
<body>
<p>
    <label for="nome">Seu nome: </label>
    <input id="nome" type="text" placeholder="Seu nome"/>
</p>
<p>
    <label for="input">Sua mensagem: </label>
    <input id="input" type="text" placeholder="Sua mensagem"/>
</p>
<hr>
<div id="chat"></div>
<script>
    let chat = document.getElementById('chat');
    let input = document.getElementById('input');
    const nome = document.getElementById('nome');
    const socket = new WebSocket('ws://localhost:9990/chat');

    // Ao receber mensagens do servidor
    socket.addEventListener('message', function (event) {
        // Deserializamos o objeto
        const data = JSON.parse(event.data);
        // Escrevemos no DOM
        chat.insertAdjacentHTML('beforeend', "<p><b>" + data.nome + " diz: </b>" + data.mensagem + "</p>");
    });

    // Ao enviar uma mensagem
    input.addEventListener('keyup', function (event) {
        if (event.keyCode === 13) {
            // Objeto com os dados que serão trafegados
            const data = {
                nome: nome.value,
                mensagem: this.value,
            };

            // Serializamos o objeto para json
            socket.send(JSON.stringify(data));

            this.value = '';
        }
    });
</script>
</body>
</html>

Antes de iniciar o servidor é importante executar o dump-autoload do Composer, para que a classe ChatServer seja reconhecida:

$ composer dump-autoload

Por fim, inicie o servidor:

$ php chat.php

Você pode testar abrindo duas instâncias do chat e usando nomes diferentes:

Os exemplos completos você pode ver nesse repositório: https://github.com/KennedyTedesco/websocket-demo

Só o navegador pode ser cliente do meu servidor?

Não, qualquer outra aplicação pode ser cliente de um servidor WebSocket. Você pode, por exemplo, criar uma aplicação cliente em PHP usando a library Pawl.

Palavras finais

O Ratchet simplifica e muito a criação de servidores de WebSocket em PHP, não obstante, ele também suporta sub-protocolos como o Wamp. Em um próximo artigo pretendo abordar um exemplo utilizando-o.

Até a próxima!

Laminas: O futuro do Zend Framework

Se você acompanha os principais frameworks em PHP já deve ter ouvido falar sobre o Laminas. Para quem não sabe, esse é o novo nome do já conhecido Zend Framework, um dos mais antigos frameworks em PHP com um foco no mercado de soluções enterprise. Antes de falar sobre essa mudança de Laminas para Zend, vamos começar falando sobre a história do Zend Framework.

História do Zend Framework

Quem programa com PHP faz algum tempo já deve ter usado ou pelo menos ter ouvido falar sobre o Zend Framework. Para uma introdução sobre o que é o Zend Framework leia esse artigo no link. Lançado em março de 2006, o Zend Framework 1 foi um dos primeiros frameworks PHP de todos os tempos, e até o momento a versão com maior tempo de suporte de todos os frameworks em PHP, totalizando um pouco mais de 10 anos de suporte.

De 2006 pra cá, o Zend Framework acompanhou e contribuiu com a evolução do ecossistema do PHP, seja suportando novas versões da linguagem, adicionando suporte ao Composer e melhorando seu comportamento interno. Em 2012 foi lançado o Zend Framework 2, que iniciou o processo de modernização do framework em componentes menores e independentes, cada qual com seu próprio ciclo de vida além de outras melhorias.

Essa quebra do core do framework permitiu que projetos derivados pudessem ser criados, como o Apigility, um pacote de ferramentas especializado no desenvolvimento de API e também o Expressive, um micro framework modular baseado na PSR-7 que te permite escolher quais componentes você quer utilizar, suportando tanto componentes do Zend Framework como outros componentes mantidos pela comunidade.

Zend Expressive - Microframework PHP
Curso de Zend Expressive - Microframework PHP
CONHEÇA O CURSO

Além da ajuda da comunidade, tudo isso aconteceu com o suporte da Zend, empresa fundada em 1999 por Andi Gutmans e Zeev Suraski depois que eles redesenharam o até então PHP-FI, escrito originalmente pelo Rasmus Lerdorf. Em 1998 eles lançaram o Zend Engine, que se tornou o PHP 4 e desde então tornou a base da linguagem que utilizamos hoje em dia (claro, com muitas melhorias e evoluções se comparado com aquela época).

O fim do Zend Framework

Não podemos negar a importância histórica que a Zend trouxe para o ecossistema do PHP, tanto com o Zend Framework como com outras iniciativas da empresa. Porém, em Outubro de 2015, a Zend Technologies foi adquirida pela Rogue Wave Software. Tudo ia bem com o suporte ao Zend Framework. Foi nessa época inclusive que foi lançado o Zend Framework 3, junto com Expressive, consolidando a visão de um framework baseados em componentes individuais e com um ciclo de vida próprio.

Mas isso mudou quando a Rogue Wave Software foi adquirida pela Perforce em Janeiro de 2019. Nessa época, a companhia decidiu rever seu portfólio de projetos e infelizmente continuar suportando o Zend Framework não estava em seus planos. Porém o projeto em si não seria abandonado, em Abril do mesmo ano foi anunciado a transição do Zend Framework para a Linux Foundation, sob o nome de The Laminas Project.

A Linux Foundation é uma organização sem fins lucrativos especializada em projetos open source. Além de ajudar a manter e padronizar o Linux, outros projetos que fazem parte da Linux Foundation incluem o Kubernetes, Jenkins, GraphQL e até NodeJS. Isso da até uma segurança maior para nós, desenvolvedores e também para as empresas, pois temos uma garantia ainda maior na continuidade da evolução do projeto, agora gerido por um modelo de governança que já foi provado que funciona.

O início do Laminas

Após esse anúncio, iniciou o processo de migração do Zend Framework para o Laminas. Por questões de copyright, acredito eu, o nome Zend precisou ser abandonado em favor do Laminas. Com isso, todos os componentes que antes estavam sob o guarda-chuva da Zend passaram para a organização do Laminas dentro do GitHub.

Apesar de ser uma grande mudança, que pode envolver alterar as dependências dos seus pacotes no Composer, essa fase de transição (que já começou e ainda está acontecendo) tem como objetivo garantir uma migração mais suave o possível. O mesmo líder de projeto responsável por manter o Zend Framework, o Matthew Weier O’Phinney, manteve sua função no projeto, agora como líder do Laminas.

Para auxiliar nessa migração, você pode utilizar uma ferramenta de migração em seu projeto que será responsável por atualizar todas as dependências do Zend gerenciadas pelo Composer para a dependência correspondente no Laminas, entregando assim uma maneira de migrar projetos inteiros em alguns poucos comandos, incluindo também suporte para migrar projetos antes desenvolvidos com Expressive e Apigility.

Falando sobre Expressive e Apigility, eles também foram renomeados para Laminas Mezzio e Laminas API Tools, respectivamente, aproveitando assim o momento de alteração do projeto principal.

Subprojetos do Laminas

Desenvolvedor PHP Sênior
Formação: Desenvolvedor PHP Sênior
Nesta formação você aprenderá aspectos avançados da linguagem PHP e de seu ecossistema, conhecimentos essenciais para se tornar um desenvolvedor diferenciado, e com isso, você terá conhecimento para desenvolver aplicações PHP usando as práticas mais modernas do mercado.
CONHEÇA A FORMAÇÃO

Status atual da migração para o Laminas

Nesse momento, já estamos em uma fase bem avançada da migração para o Laminas. Os componentes do Zend já foram migrados para os novos repositórios na organização do Laminas e as referências dos componentes no Composer já podem ser utilizados.

Inclusive, ao tentar utilizar um componente do Zend, você receberá uma mensagem informando que o mesmo foi abandonado e que você deve atualizar suas dependências para utilizar o componente do Laminas:

Componentes do Zend Framework descontinuados

Composer - Gerenciador de dependências para PHP
Curso de Composer - Gerenciador de dependências para PHP
CONHEÇA O CURSO

Já podemos acessar a documentação dos componentes do Laminas em um novo domínio, bem como do Mezzio e do Laminas MVC. Ainda não está disponível a documentação do Laminas API Tools, mas num futuro próximo já deve estar disponível.

Estou começando um novo projeto, ou quero migrar um projeto existente, o que eu faço?

Então, se você estiver pensando em construir uma nova aplicação com Zend Framework, recomendo já começar utilizando o Laminas MVC ou o Mezzio. Ambos já tem tutoriais na documentação mostrando como criar projetos do zero com essas ferramentas.

Para você que já tem um projeto desenvolvido com Zend Framework, consulte a documentação sobre a migração para o Laminas. Com poucos comandos pode ser possível você migrar toda sua aplicação de forma bastante automatizada e intuitíva.

Pessoalmente estou bastante animado com essa migração, principalmente pelas novas possibilidades que estão sendo adicionadas ao Laminas, como o suporte oficial ao Swoole para desenvolvimento assíncrono no PHP.

Para conhecer mais sobre o Swoole, configura os artigos aqui do blog da TreinaWeb sobre introdução ao Swoole e trabalhando com corrotinas. Ter uma integração oficial com o Swoole no Laminas Mezzio é algo bem promissor! 😀

Corrotinas e código assíncrono em PHP usando Generators

Nos últimos artigos eu tenho escrito sobre programação assíncrona em PHP com ReactPHP e Swoole. O Swoole tem seu mecanismo interno próprio de corrotinas, mas o que muita gente não sabe é que podemos trabalhar com corrotinas usando PHP puro, através de generators.

Eu achei que fosse interessante escrever sobre isso até mesmo como uma forma de apresentar às pessoas o conceito de generators e também como uma forma de instigá-las a buscar mais sobre o tema.

A ideia desse artigo é construir um simples (muito simples) scheduler de corrotinas bem primitivo e didático. Em outros artigos eu escrevi de forma mais abrangente sobre generators, código síncrono, assíncrono, corrotinas etc, conceitos esses que serão importantes você ter para entender o funcionamento do nosso exemplo aqui. Por esse motivo, eu recomendo que você leia esses artigos:

Opcionalmente, se você tiver interesse, também recomendo a leitura deste artigo:

Esse artigo que você está lendo é, de certa forma, uma ramificação deste:

Portanto, o artigo Generators no PHP é realmente uma leitura necessária para entender os códigos que desenvolveremos aqui.

Desenvolvedor PHP Júnior
Formação: Desenvolvedor PHP Júnior
Nesta formação você aprenderá todos os fundamentos necessário para iniciar do modo correto com a linguagem PHP, uma das mais utilizadas no mercado. Além dos conceitos de base, você também conhecerá as características e a sintaxe da linguagem de forma prática.
CONHEÇA A FORMAÇÃO

Gênesis

Sabemos que generators são como se fossem funções que podem ser interrompidas e resumidas a qualquer momento. Também sabemos que assincronismo é sobre fluxo de execução. Captando essas duas ideias centrais, podemos chegar na seguinte conclusão lógica: se um generator pode ser interrompido para que outro seja executado, eu posso usar isso para manipular o fluxo de uma execução e então atingir uma modelagem de código assíncrono.

Primeiro vamos ver um exemplo de código síncrono:

<?php

declare(strict_types=1);

$booksTask = static function () {
    for ($i = 1; $i <= 4; ++$i) {
        echo "Book $i\n";
    }
};

$moviesTask = static function () {
    for ($i = 1; $i <= 8; ++$i) {
        echo "Movie $i\n";
    }
};

$booksTask();
$moviesTask();

O resultado dessa execução:

Book 1
Book 2
Book 3
Book 4
Movie 1
Movie 2
Movie 3
Movie 4
Movie 5
Movie 6
Movie 7
Movie 8

A execução é síncrona, linear e previsível. Temos duas tarefas, mas a segunda ($moviesTask) só terá oportunidade de desempenhar seu trabalho depois que a primeira ($booksTask) terminar tudo o que tem para ser feito.

Podemos transformar esse exemplo em um código de modelo assíncrono usando generators e trabalhando com a ideia de que cada task é uma corrotina. Para isso, dois princípios são importantíssimos:

  • Uma Task (tarefa) será apenas um decorator de um generator;
  • Um Scheduler cuidará da fila de tarefas e da execução delas;

O exemplo pode ser encontrado no GitHub: https://github.com/KennedyTedesco/coroutines-php

Uma tarefa será representada pela classe Task:

<?php

declare(strict_types=1);

namespace Coral;

use Generator;

final class Task
{
    private $coroutine;
    protected $firstYield = true;

    public function __construct(Generator $coroutine)
    {
        $this->coroutine = $coroutine;
    }

    public function run(): void
    {
        if ($this->firstYield) {
            $this->firstYield = false;
            $this->coroutine->current();
        } else {
            $this->coroutine->next();
        }
    }

    public function finished(): bool
    {
        return ! $this->coroutine->valid();
    }
}

A tarefa é apenas um decorator de um generator.

E o Scheduler será representado pela classe de seu próprio nome:

<?php

declare(strict_types=1);

namespace Coral;

use SplQueue;

final class Scheduler
{
    private $tasks;

    public function __construct()
    {
        $this->tasks = new SplQueue();
    }

    public function schedule(Task $task): void
    {
        $this->tasks->enqueue($task);
    }

    public function handle(): void
    {
        while (! $this->tasks->isEmpty()) {
            /** @var Task $task */
            $task = $this->tasks->dequeue();

            $task->run();
            if (! $task->finished()) {
                $this->schedule($task);
            }
        }
    }
}

O Scheduler mantém uma fila de tarefas a serem executadas e possui um método público schedule() para adicionar tarefas nessa fila. O método handle() itera nas tarefas executando-as. Antes de entrarmos em mais detalhes, ao executar o exemplo:

<?php

declare(strict_types=1);

require 'vendor/autoload.php';

use Coral\Task;
use Coral\Scheduler;

$scheduler = new Scheduler();

$booksTask = static function () {
    for ($i = 1; $i <= 4; ++$i) {
        echo "Book $i\n";

        yield;
    }
};

$moviesTask = static function () {
    for ($i = 1; $i <= 8; ++$i) {
        echo "Movie $i\n";

        yield;
    }
};

$scheduler->schedule(new Task($booksTask()));
$scheduler->schedule(new Task($moviesTask()));

$scheduler->handle();

Nota: As duas tarefas retornam um generator, que depois é passado para o construtor de Task.

Temos o seguinte resultado:

Book 1
Movie 1
Book 2
Movie 2
Book 3
Movie 3
Book 4
Movie 4
Movie 5
Movie 6
Movie 7
Movie 8

Diferentemente do exemplo síncrono mostrado anteriormente, neste temos a alternância da execução das tarefas, no sentido de que são colaborativas, uma abre espaço para que a outra também tenha oportunidade de ser executada. Isso acontece pois o Scheduler executa a tarefa, o valor corrente dela é impresso, nisso ela volta novamente para a fila do Scheduler para ser executada novamente em outro momento. As tarefas sempre voltam para a fila enquanto ainda tiverem valores a serem processados:

$task->run();
if (! $task->finished()) {
    $this->schedule($task);
}

Essa é uma estratégia para mantê-las em sua essência colaborativas, ou seja, a tarefa abre mão do seu tempo de execução para que outra tarefa também tenha oportunidade.

Considerações finais

Este foi um simples exemplo de como podemos ter uma operação assíncrona utilizando generators. E aqui nem estamos nos referindo a multiplexing de I/O, mas poderíamos usar esse mesmo conceito de generators e implementar I/O não bloqueante (assíncrono) usando algum padrão como o Reactor, usado pelo ReactPHP ou algo mais “simples” usando diretamente a função stream_select(). Inclusive, O Nikita Popov (desenvolvedor do core do PHP) escreveu exatamente sobre isso no artigo Cooperative multitasking using coroutines (in PHP!), que por sinal, é a principal referência desse artigo aqui. Recomendo essa leitura pois ele também fez com que o generator se comunicasse com outros generators e com o Scheduler (que é a realmente essência de uma corrotina), numa espécie de canal de comunicação, o que torna as coisas ainda mais poderosas. O framework assíncrono Amp faz um uso bem intensivo de generators, também vale a pena testá-lo.

Ah, não poderia deixar de pontuar novamente: se você tem interesse por programação assíncrona com PHP, vale a pena a leitura desses artigos:

Até a próxima!

O que é o Slim Framework?

Neste artigo falaremos sobre o Slim Framework, micro-framework escrito em PHP para, principalmente, criação de APIs RESTful.

Open source, multiplataforma, lançado no final de 2010, o Slim ganhou destaque para desenvolvedores que precisam de um Framework para criar serviços REST.

Caso você não saiba o que é um Micro-Framework…

Em um artigo anterior, expliquei “o que é um Micro-Framework”.

Um Micro-Framework são Frameworks modularizados que possuem uma estrutura inicial muito mais simples quando comparado a um Framework convencional.

Pense em um Micro-Framework como uma peça de lego. Inicialmente, um projeto criado com o micro-framework possui apenas o básico para funcionar, (normalmente, sistema de rotas), porém, ao decorrer do projeto, podem haver necessidades para utilização de outros recursos como, conexão de banco de dados, sistemas de templates, envio de email, etc. A partir desta necessidade, novas bibliotecas são “encaixadas” no projeto, como uma estrutura de lego.

Basicamente, como podemos ver na imagem abaixo:

E APIs RESTful?

Aqui no blog também possuímos um artigo sobre os conceitos que cercam as APIs RESTful.

Basicamente, REST é um acrônimo para REpresentational State Transfer, ou seja, Transferência de Representação de Estado. O REST é, no final das contas, um estilo arquitetural que podemos utilizar ou não em nossas aplicações.

A ideia do REST é utilizar de maneira mais eficiente e em sua plenitude as características do protocolo HTTP, principalmente no que diz respeito à semântica do protocolo. O resultado disso ao final das contas é, além da utilização mais “correta” do protocolo, um trânsito de informações mais eficiente e, por consequência, mais rápido.

Slim - Microframework PHP
Curso de Slim - Microframework PHP
CONHEÇA O CURSO

De volta ao Slim…

É um Micro-Framework bastante leve e prático, e como dito anteriormente, possui como principal característica a implementação RESTful, facilitando a vida do desenvolvedor na criação de APIs de pequeno ou médio porte de maneira organizada.

Com o Slim é permitido criar aplicações seguras e robustas de maneira mais simples e com uma baixa complexidade se comparado a demais Frameworks do mercado.

Como principais características, podemos citar:

  • Baixa necessidade de configuração;
  • Por não possuir um padrão de desenvolvimento definido, há liberdade para definir uma estrutura personalizada para o projeto;
  • Facilidade na integração de aplicações externas ao projeto;
  • Possui suporte nativo a rotas HTTP;
  • Possui suporte a injeção de dependências;
  • Possui uma comunidade ativa, entre outros.

Seu download pode ser feito em seu site, que possui toda sua documentação e suporte. Desta forma, é uma excelente opção de tecnologia na criação de serviços REST.

Docker Desktop no Windows rodando com WSL 2

Com a inclusão de um kernel Linux completo dentro do Windows graças ao Windows Subsystem for Linux (WSL) 2, ferramentas que antes não podiam ser utilizadas no WSL por questões de compatibilidade agora podem ser executadas sem problemas. Uma dessas ferramentas muito utilizadas pelos desenvolvedores é o Docker.

No Windows 10 é possível utilizar o Docker Desktop para ter uma experiência integrada com o Docker, se aproximando muito de um sistema Linux. Entretanto, existem algumas limitações que com a atual versão que serão tratadas com a nova integração entre o Docker Desktop com o WSL 2.

Docker Desktop

O Docker Desktop é uma solução para executar containers Linux nos sistemas Windows e macOS. Ele permite que você tenha uma experiência semelhante a uma distribuição Linux, integrando o sistema de arquivos do sistemas e a rede com a máquina virtual que executa o Docker.

Essa máquina virtual é gerenciada pelo Docker Desktop através do hypervisor de virtualização do sistema, no caso do Windows o Hyper-V e do macOS o hyperkit. A execução dos containers em si acontece nessa máquina virtual, como podemos ver no diagrama abaixo:

Arquitetura do Docker Desktop

O Docker Desktop está disponível faz alguns anos, sendo a forma recomendada de executar Docker nos sistemas Windows e macOS, sendo possível com ele até executar um cluster de Kubernetes na sua máquina local.

Restrições do Docker Desktop

Apesar de ser a forma recomendada de executar o Docker nesses sistemas, um grande fator que impacta no seu uso é a performance em operações de leitura e escrita. Por executar dentro de uma máquina virtual, existe um delay para sincronizar os arquivos que estão no sistema operacional com a máquina virtual.

Isso pode ser um problema para aplicações que exigem constante leitura em disco, como é o caso de linguagens interpretadas, como o PHP. Além disso, mais especificamente com o Docker Desktop for Windows, é preciso ter o Hyper-V habilitado, que só é incluído com o Windows 10 Pro.

Então se você utiliza o Windows 10 Home ou trabalha com uma linguagem interpretada, sua experiência com o Docker no Windows pode não ser a melhor possível. Felizmente, isso vai mudar com o Docker com WSL 2.

Docker com WSL 2

O WSL 2 traz para o Windows o kernel completo do Linux através de uma máquina virtual moderna e com uma performance de disco próxima a uma máquina rodando Linux. Você pode ler mais sobre o WSL 2 nesse artigo aqui.

Com esse anúncio, o WSL 2 se tornou uma opção mais interessante para executar o Docker nos sistemas Windows. Além do ganho de performance, o WSL 2 será compatível com o Windows 10 Home, tornando o Docker Desktop disponível para um maior número de usuários.

O suporte do Docker Desktop utilizando o WSL 2 ainda está em preview, mas você já pode testá-lo se tiver executando o Windows 10 Insider. A previsão de lançamento do WSL 2 será no release 2003, e se espera que o suporte na versão estável do Docker chegue nessa época também.

O Docker Desktop irá incluir o suporte ao WSL 2 e utilizá-lo sempre que possível, mantendo o comportamento atual de usar uma máquina virtual no Hyper-V como um fallback para versões do Windows 10 que ainda não suportam WSL 2. Com isso, teremos todas as vantagens presentes no Docker Desktop, mas com uma melhor performance e suporte para o Windows 10 Home (o que hoje não é possível por requerer o Hyper-V).

Internamente, o Docker Desktop provisiona duas distribuições Linux na sua máquina, uma contendo o daemon do Docker e outra é utilizada para armazenar dados como os containers e as imagens que você utilizará. A comunicação entre essas distribuições acontece através de sockets, tanto entre o Windows como com a distribuição que você utiliza com o WSL 2 no seu dia-a-dia. Mais detalhes dessa implementação podem ser encontrados nesse post do blog de engenharia do Docker.

Arquitetura do Docker com WSL 2

Mas será que tudo isso vale a pena? Além do tempo de carregamento para iniciar o Docker Desktop, que reduziu para poucos segundos, a performance com o disco melhorou bastante. Podemos comparar esses números com o benchmark abaixo.

Teste de performance com disco

Para esse teste, vamos comparar a diferença entre o Docker Desktop utilizando Hyper-V e o WSL 2. Ambos os testes foram executados no mesmo ambiente, com a mesma máquina e quantidade de recursos disponíveis.

Para esse teste utilizei a aplicação de demonstração do Symfony junto com uma configuração de containers para Symfony mantida por um de seus core contributor. O código desses dois projetos juntos se encontra no GitHub da TreinaWeb também.

Depois de clonar o projeto e ter instalado o Docker, basta executar o comando docker-compose up para iniciar o projeto. O tempo a ser medido como comparação é para o primeiro carregamento da página inicial do projeto. Graças ao Symfony Profiler temos diretamente no navegador utilitários que auxiliam o desenvolvimento da nossa aplicação, incluindo o tempo de carregamento da mesma.

No primeiro teste, abri a página inicial do projeto utilizando o Docker Desktop com Hyper-V e essa página levou 2681ms para ser carregada:

Tempo de carregamento Docker Hyper-V

Com Docker Desktop usando WSL 2, a mesma página levou apenas 249ms!

Tempo de carregamento Docker WSL 2

Tudo isso considerando o primeiro carregamento do projeto. Foi preciso baixar todas as dependências do Composer, o container do PHP não tinha nenhum OpCache, o Symfony não chegou a fazer nenhum tipo de otimização, como compilar as views do Twig, ler as rotas da aplicação presentes nas anotations. Se considerarmos essas otimizações que farão efeito nas requests subsequentes, esse tempo cai para 20ms, comparado com um Linux rodando nativamente:

Tempo de carregamento Docker WSL 2 - carregamento subsequente

Como começar a utilizar

O Docker Desktop com WSL 2 está disponível na versão edge do Docker Desktop e pode ser baixada aqui. Por enquanto é preciso utilizar a versão insider do Windows 10 para habilitar o acesso ao WSL 2 caso você queira testar hoje mesmo.

Como vimos na comparação, o Docker Desktop com WSL 2 vai trazer um grande salto na performance para aplicações que usam muito processamento em disco. Para mim, que de vez em quando trabalho com projetos em PHP vai ser uma mão na roda! 😀

E para você? Pretende testar o Docker com WSL 2 no seu projeto? Conte pra gente o que você achou!

O que é o Zend Framework?

O Zend Framework é um robusto Framework para o desenvolvimento de aplicações PHP, orientado à objetos e licenciado sobre a New BSD License (Licença de código aberto de domínio público).

Caso você não saiba, entenda o que é um Framework

Aqui no blog já possuímos um artigo que aborda “Para que serve um Framework”, mas em palavras mais simples, o framework é um facilitador no desenvolvimento de diversas aplicações e, sem dúvida, sua utilização poupa tempo e custos para quem utiliza, pois de forma mais básica, é um conjunto de bibliotecas utilizadas para criar uma base, onde as aplicações são construídas, um otimizador de recursos.

Possui como principal objetivo resolver problemas recorrentes com uma abordagem mais genérica. Ele permite ao desenvolvedor focar nos “problemas” da aplicação, não na arquitetura e configurações.

Zend Expressive - Microframework PHP
Curso de Zend Expressive - Microframework PHP
CONHEÇA O CURSO

De volta ao Zend Framework…

Multiplataforma e de fácil escrita, implementar códigos mais complexos com o Zend torna-se algo mais acessível, permitindo que os desenvolvedores utilizem componentes quando e onde fizerem sentido em suas aplicações, sem a necessidade da instalação de componentes desnecessários para um projeto.

É um Framework utilizado por grandes empresas, como Cisco, Serpro, AutoTrack, BBC, entre outras, e possui uma comunidade extremamente ativa.

Possui como características:

  • É escrito em PHP;
  • Também é possível trabalhar com modelos de banco de dados NoSQL e XML;
  • Possui suporte para múltiplos sistemas de bancos de dados, incluindo MySQL, Oracle, IBM DB2, Microsoft SQL Server, PostgreSQL, SQLite, e Informix Dynamic Server;
  • Possui gerenciamento de sessão;
  • Possui Componente nativo PHP para leitura, atualização e criação de documentos PDF;
  • Autenticação e autorização baseada em ACL (Controle de acesso);
  • Arquitetura “use-a-vontade”, que permite a reutilização de componentes, entre outras;

Padrão

Por padrão, o Zend Framework não implementa modelo arquitetural, porém há diversos componentes que utilizam o padrão MVC, comumente mais utilizado nos projetos.

Sendo assim, o Zend Framework permite que o desenvolvedor tenha total liberdade para definir a estrutura do seu projeto, porém, claro, é sempre bom seguir um padrão amplamente adotado pela comunidade, como o MVC.

Basicamente, o MVC funciona da seguinte forma:

Ao receber uma requisição, o Controller solicita ao Model as informações necessárias (que provavelmente virão do banco de dados), que as obtém e retorna ao Controller. De posse dessas informações, o Controller as envia para a View que irá renderizá-las.

Model: Parte lógica da aplicação que gerencia o comportamento dos dados, ou seja, todos os seus recursos (consultas ao BD, validações, notificações, etc). A camada de model apenas tem o necessário para que tudo aconteça, mas não sabe quando irá executar.

View: Gerencia a saída gráfica e textual da parte da aplicação visível ao usuário final, não possuindo a responsabilidade de saber quando vai exibir os dados, apenas como irá exibi-los.

Controller: Essa é a camada que sabe quem chamar e quando chamar para executar determinada ação, interpretando as entradas do mouse e teclado do usuário, comandando a visão e o modelo para se alterarem de forma apropriada.

Podemos então concluir:

O Zend Framework é um excelente Framework para criação de aplicações em PHP. Com ele podemos criar grandes projetos, com total segurança e desempenho. Neste artigo vimos algumas das suas principais características, e em seu site é possível realizar o download do Framework, além de encontrar toda sua documentação.

Integração contínua para aplicações PHP usando Github Actions

Github Actions é um serviço relativamente novo e, ainda que em estágio beta, possui boa estabilidade. Ele permite que criemos workflows com jobs a serem executados a partir dos eventos disparados no Github. Então, por exemplo, sempre que um novo pull request é criado ou que um push é feito no Github, são exemplos de eventos que podem disparar a execução de workflows. Esse serviço favorece muito a implementação de integração e entrega contínua sem que haja a necessidade de sairmos do Github para serviços externos como Travis, CircleCI etc.

No caso específico do PHP podemos criar um workflow para que a cada novo push em determinado branch dispare a execução de um job para rodar os testes unitários do projeto, executar o PHPStan (analisador estático) e até mesmo executar o PHP Insights para avaliar a qualidade do código. Enfim, é possível executar qualquer ferramenta que faça sentido pro contexto do projeto.

Mas, há de ressaltar, o Github Actions é muito mais que isso. Possui uma infinidade de eventos que podem ser trabalhados. Nesse artigo focaremos na integração contínua de uma aplicação PHP tradicional, no sentido de rodar testes unitários, executar o PHPStan e o PHP Insights a cada novo push ou pull request a fim de garantir que o código esteja dentro dos parâmetros exigidos pela equipe antes dele ser incorporado. Mas o Github Actions pode ser usado para qualquer projeto de qualquer linguagem.

Git e GitHub - Controle de versão
Curso de Git e GitHub - Controle de versão
CONHEÇA O CURSO

No que se baseia o Github Actions?

O Github Actions se baseia em Workflows que são fluxos de trabalho, por exemplo: Build, Teste e Publicação poderiam ser três tipos de workflows. Cada Workflow deve possuir um ou mais jobs. O Job refere-se à execução de uma tarefa, por exemplo, poderíamos em um workflow termos um job para dar build nos assets e outros job para executar os testes. É no workflow que configuramos quais eventos e ambientes (ubuntu, macOS etc) os nossos jobs serão executados. Em resumo, temos um workflow que possui N jobs e um job possui N steps (ações). Veremos isso na prática daqui a pouquinho, o que tornará mais fácil de visualizar a dinâmica da coisa toda.

O Github Actions é pago ou grátis?

É grátis para repositórios públicos e pago para repositórios privados com cobrança por minuto de execução. Na página do Github Actions você consegue visualizar melhor os detalhes de cobrança. Lembrando que enquanto estiver em fase beta o serviço estará grátis tanto para repositórios públicos quanto para privados.

O que eu preciso fazer para começar a usar?

Como o serviço ainda está em beta é preciso que você se inscreva para utilizá-lo. Basta que você esteja logado na sua conta do Github e então clicar no botão “Sign up for the beta“:

Depois é só confirmar:

O projeto de teste

Criei um projeto de uma classe e um método só, para que foquemos no assunto central do artigo, que se trata da criação de um Workflow de teste e validação do código do nosso projeto.

O nosso Workflow executará as seguintes ferramentas:

  • PHPUnit
  • PHPStan
  • PHP Insights

Recomendo que você leia o artigo Ferramentas essenciais para um projeto PHP para conhecer um pouco mais sobre elas.

O projeto completo com o Workflow criado e ativo você pode visualizar em:

https://github.com/KennedyTedesco/github-actions-php

Antes de entrarmos na parte que toca a configuração do Workflow, primeiro quero mostrar o funcionamento visual dele lá no Github.

No projeto tem uma aba chamada Actions, clique nela:

Ela lista os Workflows configurados para o projeto. No nosso caso o workflow se chama “App Workflow“. Do lado direito temos a relação dos eventos que dispararam a execução dele. Ele foi disparado três vezes no evento de “push”, ou seja, quando subimos pro Github alterações.

No primeiro evento (de baixo pra cima) o workflow foi executado com sucesso, sem nenhum erro. Os testes passaram, o PHPStan não reportou nenhum erro e as métricas do PHP Insights estavam todas boas.

Já no segundo teste houve uma falha. Ah, o resultado da execução dos workflows também aparece na listagem dos commits:

https://github.com/KennedyTedesco/github-actions-php/commits/master

Observe os ícones de sucesso e erro.

Voltando na tela anterior, clique em um evento em que a execução do workflow teve sucesso, por exemplo, esse aqui:

https://github.com/KennedyTedesco/github-actions-php/commit/4a5be2bbaf5a09247321efa6c2e799f4c621d699/checks?check_suite_id=264838943

É nessa tela que vemos os Jobs e suas Steps (passos onde ações são executadas). No caso, o Job “Tests” (à esquerda) executou todas as Steps (ações) da tela escura à direita. As ações que configuramos foram no sentido de definir o ambiente PHP em que a execução ocorrerá, instalação das dependências do Composer no projeto e execução das ferramentas de análise e teste.

O evento que causou falha na execução do workflow se deu por causa desse commit:

https://github.com/KennedyTedesco/github-actions-php/commit/9f6fb1142a5e2b98a2ecedd904eaa10301c1eb0e

Explicitamente fiz um teste falhar ao fazer ele comparar 4 === 8.

Quando subi essa alteração para o Github, um evento de push foi disparado fazendo executar o workflow, que então falhou:

https://github.com/KennedyTedesco/github-actions-php/commit/9f6fb1142a5e2b98a2ecedd904eaa10301c1eb0e/checks?check_suite_id=264846074

Definindo o Workflow no projeto

Agora que já vimos como funciona lá na interface do Github a parte visual da execução do workflow, veremos como de fato podemos criar-lo dentro do nosso projeto.

Primeiro de tudo, devemos criar uma pasta chamada .github dentro do projeto. E, dentro dessa pasta, devemos criar uma pasta chamada workflows.

- github-actions-php
- - .github
- - - workflows
- - - - main.yaml

É dentro da pasta workflows que criamos os arquivos de definição dos nossos workflows. São arquivos yaml. Criamos apenas um workflow, portanto teremos apenas um arquivo yaml nessa pasta e demos o nome dele de main.yaml:

name: App Workflow

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

jobs:
  build:
    name: Tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@master

      - name: Setting up PHP
        uses: ./.github/actions/php

      - name: Installing Composer
        run: ./.github/scripts/run-composer.sh

      - name: Running PHPStan
        run: ./.github/scripts/run-phpstan.sh

      - name: PHP Insights
        run: ./.github/scripts/run-phpinsights.sh

      - name: Running PHPUnit
        run: ./.github/scripts/run-phpunit.sh

Esse é o nosso workflow completo. Em name definimos o nome dele.

Em on especificamos em quais eventos de quais branches esse workflow será executado. No caso, estamos dizendo que ele será executado sempre que um push ou um pull request for feito para o branch master.

Em jobs é onde definimos os jobs. No caso, temos apenas um job chamado build, veja que na estrutura do yaml eu dei o nome de build mas ao definir o nome dele lá pro Github eu o chamei de “Tests”:

  build:
    name: Tests

Isso não importa muito, você vai definir o nome que achar mais conveniente e lógico para o que você estiver executando. Eu poderia tranquilamente tê-lo chamado de:

  tests:
    name: Tests

Sem problema algum.

A diretiva runs-on do job especifica em qual ambiente a execução acontecerá. O ideal é você escolher o mesmo ambiente que você roda em produção.

Em steps é onde executamos as ações do job. Um job pode executar N ações. A action Checkout é padrão, é ela quem clona o projeto no ambiente de execução. Veja que ela possui um endereço do Github:

    steps:
      - name: Checkout
        uses: actions/checkout@master

Ela é mantida pelo Github e pode ser visualizada aqui: https://github.com/actions/checkout

Ou seja, podemos executar actions que estejam em repositórios arbitrários. Existe todo um ecossistema de actions que podem ser reutilizadas. Existe até mesmo um Marketplace de Actions no próprio Github:

https://github.com/marketplace?type=actions

Também tem uma lista de centenas de actions mantidas pela comunidade:

https://github.com/sdras/awesome-actions

Voltando ao nosso workflow, a segunda action:

      - name: Setting up PHP
        uses: ./.github/actions/php

Ela especifica que se trata de um Dockerfile contido em ./.github/actions/php que instalará a versão 7.3 do PHP que será usada para rodar os testes e validações:

https://github.com/KennedyTedesco/github-actions-php/tree/master/.github/actions/php

FROM php:7.3-cli-alpine

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

Ou seja, podemos criar actions baseadas em Dockerfiles.

A terceira action:

https://github.com/KennedyTedesco/github-actions-php/blob/master/.github/scripts/run-composer.sh

      - name: Installing Composer
        run: ./.github/scripts/run-composer.sh

Veja que ao invés de uses ela especifica run, pois estamos definindo qual comando ou arquivo ela deverá executar ao invés de qual action ela deverá usar.

Essa action é responsável pela instalação das dependências do composer no projeto. Nesse ponto já teremos o PHP configurado. Eu gosto de trabalhar com arquivos shell script para definir as tarefas que serão executadas. O conteúdo do arquivo run-composer.sh é:

#!/bin/sh
cp .env.ci .env
composer install --no-ansi --no-interaction --no-suggest --prefer-dist

Ela copia o arquivo .env.ci para .env na raiz do projeto e depois instala as dependências. O arquivo env.ci não tem nenhuma utilidade para o nosso projeto, eu deixei ele criado apenas para mostrar o que você pode fazer quando estiver criando um workflow para uma aplicação Symfony ou Laravel que use variáveis de ambiente. Nesse sentido, você pode ter variáveis de ambiente específicas para a execução do workflow.

Ao invés de executar o arquivo run-composer.sh poderíamos ter executado esses comandos diretamente no yaml do workflow, por exemplo:

      - name: Installing Composer
        run: |
          cp .env.ci .env
          composer install --no-ansi --no-interaction --no-suggest --prefer-dist

Mas eu prefiro definir arquivos shell script.

As últimas três ações rodam o PHPStan, PHP Insights e PHPUnit, respectivamente. Também são simples arquivos shell script. E com isso fechamos a configuração do nosso workflow.

Testando localmente as suas actions

Você pode usar a ferramenta act para localmente testar as suas actions. Incrível, não?

Concluindo

Eu encorajo que você leia a documentação e veja todos os recursos e opções disponíveis. Por exemplo, na página Workflow syntax for GitHub Actions tem muita informação valiosa sobre as outras sintaxes e opções.

E, não menos importante, quando você for criar um workflow para sua aplicação de maior porte, você certamente precisará lidar com a integração de variáveis de ambiente entre os jobs e actions, para isso, sugiro que você leia Virtual environments for GitHub Actions.

Como não dá pra masterizar o assunto em um artigo, tentei focar no essencial, no principal para uma aplicação PHP tradicional. Mas essas mesmas ideias podem ser agregadas para que você crie sua integração contínua para Laravel, Symfony ou outro framework qualquer. Sem contar que além do workflow de teste, você poderia ter um workflow para uma tarefa de corrigir o estilo do seu código e criar um pull request disso de forma automatizada, por exemplo. Como também poderia ter workflows para entrega/deploy contínuos. As possibilidades são muitas.

Até a próxima!

Analista DevOps Júnior
Formação: Analista DevOps Júnior
A formação Analista DevOps nível Júnior da TreinaWeb visa introduzir desenvolvedores a tecnologias como o Docker e o servidor HTTP Nginx, tecnologias estas intimamente relacionadas ao notável universo DevOps.
CONHEÇA A FORMAÇÃO

Ferramentas essenciais para um projeto PHP

Este será um artigo um pouco diferente dos últimos que tenho escrito, vou citar as quatro principais ferramentas que considero essenciais para qualquer projeto PHP:

  • PHPUnit (Testes unitários);
  • PHPStan (Analisador estático);
  • PHP-CS-Fixer (Corrige estilo de código);
  • PHP Insights (Analisa o estilo e a qualidade do código);
Desenvolvedor PHP Sênior
Formação: Desenvolvedor PHP Sênior
Nesta formação você aprenderá aspectos avançados da linguagem PHP e de seu ecossistema, conhecimentos essenciais para se tornar um desenvolvedor diferenciado, e com isso, você terá conhecimento para desenvolver aplicações PHP usando as práticas mais modernas do mercado.
CONHEÇA A FORMAÇÃO

1) PHPUnit

O PHPUnit é o principal e mais estabelecido framework para testes unitários em PHP. Automatizar testes é um requisito cada vez mais essencial para os projetos, não importando se você vai seguir TDD como metodologia, mas pelo menos testar as partes mais críticas da aplicação é algo que pode te salvar algumas boas horas procurando bugs e corrigindo problemas da evolução natural do software.

Você pode instalá-lo em seu projeto executando:

$ composer require phpunit/phpunit:^8.0 --dev

Hoje a ferramenta já tem a documentação traduzida para o português, você pode ler aqui.

Testar no PHPUnit é super simples e bem parecido com a forma que se faz em outros frameworks de teste de outras linguagens:

Exemplo extraído da documentação:

<?php

declare(strict_types=1);

final class Email
{
    private $email;

    private function __construct(string $email)
    {
        $this->ensureIsValidEmail($email);

        $this->email = $email;
    }

    public static function fromString(string $email): self
    {
        return new self($email);
    }

    public function __toString(): string
    {
        return $this->email;
    }

    private function ensureIsValidEmail(string $email): void
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException(
                sprintf(
                    '"%s" is not a valid email address',
                    $email
                )
            );
        }
    }
}

Essa classe poder ser facilmente testada:

<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class EmailTest extends TestCase
{
    public function testCanBeCreatedFromValidEmailAddress(): void
    {
        $this->assertInstanceOf(
            Email::class,
            Email::fromString('user@example.com')
        );
    }

    public function testCannotBeCreatedFromInvalidEmailAddress(): void
    {
        $this->expectException(InvalidArgumentException::class);

        Email::fromString('invalid');
    }

    public function testCanBeUsedAsString(): void
    {
        $this->assertEquals(
            'user@example.com',
            Email::fromString('user@example.com')
        );
    }
}
PHP - Testes unitários com PHPUnit
Curso de PHP - Testes unitários com PHPUnit
CONHEÇA O CURSO

2) PHPStan

O PHPStan é um analisador estático que varre o código da sua aplicação procurando por erros e incongruências sem que haja a necessidade de executá-lo, a análise é toda estática.

É uma ferramenta essencial para te apontar bugs ou erros que certamente aparecerão em algum momento do uso da aplicação.

Para instalar o PHPStan no seu projeto:

$ composer require --dev phpstan/phpstan

Uma vez instalado, basta executar a ferramenta indicando quais pastas ele deve avaliar:

$ ./vendor/bin/phpstan analyse src tests

Nesse caso, estará avaliando as pastas src e tests.

Você ainda pode especificar o quão estrita você quer que a análise seja feita. Por padrão ele opera no level 0, mas vai até o 7:

$ ./vendor/bin/phpstan analyse src tests --level 7

Para aplicações que rodam em cima do Laravel, tem o Larastan, que facilita a instalação e personalização.

Apesar de eu preferir o PHPStan, também existe o ótimo Psalm, mantido pelo pessoal do Vimeo. Vale a pena estudá-lo, talvez você venha a preferi-lo.

3) PHP-CS-Fixer

O PHP-CS-Fixer corrige todo o estilo do seu código, quebras, espaços, estilo de declarações etc. É possível definir para que ele siga as PSR-1 e PSR-2, por exemplo. Não obstante, já estão desenvolvendo suporte nele para o mais novo padrão de estilo de código, o PSR-12.

Para instalá-lo em seu projeto:

$ composer require friendsofphp/php-cs-fixer --dev

E para rodar as correções usando as regras da PSR-2:

./vendor/bin/php-cs-fixer fix src --rules=@PSR2 --allow-risky yes

Nesse caso ele rodará na pasta src.

Uma forma mais simples de configurar o perfil de execução dele é criando um arquivo .php_cs na raiz do seu projeto e então definir o que é pra ele validar, o que não é, quais regras usar, quais configurações de estilo usar etc. Em nossas aplicações no TreinaWeb usamos esse perfil:

<?php

$finder = Symfony\Component\Finder\Finder::create()
    ->notPath('vendor')
    ->notPath('bootstrap')
    ->notPath('storage')
    ->notPath('resources')
    ->in(__DIR__)
    ->name('*.php')
    ->notName('*.blade.php');

return PhpCsFixer\Config::create()
    ->setRules([
        '@PSR2' => true,
        'final_class' => true,
        'static_lambda' => true,
        'linebreak_after_opening_tag' => true,
        'blank_line_after_opening_tag' => true,
        'declare_strict_types' => true,
        'array_syntax' => ['syntax' => 'short'],
        'ordered_imports' => ['sortAlgorithm' => 'length'],
        'no_unused_imports' => true,
        'native_function_invocation' => true,
        'is_null' => true,
        'list_syntax' => [
            'syntax' => 'short',
        ],
        'lowercase_cast' => true,
        'lowercase_static_reference' => true,
        'mb_str_functions' => true,
        'modernize_types_casting' => true,
        'native_constant_invocation' => true,
        'native_function_casing' => true,
        'new_with_braces' => true,
        'blank_line_before_statement' => [
            'statements' => ['declare',],
        ],
        'return_type_declaration' => [
            'space_before' => 'none',
        ],
    ])
    ->setFinder($finder);

Outras configurações você pode verificar no repositório oficial da ferramenta. Uma vez tendo o arquivo .php_cs, é só executar sem precisar informar --rules:

./vendor/bin/php-cs-fixer fix src --allow-risky yes

4) PHP Insights

Ferramenta para análise da qualidade do código. Ele divide a análise em quatro categorias: código, complexidade (complexidade ciclomática etc), arquitetura (tamanho de métodos e classes, entre outras análises) e estilo de código (formatação, padronização).

Para instalar a ferramenta no seu projeto:

$ composer require nunomaduro/phpinsights --dev

Depois, basta executar:

$ ./vendor/bin/phpinsights

Você pode criar um arquivo phpinsights.php na raiz do seu projeto para configurar o perfil da análise, ou seja, para definir o que você quer remover da análise, o que quer incluir etc. Por exemplo:

<?php

declare(strict_types=1);

use ObjectCalisthenics\Sniffs\Files\FunctionLengthSniff;
use NunoMaduro\PhpInsights\Domain\Insights\ForbiddenTraits;
use ObjectCalisthenics\Sniffs\Metrics\MethodPerClassLimitSniff;
use SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff;
use NunoMaduro\PhpInsights\Domain\Insights\CyclomaticComplexityIsHigh;
use SlevomatCodingStandard\Sniffs\TypeHints\DisallowMixedTypeHintSniff;
use NunoMaduro\PhpInsights\Domain\Insights\Composer\ComposerMustBeValid;
use SlevomatCodingStandard\Sniffs\Namespaces\AlphabeticallySortedUsesSniff;
use PHP_CodeSniffer\Standards\PSR1\Sniffs\Methods\CamelCapsMethodNameSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff;

return [
    'preset' => 'default',

    'exclude' => [
        'vendor',
        'config',
        'bootstrap',
        'resources',
        'storage',
        'public',
        'tests',
    ],

    'add' => [

    ],

    'remove' => [
        ForbiddenTraits::class,
        TypeHintDeclarationSniff::class,
        DisallowMixedTypeHintSniff::class,
        ComposerMustBeValid::class,
        AlphabeticallySortedUsesSniff::class,
        CamelCapsMethodNameSniff::class,
        LineLengthSniff::class,
    ],

    'config' => [
        CyclomaticComplexityIsHigh::class => [
            'maxComplexity' => 7,
        ],

        FunctionLengthSniff::class => [
            'maxLength' => 30,
        ],

        MethodPerClassLimitSniff::class => [
            'maxCount' => 12,
        ],
    ],
];

Maiores informações sobre a configuração da ferramenta, você pode ver no site oficial.

Usando o Composer como atalho para a execução de scripts

Por fim, uma coisa que acho bem útil e que uso bastante é colocar no composer.json atalhos para os scripts de linha de comando que mais executo no projeto. É possível defini-los numa área scripts, por exemplo:

    "scripts": {
        "phpunit": [
            "./vendor/bin/phpunit"
        ],        
        "cs": [
            "./vendor/bin/php-cs-fixer fix --allow-risky yes"
        ],
        "phpstan": [
            "./vendor/bin/phpstan analyse src tests --level 7"
        ],
        "phpinsights": [
            "./vendor/bin/phpinsights"
        ]
    },

Então, ao invés de executar ./vendor/bin/phpstan analyse src tests --level 7 eu apenas faço:

$ composer phpstan

O mesmo vale para a correção do estilo do código, apenas executo:

$ composer cs

Concluindo

Espero que o artigo tenha despertado a sua curiosidade. Recomendo demais que você leia a documentação dessas ferramentas e passe a utilizá-las, em pouco tempo você estará “viciado” e certamente as usará em todos os futuros projetos.

Até a próxima!

Stream Wrappers personalizados no PHP

No artigo Streams no PHP, que é uma leitura pré-requisito deste, vimos o essencial sobre streams e como podemos operar sobre elas. Também vimos que o PHP implementa diversos wrappers nativamente, como http://, php://, file:// entre outros, que permitem que operemos determinados protocolos usando as funções nativas do PHP como fgets(), fopen(), fwrite() etc. Nesse artigo veremos como podemos implementar um protocolo próprio a partir de um stream wrapper customizado.

PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO

Não existe uma classe base a ser estendida ou uma interface a ser implementada, tudo o que o PHP fornece é um protótipo de quais métodos podem ser utilizados no wrapper personalizado. Isso pode ser visto na referência The StreamWrapper Class.

Funcionamento do Wrapper

SqliteJsonWrapper será o nome dele. Definiremos o protocolo sqlj:// e ele terá a responsabilidade de consultar um banco de dados SQLite e retornar os resultados no formato json.

O código completo do wrapper pode ser obtido no repositório KennedyTedesco/artigo-stream-wrappers-php.

Dinâmica de funcionamento:

Dentro da pasta resources do projeto temos uma base de dados SQLite chamada movies.sqlite3, nela existe uma tabela chamada movies com uma relação de filmes e alguns dados sobre eles, nesse formato:

filmgenrestudioyear
(500) Days of SummercomedyFox2009
27 DressesComedyFox2008
A Dangerous MethodDramaIndependent2011
A Serious ManDramaUniversal2009
Across the UniverseromanceIndependent2007
BeginnersComedyIndependent2011
Dear JohnDramaSony2010

Usando o componente illuminate/database do Laravel, vamos nos conectar a essa base de dados e realizar uma consulta, a depender do que será informado na URL do wrapper.

O uso do nosso wrapper se dará dessa forma:

$stream = \fopen('sqlj://movies/year/2009', 'rb', false, $context);

Informamos o protocolo sqlj://, movies é o nome da tabela que será consultada, year é o campo que será usado para delimitar a consulta, ou seja, nesse caso, estaremos resgatando apenas os filmes do ano de 2009 cadastrados na nossa base.

Se a ideia é obter todos os filmes, basta que informemos apenas o nome da tabela:

$stream = \fopen('sqlj://movies', 'rb', false, $context);

Ou, se quisermos obter apenas filmes do gênero drama:

$stream = \fopen('sqlj://movies/genre/drama', 'rb', false, $context);

Você já deve ter percebido que “The Patifaria Never Ends“, esse é um exemplo puramente didático. Não faz muito sentido um wrapper com essa implementação em uma aplicação real que realiza uma consulta em um banco de dados SQLite local, ademais, poderíamos nos conectar diretamente ao banco usando PDO.

A ideia desse artigo é mostrar que é possível abstrair qualquer tipo de coisa para streams e usar as funções nativas do PHP para lidar com esses chunks de dados, o que faz mais sentido quando operamos com grandes arquivos. Mas no final desse artigo veremos referências de casos de uso de Stream Wrappers que são implementados por grandes projetos. Vamos ao código?

Desenvolvedor PHP Sênior
Formação: Desenvolvedor PHP Sênior
Nesta formação você aprenderá aspectos avançados da linguagem PHP e de seu ecossistema, conhecimentos essenciais para se tornar um desenvolvedor diferenciado, e com isso, você terá conhecimento para desenvolver aplicações PHP usando as práticas mais modernas do mercado.
CONHEÇA A FORMAÇÃO

Show me the code

A classe SqliteJsonWrapper é a implementação do wrapper:

<?php

declare(strict_types=1);

use Illuminate\Database\Connection;
use Illuminate\Database\Capsule\Manager as Capsule;

final class SqliteJsonWrapper
{
    /** @var resource */
    public $context;

    /** @var string */
    private $result;

    /** @var Connection */
    private $connection;

    /** @var int */
    private $position = 0;

    public function stream_open(string $path, string $mode, int $options) : bool
    {
        // Resgata as informações de contexto da stream que foram passadas
        $streamContext = \stream_context_get_options($this->context);

        if (empty($streamContext['database'])) {
            throw new \RuntimeException('Missing Stream Context');
        }

        // Conecta à base de dados
        $this->connect($streamContext);

        // Realiza a pesquisa
        $this->query($path);

        return true;
    }

    public function stream_read(int $count)
    {
        $chunk = \mb_substr($this->result, $this->position, $count);

        $this->position += $count;

        return $chunk;
    }

    public function stream_eof() : bool
    {
        return ! ($this->position < \mb_strlen($this->result));
    }

    public function stream_stat() : ?array
    {
        return null;
    }

    private function connect(array $options) : void
    {
        $capsule = new Capsule();

        $capsule->addConnection([
            'driver'    => 'sqlite',
            'database'  =>  $options['database']['file'],
            'prefix'    => '',
        ]);

        $this->connection = $capsule->getConnection();
    }

    private function query(string $path) : void
    {
        // Extrai o nome da tabela
        $table = \parse_url($path, \PHP_URL_HOST);

        // Tenta extrair se é pra delimitar a consulta com where
        $where = [];
        if ($path = \parse_url($path, \PHP_URL_PATH)) {
            $criteria = \explode('/', $path);

            $where = [
                $criteria[1] => $criteria[2]
            ];
        }

        // Armazena os resultados no formato json
        $this->result = $this->connection->table($table)->where($where)->get()->toJson();
    }
}

A classe do wrapper precisa se basear no protótipo especificado em The StreamWrapper Class, mas utilizando apenas os métodos que fazem sentido para o caso de uso dela. No nosso caso, nos limitamos aos métodos:

  • stream_open(): Executado quando a stream é aberta usando fopen().
  • stream_read(): Executado quando alguma função de leitura é utilizada, como fgets() ou stream_get_contents(), ele retorna em chunks os dados e avança o ponteiro que guarda até que posição de bytes de dados já foram lidos, para que na próxima iteração ele pegue a partir daquele ponto.
  • stream_eof(): Executado quando a função feof() é invocada. Ele informa se está no final do arquivo.
  • stream_stat(): Executado em resposta à função fstat(), mas também quando a stream_get_contents() é chamada. Para o nosso exemplo, esse método não precisa retornar nenhum dado sobre o recurso que estamos operando, mas ele precisa existir na classe, mesmo que sem implementação.

Esses são os quatro métodos que permitem com que façamos as principais operações em cima dos dados resgatados.

Os outros métodos que a classe implementa são apenas da lógica dela para consulta na base de dados:

  • connect(): Esse método realiza a conexão com a base de dados;
  • query(): Uma vez que uma conexão foi aberta, esse método recebe a URL de consulta do wrapper para realizar uma pesquisa na base de dados.

Um uso básico do nosso wrapper:

<?php

declare(strict_types=1);

require './vendor/autoload.php';
require './SqliteJsonWrapper.php';

\stream_wrapper_register('sqlj', SqliteJsonWrapper::class);

$context = \stream_context_create([
    'database' => [
        'file' => './resources/movies.sqlite3',
    ],
]);

$stream = \fopen('sqlj://movies/year/2009', 'rb', false, $context);

$buffer = '';
while (\feof($stream) === false) {
    $buffer .= \fread($stream, 128);
}

echo $buffer;

fclose($stream);

O resultado:

[{"film":"(500) Days of Summer","genre":"comedy","studio":"Fox","year":"2009"},{"film":"A Serious Man","genre":"drama","studio":"Universal","year":"2009"},{"film":"Ghosts of Girlfriends Past","genre":"comedy","studio":"Warner Bros.","year":"2009"},{"film":"He's Just Not That Into You","genre":"comedy","studio":"Warner Bros.","year":"2009"},{"film":"It's Complicated","genre":"comedy","studio":"Universal","year":"2009"},{"film":"Love Happens","genre":"drama","studio":"Universal","year":"2009"},{"film":"Not Easily Broken","genre":"drama","studio":"Independent","year":"2009"},{"film":"The Invention of Lying","genre":"comedy","studio":"Warner Bros.","year":"2009"},{"film":"The Proposal","genre":"comedy","studio":"Disney","year":"2009"},{"film":"The Time Traveler's Wife","genre":"drama","studio":"Paramount","year":"2009"},{"film":"The Twilight Saga: New Moon","genre":"drama","studio":"Summit","year":"2009"},{"film":"The Ugly Truth","genre":"comedy","studio":"Independent","year":"2009"}]⏎

A stream_wrapper_register() registra o wrapper. Na variável $context criamos o contexto da stream com a informação de localização do arquivo da base de dados que será trabalhado. Depois disso, abrimos a stream em cima do protocolo que definimos e extraímos os dados dela em chunks de 128 bytes.

O mesmo exemplo também poderia ser reescrito para usar stream_get_contents():

<?php

declare(strict_types=1);

require './vendor/autoload.php';
require './SqliteJsonWrapper.php';

\stream_wrapper_register('sqlj', SqliteJsonWrapper::class);

$context = \stream_context_create([
    'database' => [
        'file' => './resources/movies.sqlite3',
    ],
]);

$stream = \fopen('sqlj://movies/year/2009', 'rb', false, $context);

echo stream_get_contents($stream);
PHP Intermediário
Curso de PHP Intermediário
CONHEÇA O CURSO

Concluindo

Igual comentei anteriormente, existem diversas implementações possíveis para Stream Wrappers, vou me delimitar a duas bem legais, que são:

  • Google Storage: No cliente do Google Cloud para PHP eles definiram um Stream Wrapper para que o usuário consiga recuperar ou gravar objetos no serviço usando as funções nativas do PHP a partir do protocolo gs://.
  • AWS S3: O mesmo existe na SDK da AWS para PHP, um Stream Wrapper para que seja possível usar o protocolo s3:// e recuperar / gravar objetos no serviço de storage S3.

No caso da AWS, ter uma implementação de Stream Wrapper para abstrair a comunicação com o serviço de storage dela, traz benefícios para o desenvolvedor que usa sua SDK tais como:

Para recuperar um arquivo:

$data = file_get_contents('s3://bucket/key');

Se precisa trabalhar com grandes arquivos:

// Abre a stream
if ($stream = fopen('s3://bucket/key', 'r')) {
    // Enquanto a stream continua aberta
    while (!feof($stream)) {
        // Lê 1024 bytes da stream
        echo fread($stream, 1024);
    }
    fclose($stream);
}

Como também permite que objetos sejam criados:

file_put_contents('s3://bucket/key', 'Hello!');

Bom, é isso! É bem possível que eu continue escrevendo sobre o assunto de streams nos próximos artigos, então, até breve!

Streams no PHP

Streams representam coleções de dados que podem não estar completamente disponíveis de imediato e que também não possuem a limitação de terem que caber de uma só vez na memória. Isso faz com que elas sejam uma poderosa ferramenta para lidarmos com os dados em partes (chunks) e acessados sob demanda. Uma definição mais técnica para streams é que representam a transmissão/fluxo de dados de uma fonte para um destino.

Entender o essencial sobre streams é fundamental para se trabalhar com arquivos. Sem streams não conseguiríamos abrir um arquivo de 20GB se não tivéssemos 20GB de memória disponíveis. Com elas, abrimos o arquivo e o lemos em partes. Sistemas operacionais baseados no Unix proveem uma interface padrão para operações I/O (input e output) que se passa por descritores de arquivo. Um descritor de arquivo é representado por um número inteiro que se associa a um arquivo aberto e, nesses sistemas, há uma generalização de que “tudo é um arquivo”, então, quando falamos aqui de arquivo, ele pode ser uma conexão de rede (um socket é um tipo especial de arquivo), um arquivo de texto, um arquivo de vídeo ou até mesmo uma pasta (que é um tipo especial de arquivo).

PHP Básico
Curso de PHP Básico
CONHEÇA O CURSO

Grande parte do que é feito no PHP é sobre streams. Ao ler um arquivo, lidamos com streams. Ao retornar um output para o cliente, lidamos com streams. Ao ler um input, lidamos com streams. Ao obter dados de uma conexão TCP/IP, também estaremos trabalhando com streams. O PHP abstrai ao máximo o trabalho com as streams e, na maior parte do tempo, usamos funções de alto nível da linguagem que deixam bem transparente o uso delas. No entanto, há momentos em que precisamos conhecer um pouco mais sobre elas para que consigamos resolver problemas e necessidades específicas. Essa é a ideia desse artigo.

Operações básicas com streams

Uma stream é referenciada dessa maneira:

<scheme>://<target>

Scheme é nome do wrapper que está sendo utilizado. Target é a identificação do recurso que será trabalhado. E esse recurso vai depender do contexto, se estamos falando do sistema de arquivos ou da rede, por exemplo.

O PHP possui algumas funções genéricas que nos permitem interagir com streams e até mesmo mudar o comportamento delas. Muitas das funções que trabalham com arquivos permitem essa interação. Algumas das mais conhecidas são:

  • fopen()
  • fwrite() ou fputs()
  • fclose()
  • file_get_contents()
  • file_put_contents()
  • Entre outras.

O PHP possui uma extensão chamada Stream que já vem habilitada por padrão. Como o próprio nome sugere, ela lida com toda a abstração de streams de I/O (input e output) do PHP. No decorrer do artigo utilizaremos algumas dessas funções.

Antes, apenas para dar contexto ao que falamos anteriormente de que com streams lidamos com chunks e que isso permite trabalharmos com grandes quantidade de dados sem estourar o limite de memória, desenvolveremos um simples exemplo.

Se você usa um sistema baseado em unix, em um diretório de testes, crie o seguinte arquivo:

$ awk 'BEGIN { n = 1; while (n < 10000000) print (n++) }' > numeros.txt

O arquivo criado tem ~78MB e é composto apenas de números sequenciais. Agora, digamos hipoteticamente que a nossa instalação do PHP nos permite usar apenas 4MB por script, vamos tentar ler o arquivo utilizando file_get_contents():

<?php

ini_set('memory_limit', '4M');

$file = file_get_contents('numeros.txt');

Ao executar, teremos o erro:

PHP Fatal error:  Allowed memory size of ...

Ele tentou alocar mais memória que o permitido, pois o arquivo é muito grande e a file_get_contents() lê o arquivo inteiro na memória.

Agora, a coisa muda quando passamos a utilizar fopen() e fgets(). A fopen() lida “diretamente” com descritores de arquivos, ela apenas abre uma stream para que possamos utilizá-la. Ou seja, dar um echo em fopen não produzirá nenhum resultado, diferentemente de file_get_contents(). Se você der um var_dump() na fopen() verá que é um resource do tipo stream.

<?php

$file = fopen('numeros.txt', 'rb');

var_dump($file); // resource(5) of type (stream)

Usando fgets() podemos ler a linha da stream aberta, sem carregar o arquivo inteiro na memória:

<?php

ini_set('memory_limit', '4M');

$stream = fopen('numeros.txt', 'rb');

while (feof($stream) === false) {
    echo fgets($stream);
}

echo 'Memória utilizada: ' . (memory_get_peak_usage(true) / 1024 / 1024);

fclose($stream);

A função feof() avalia se já chegou ao final do arquivo, se não, continua no while até que o arquivo seja completamente lido e seus dados impressos no output buffer. A fgets() lê linha a linha da stream aberta.

No entanto, a fgets() retorna false quando não tiver mais nenhum byte a ser lido e, nesse sentido, o while do exemplo acima poderia ser reescrito para:

while (($line = fgets($stream)) !== false) {
    echo $line;
}

Você pode usar a abordagem que preferir, ambas resolvem o problema.

O principal wrapper de streams é o file://, ele é o padrão utilizado pela fopen() e outras funções relacionadas. Sempre que acessamos o sistema de arquivos, estamos de forma transparente o utilizando. Por esse motivo, o mesmo exemplo acima poderia ser reescrito para:

<?php

$stream = fopen('file://' . __DIR__ . '/numeros.txt', 'rb');

while (feof($stream) === false) {
    echo fgets($stream);
}

fclose($stream);

Se abríssemos o servidor embutido do PHP no diretório onde o exemplo está salvo:

$ php -S localhost:8000

Também conseguiríamos acessar o numeros.txt através do wrapper http:// do PHP:

<?php

$stream = fopen('http://localhost:8000/numeros.txt', 'rb');

while (feof($stream) === false) {
    echo fgets($stream);
}

fclose($stream);

Teríamos o mesmo resultado, mas no fundo seria aberto um socket TCP/IP para acessar esse servidor, ou seja, no caso desse exemplo em particular em que só precisamos abrir o arquivo e ele está localmente disponível no nosso sistema de arquivo, abrí-lo através da rede é desnecessário. Fizemos isso apenas para fins didáticos.

Falando em sockets, uma dica de leitura: Programação de Sockets em PHP

Nos exemplos acima usamos fgets() que lê linha a linha, mas se temos a necessidade de pegar chunks de um determinado tamanho em bytes por vez, podemos usar a fread():

<?php

ini_set('memory_limit', '4M');

$stream = fopen('http://localhost:8000/numeros.txt', 'rb');

echo fread($stream, filesize('numeros.txt'));

Nesse exemplo estamos pedindo pra fread() ler o tamanho total do arquivo, o que não será possível, devido ao limite de memória de 4MB que temos. Vai estourar o erro da incapacidade de alocação desses dados.

Agora, se a gente diminui o tanto de bytes que queremos ler por vez:

<?php

$stream = fopen('http://localhost:8000/numeros.txt', 'rb');

while (feof($stream) === false) {
    echo '[-------- CHUNK --------]' . PHP_EOL;
    echo fread($stream, 2048);
}

A cada novo [-------- CHUNK --------] que você notar no terminal significa que 2048 bytes foram lidos.

A função stream_get_meta_data() retorna os metadados de uma stream aberta:

<?php

$stream = fopen('numeros.txt', 'rb');

var_dump(stream_get_meta_data($stream));

fclose($stream);

Teria o retorno:

array(9) {
  'timed_out' => bool(false)
  'blocked' => bool(true)
  'eof' => bool(false)
  'wrapper_type' => string(9) "plainfile"
  'stream_type' => string(5) "STDIO"
  'mode' => string(2) "rb"
  'unread_bytes' => int(0)
  'seekable' => bool(true)
  'uri' => string(11) "numeros.txt"
}

Numa stream que usa o wrapper http teríamos algumas informações adicionais como alguns cabeçalhos http da resposta:

<?php

$stream = fopen('http://localhost:8000/numeros.txt', 'rb');

var_dump(stream_get_meta_data($stream));

fclose($stream);

E o retorno é:

array(10) {
  'timed_out' => bool(false)
  'blocked' => bool(true)
  'eof' => bool(false)
  'wrapper_data' => array(6) {
    [0] => string(15) "HTTP/1.0 200 OK"
    [1] => string(20) "Host: localhost:8000"
    [2] => string(35) "Date: Wed, 21 Aug 2019 18:41:52 GMT"
    [3] => string(17) "Connection: close"
    [4] => string(39) "Content-Type: text/plain; charset=UTF-8"
    [5] => string(24) "Content-Length: 78888888"
  }
  'wrapper_type' => string(4) "http"
  'stream_type' => string(14) "tcp_socket/ssl"
  'mode' => string(2) "rb"
  'unread_bytes' => int(0)
  'seekable' => bool(false)
  'uri' => string(33) "http://localhost:8000/numeros.txt"
}

Voltaremos em breve no wrapper http quando vermos sobre contextos de streams.

A função stream_copy_to_stream()

A função stream_copy_to_stream() permite copiar dados de uma stream para outra. Isso costuma ser útil em algumas operações on-the-fly.

O exemplo mais básico de como isso pode ser feito:

<?php

$destination = fopen('cep.json', 'wb');
$source = fopen('https://viacep.com.br/ws/01001000/json/', 'rb');

stream_copy_to_stream($source, $destination);

Copiará para o arquivo cep.json os dados desse CEP.

PHP Intermediário
Curso de PHP Intermediário
CONHEÇA O CURSO

O wrapper php://

Esse é um wrapper especial que permite com que acessemos as streams de I/O da linguagem. Por exemplo, se queremos ler a STDIN (standard input stream) do PHP e depois escrever direto na STDOUT (standard output stream):

<?php

$input = fopen('php://stdin', 'rb');
$output = fopen('php://stdout', 'rb');

fwrite($output, 'Ei, fale algo. :D' . PHP_EOL);

while (true) {
    $line = trim(fgets($input));

    if ($line === ':close') {
        fwrite($output, 'Bye!');

        break;
    }

    fwrite($output, "User says: {$line}" . PHP_EOL);
}

fclose($input);
fclose($output);

Esse exemplo abre duas streams de I/O do PHP, uma relacionada ao input e outra ao output. Depois, num laço infinito ficamos “ouvindo” por novas entradas e escrevemos de volta tudo o que recebemos.

Se você testar o exemplo executando no seu terminal:

$ php index.php
Ei, fale algo. :D
Oi!
User says: Oi!
PHP
User says: PHP
:close
Bye!

Definimos :close como sendo o comando que vai interromper o while.

Ao invés de explicitamente abrimos a stdin e stdout usando fopen(), poderíamos usar duas constantes que o PHP disponibiliza que já fazem esse trabalho pra nós, são as STDIN e STDOUT.

Internamente o PHP define essas constantes:

define('STDIN', fopen('php://stdin', 'r'));
define('STDOUT', fopen('php://stdout', 'w'));

Por exemplo:

<?php

echo fgets(STDIN);

No terminal, execute:

$ echo "Hello World" | php index.php

O resultado será Hello World. O | refere-se ao mecanismo de pipiline do unix, onde podemos passar o stdout de um processo para o stdin de outro (uma forma rudimentar de se atingir inter-process communication).

Poderíamos alterar o exemplo para utilizar essas constantes e torná-lo menos verboso:

<?php

fwrite(STDOUT, 'Ei, fale algo. :D' . PHP_EOL);

while (true) {
    $line = trim(fgets(STDIN));

    if ($line === ':close') {
        fwrite(STDOUT, 'Bye!');

        break;
    }

    fwrite(STDOUT, "User says: {$line}" . PHP_EOL);
}

A relação com todos os I/O streams disponíveis no PHP você pode acessar clicando aqui.

Também poderíamos escrever direto na stream de output do PHP, no sentido de mandar pro output buffer dados sem que precisemos usar echo ou print. Por exemplo:

<?php

$ouput = fopen('php://output', 'rb');

fwrite($ouput, 'Hello World');

fclose($ouput);

Isso é o mesmo que:

<?php

echo 'Hellow World!';

O PHP implementa outros wrappers nativamente, a lista dos disponíveis pode ser obtida executando:

$ php -r "var_dump(stream_get_wrappers());"
array(12) {
  [0] => string(5) "https"
  [1] => string(4) "ftps"
  [2] => string(13) "compress.zlib"
  [3] => string(14) "compress.bzip2"
  [4] => string(3) "php"
  [5] => string(4) "file"
  [6] => string(4) "glob"
  [7] => string(4) "data"
  [8] => string(4) "http"
  [9] => string(3) "ftp"
  [10] => string(4) "phar"
  [11] => string(3) "zip"
}

As streams php://temp e php://memory

São streams de leitura e escrita que permitem o armazenamento temporário de dados na memória, com a diferença que a php://temp armazena na memória até que se atinja o limite padrão de 2MB (esse limite pode ser alterado por configuração), depois disso, os dados são armazenados em arquivo temporário no sistema de arquivos.

Elas são úteis quando se tem a necessidade de tratar um determinado dado como um stream, por exemplo:

<?php

function createStreamFromString(string $contents) {
    $stream = fopen('php://memory', 'r+b');

    fwrite($stream, $contents);
    rewind($stream);

    return $stream;
}

echo fgets(createStreamFromString('Hello World!'));

A função rewind() volta o ponteiro do arquivo para o início.

Um possível caso de uso pra essa abordagem seria, por exemplo, em memória a construção de um arquivo CSV, depois, a compactação dele (para ZIP) e o streaming disso para quem requisitar, sem envolver salvar no disco, por exemplo. No entanto, esse mesmo objetivo pode ser atingido mais facilmente usando a library ZipStream-PHP que abstrai numa API de alto nível o streaming de arquivos zips:

setSendHttpHeaders(true);

$zip = new ZipStream\ZipStream('example.zip', $options);
$zip-&gt;addFile('hello.txt', 'This is the contents of hello.txt');

$zip-&gt;finish();
PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO

A função stream_get_contents()

Identica à file_get_contents(), com a diferença que essa função retorna os dados de uma stream aberta. O exemplo anterior poderia ser reescrito para:

<?php

function createStreamFromString(string $contents) {
    $stream = fopen('php://memory', 'r+b');

    fwrite($stream, $contents);
    rewind($stream);

    return $stream;
}

echo stream_get_contents(createStreamFromString('Hello World!'));

Stream contexts

Um contexto é uma série de parâmetros e opções específicas de um determinado wrapper que pode ser informado na maioria das funções que trabalham com streams. Ele serve pra aprimorar o uso ou até mesmo alterar o comportamento de uma stream. Por exemplo, as funções file_get_contents() e file_put_contents() aceitam contexto de stream como parâmetro. Um contexto é criado usando a função stream_context_create().

O uso mais comum de contextos para streams se dá na definição de cabeçalhos HTTP usando o wrapper http. Por exemplo, crie um arquivo producer.php:

<?php

header('Content-Type: application/json');

$id = $_POST['id'];
$qtd = $_POST['qtd'];

if ( ! isset($_POST['id'], $_POST['qtd'])) {
    http_response_code(400);

    exit;
}

echo json_encode([
    'id' => $id,
    'qtd' => $qtd,
    'timestamp' => time(),
]);

E em outro arquivo, no meu caso estou usando um index.php, podemos requisitá-lo passando os dados que ele exige:

<?php

$context = stream_context_create([
    'http'=> [
        'method' => 'POST',
        'header'=> 'Content-type: application/x-www-form-urlencoded',
        'content' => http_build_query([
            'id' => 1,
            'qtd' => 10,
        ]),
    ]
]);

$json = file_get_contents('http://localhost:8000/producer.php', false, $context);

var_dump(json_decode($json, true));

Lembrando que o servidor embutido do PHP precisa estar rodando no diretório do exemplo:

$ php -S localhost:8000

E então, podemos executar:

$ php index.php
array(3) {
  'id' => string(1) "1"
  'qtd' => string(2) "10"
  'timestamp' => int(1566422525)
}

Todas as opções do contexto do wrapper http podem ser vistas em Opções de contexto do HTTP. E o índice com as opções para os diferentes wrappers pode ser visto em Opções e parâmetros de contexto.

O mesmo exemplo consumidor poderia ser reescrito para:

<?php

$context = stream_context_create([
    'http'=> [
        'method' => 'POST',
        'header'=> 'Content-type: application/x-www-form-urlencoded',
        'content' => http_build_query([
            'id' => 1,
            'qtd' => 10,
        ]),
    ]
]);

$stream = fopen('http://localhost:8000/producer.php', 'rb', false, $context);

$json = '';

while (feof($stream) === false) {
    $json .= fgets($stream);
}

var_dump(json_decode($json, true));

fclose($stream);

Ficou mais verboso, mas atingimos o mesmo resultado. Essa segunda opção utilizando fopen() é puramente didática, para mostrar que contextos podem ser trabalhados com fopen() normalmente, mas ela não é a melhor opção para esse exemplo em específico.

Uma função interessante e que pode ser útil dependendo do que se vá desenvolver é a fpassthru(), ela recebe um resource de uma stream e imprime compulsivamente tudo o que recebe dele. É como se ela fizesse um while (true) com echo fgets().

Vamos testá-la? Altere o exemplo de producer.php para:

<?php

$stream = fopen('numeros.txt', 'rb');

while (feof($stream) === false) {
    echo fgets($stream);
}

fclose($stream);

Estamos iterando e imprimindo as partes do arquivo. Agora, no index.php, vamos consumir isso:

<?php

$stream = fopen('http://localhost:8000/producer.php', 'rb');

fpassthru($stream);

fclose($stream);

Ao executar:

$ php index.php

Você verá um tsunami de números sendo impressos no seu terminal.

E, claro, nada impede que o producer.php também use fpassthru():

<?php

$stream = fopen('numeros.txt', 'rb');

fpassthru($stream);

fclose($stream);

Teremos o mesmo resultado.

Stream Filters

Um filtro performa uma transformação on-the-fly no dado que está sendo lido ou gravado em uma stream e N filtros podem ser usados ao mesmo tempo. Os filtros nativamente disponíveis podem ser listados executando:

$ php -r "var_dump(stream_get_filters());"
array(10) {
  [0] => string(6) "zlib.*"
  [1] => string(7) "bzip2.*"
  [2] => string(15) "convert.iconv.*"
  [3] => string(12) "string.rot13"
  [4] => string(14) "string.toupper"
  [5] => string(14) "string.tolower"
  [6] => string(17) "string.strip_tags"
  [7] => string(9) "convert.*"
  [8] => string(8) "consumed"
  [9] => string(7) "dechunk"
}

E, claro, podemos criar nossos próprios filtros. Mas, primeiro vamos focar em usar um filtro nativo Começaremos pelo filtro string.toupper que converte todos os dados da stream para caixa alta. Primeiro, crie um arquivo nomes.txt com o seguinte conteúdo:

<b>Thalia Bishop</b>
<b>Mckenna Carrillo</b>
<b>Wesley Curtis</b>
<b>Irene Neal</b>
<b>Norah Fuentes</b>
<b>Bailey Freeman</b>
<b>Lyric Shields</b>
<b>Chad Freeman</b>
<b>Beatrice Kline</b>
<b>Shayna Jennings</b>
<b>Clayton Potter</b>
<b>Karson Cowan</b>

E então podemos desenvolver esse exemplo:

<?php

$stream = fopen('nomes.txt', 'rb');

stream_filter_append($stream, 'string.toupper');

fpassthru($stream);

fclose($stream);

O retorno será todos os nomes em maiúsculo:

<B>THALIA BISHOP</B>
<B>MCKENNA CARRILLO</B>
<B>WESLEY CURTIS</B>
<B>IRENE NEAL</B>
<B>NORAH FUENTES</B>
<B>BAILEY FREEMAN</B>
<B>LYRIC SHIELDS</B>
<B>CHAD FREEMAN</B>
<B>BEATRICE KLINE</B>
<B>SHAYNA JENNINGS</B>
<B>CLAYTON POTTER</B>
<B>KARSON COWAN</B>

Tudo o que precisamos fazer é usar a função stream_filter_append() que adiciona um filtro na “stack” de filtros a serem executados naquela stream.

O filtro nativo string.strip_tags que aplica a função striptags() foi marcado como deprecated no PHP 7.3. Por esse motivo, vamos recriá-lo a partir de um filtro personalizado:

<?php

class StripTagsFilter extends \PHP_User_Filter
{
    public function filter($in, $out, &$consumed, $closing) : int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = strip_tags($bucket->data);

            stream_bucket_append($out, $bucket);
        }

        return PSFS_PASS_ON;
    }
}

// Registra o filtro
stream_filter_register('striptags', StripTagsFilter::class);

$stream = fopen('nomes.txt', 'rb');

stream_filter_append($stream, 'striptags');
stream_filter_append($stream, 'string.toupper');

fpassthru($stream);

fclose($stream);

O mais importante a ser notado é que a classe que criamos precisa estender \PHP_User_Filter. Os filtros trabalham com os dados no estilo de uma bucket brigade, onde cada bucket recuperado é passado de uma brigade para a outra. A função stream_bucket_make_writeable() faz algo parecido com o que a fgets() faz, só que retorna um objeto bucket que na realidade não possui um tipo específico, é um objeto genérico da stdclass. É no atributo data do bucket onde ficam os dados, é nele que fazemos as transformações que queremos. Na prática, estamos transformando os dados dos buckets da brigade $in e depois repassando esses buckets para a brigade $out. A constante PSFS_PASS_ON informa que terminamos de enviar os buckets para a brigade $out.

Você deve ter percebido que é bem confuso. O sistema de streams do PHP é algo muito poderoso e incrível. O problema está na API, em especial na de filtros, que é uma das mais confusas do PHP e não existe boa documentação para ela. Muitas vezes precisamos trabalhar com a intuição do que está acontecendo por debaixo dos panos. Por exemplo, o fato de ter que usar a função stream_bucket_make_writeable() dentro de um loop é uma recomendação da documentação do PHP, pois subentende-se que vários buckets possam ser recuperados de uma brigade, mas até hoje, em todos os exemplos que pude testar, não percebi tal necessidade.

Desenvolvedor PHP Júnior
Formação: Desenvolvedor PHP Júnior
Nesta formação você aprenderá todos os fundamentos necessário para iniciar do modo correto com a linguagem PHP, uma das mais utilizadas no mercado. Além dos conceitos de base, você também conhecerá as características e a sintaxe da linguagem de forma prática.
CONHEÇA A FORMAÇÃO

Uma abordagem moderna para se trabalhar com filtros de streams

Inspirado pela php-stream-filter, criei uma library chamada simple-stream-filter que abstrai a criação de filtros de streams, fazendo que com foquemos apenas na transformação do dado.

Vamos utilizá-la? No raiz do projeto, execute:

$ composer require kennedytedesco/simple-stream-filter

O mesmo exemplo anterior do filtro personalizado que criamos pode ser reescrito para:

<?php

require __DIR__.'/vendor/autoload.php';

use KennedyTedesco\SimpleStreamFilter\Filter;

$stream = fopen('nomes.txt', 'rb');

Filter::append($stream, static function($chunk = null) {
    return strip_tags($chunk);
});

fpassthru($stream);

fclose($stream);

Mais de um filtro pode ser aplicado:

<?php

require __DIR__.'/vendor/autoload.php';

use KennedyTedesco\SimpleStreamFilter\Filter;

$stream = fopen('nomes.txt', 'rb');

Filter::append($stream, static function($chunk = null) {
    return strip_tags($chunk);
});

Filter::append($stream, static function($chunk = null) {
    return strtoupper($chunk);
});

fpassthru($stream);

fclose($stream);

Se o desejo é aplicar o filtro apenas na leitura:

Filter::append($stream, static function($chunk = null) {
    return strip_tags($chunk);
}, STREAM_FILTER_READ);

Passe no terceiro parâmetro a constante STREAM_FILTER_READ (algo que será explicado mais abaixo).

Ou, como estamos usando funções nativas do PHP, também podemos simplesmente escrever assim:

<?php

require __DIR__.'/vendor/autoload.php';

use KennedyTedesco\SimpleStreamFilter\Filter;

$stream = fopen('nomes.txt', 'rb');

Filter::append($stream, 'strip_tags');

Filter::append($stream, 'strtoupper');

fpassthru($stream);

fclose($stream);

Dessa forma, abstraímos toda a dificuldade e verbosidade de se trabalhar com filtros básicos de streams.

Filtros na escrita de streams

Filtros também podem ser utilizados na escrita de streams. Crie um arquivo teste.txt vazio, no mesmo diretório dos exemplos. E então teste:

<?php

$stream = fopen('teste.txt', 'r+b');

stream_filter_append($stream, 'string.toupper');

fwrite($stream, 'hello world!');

Ao abrir o arquivo, você verá:

HELLO WORLD!

Mas, se a intenção é registrar um filtro apenas na escrita de uma stream, a stream_filter_append() possui um terceiro parâmetro que aceita as constantes:

  • STREAM_FILTER_WRITE
  • STREAM_FILTER_READ
  • STREAM_FILTER_ALL

A primeira marcará que o filtro só será aplicado na escrita. A segunda indicará que apenas na leitura. Se utilizarmos a constante STREAM_FILTER_READ :

<?php

$stream = fopen('teste.txt', 'r+b');

stream_filter_append($stream, 'string.toupper', STREAM_FILTER_READ);

fwrite($stream, 'hello world!');

Será escrito no arquivo:

hello world!

Não aplicou o string.toupper, ademais, exigimos que ele só fosse aplicado na leitura da stream. Se a ideia é permitir o filtro tanto pra escrita quanto pra leitura, o terceiro parâmetro pode ser ignorado ou pode se usar a terceira constante STREAM_FILTER_ALL que é o mesmo que declarar STREAM_FILTER_WRITE | STREAM_FILTER_READ, ou seja, libera tanto para escrita quanto pra leitura.

O meta-wrapper php://filter

Agora já vimos como usar e criar filtros, veremos sobre o meta-wrapper php://filter, apesar de não ser muito comum o seu uso, ele permite a aplicação de filtros junto com a abertura da stream. Útil, por exemplo, pra usar com funções como a file_get_contents() onde o registro tradicional de filtros não é aplicável, pois ela não retorna uma stream.

A sintaxe desse meta-wrapper é:

php://filter/string.toupper/resource=http://localhost:8000/nomes.txt

Informamos depois de filter/ os nomes dos filtros que serão aplicados nessa stream. Exemplo:

<?php

class StripTagsFilter extends \PHP_User_Filter
{
    public function filter($in, $out, &$consumed, $closing) : int
    {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = strip_tags($bucket->data);

            stream_bucket_append($out, $bucket);
        }

        return PSFS_PASS_ON;
    }
}

stream_filter_register('striptags', StripTagsFilter::class);

echo file_get_contents('php://filter/string.toupper|striptags/resource=nomes.txt');

Recuperamos os nomes já aplicando os filtros string.toupper e striptags (aquele registrado por nós). Da mesma forma, esse meta-wrapper pode ser usado para escrever em um arquivo, por exemplo:

<?php

file_put_contents(
    'php://filter/string.toupper/resource=teste-2.txt', 
    'hello world!'
);

O que ler a seguir?

Concluindo

Entender o essencial sobre streams pode ser uma poderosa ferramenta para resolver algumas das complexidades e dificuldades de se trabalhar com arquivos e sockets.

Recomendo também a leitura do artigo sequência deste: Stream Wrappers personalizados no PHP.

Até a próxima!