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.
Curso PHP - Fundamentos
Conhecer o cursoNã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:
film | genre | studio | year |
---|---|---|---|
(500) Days of Summer | comedy | Fox | 2009 |
27 Dresses | Comedy | Fox | 2008 |
A Dangerous Method | Drama | Independent | 2011 |
A Serious Man | Drama | Universal | 2009 |
Across the Universe | romance | Independent | 2007 |
Beginners | Comedy | Independent | 2011 |
Dear John | Drama | Sony | 2010 |
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?
Curso PHP - Recursos essenciais
Conhecer o cursoShow 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()
oustream_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 astream_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);
Curso PHP - Orientação a Objetos - Parte 1
Conhecer o cursoConcluindo
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!