PHP

API de Reflexão do PHP

O PHP implementa uma API de reflexão que permite com que façamos “engenharia reversa” para extrair e até mesmo alterar características internas de classes, interfaces, métodos e até mesmo de extensões, tudo isso em tempo de execução.

Os casos de uso para reflexão não costumam ser tão comuns no dia a dia de desenvolvimento, mas podemos destacar alguns:

  • Core de frameworks (para alguns comportamentos especiais como, por exemplo, no container de injeção de dependência para saber se determinada classe pode ser instanciada ou não);
  • Para testes (frameworks de teste usam principalmente para mocks);
  • Extrair informações de classes e gerar documentação;
  • Criar hydrators que extraiam/alimentem objetos com dados de forma dinâmica;
  • Extrair comentários de classes e criar meta-dados estendendo o comportamento delas (anotações);

Tudo sobre a API de reflexão você pode consultar direto na documentação oficial do PHP. Nesse artigo vou passar os aspectos que considero principais.

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

Um exemplo elementar pra dar a ideia do que é possível extrair:

<?php

/** @psalm-immutable */
final class Email
{
    public string $email;

    public function __construct(string $email)
    {
        $this->email = $email;
    }
}

$reflectionClass = new ReflectionClass('Email');

echo $reflectionClass->getName() . PHP_EOL;
echo ($reflectionClass->isFinal() ? 'Final' : 'Not final') . PHP_EOL;
echo $reflectionClass->getDocComment() . PHP_EOL;
echo $reflectionClass->getConstructor()->getNumberOfParameters() . PHP_EOL;

echo PHP_EOL . 'Property:' . PHP_EOL . PHP_EOL;

/** @var ReflectionProperty $property */
$property = $reflectionClass->getProperties()[0];
echo $property->getName() . PHP_EOL;
echo ($property->isPrivate() ? 'public' : 'not public') . PHP_EOL;
echo $property->getType() . PHP_EOL;

O resultado da execução desse exemplo:

Email
Final
/** @psalm-immutable */
1

Property:

email
not public
string

A ReflectionClass é responsável por extrair os dados de uma classe. Extraímos o nome da classe, se ela é final, o comentário associado a ela e o número de argumentos do seu construtor. O método getConstructor() retorna uma instância de ReflectionMethod. Depois, usando getProperties() , obtemos um array com os atributos da classe, mas na forma de instâncias de ReflectionProperty, que provê outros métodos de acesso.

Se quisermos dar uma espécie de var_dump() em uma classe:

<?php

/** @psalm-immutable */
final class Email
{
    public string $email;

    public function __construct(string $email)
    {
        $this->email = $email;
    }
}

Reflection::export(new ReflectionClass('Email'));

O resultado será:

/** @psalm-immutable */
Class [ <user> final class Email ] {
  @@ /home/kennedy/Documents/www/php-reflection/index.php 4-12

  - Constants [0] {
  }

  - Static properties [0] {
  }

  - Static methods [0] {
  }

  - Properties [1] {
    Property [ <default> public $email ]
  }

  - Methods [1] {
    Method [ <user, ctor> public method __construct ] {
      @@ /home/kennedy/Documents/www/php-reflection/index.php 8 - 11

      - Parameters [1] {
        Parameter #0 [ <required> string $email ]
      }
    }
  }
}

Podemos extrair diretamente as informações de um método sem precisar usar a ReflectionClass:

<?php

final class Node
{
    private Node $next;

    private function next(): Node
    {
        return $this->next;
    }
}

$reflectionMethod = new ReflectionMethod('Node', 'next');
echo $reflectionMethod->getName() . PHP_EOL; // next
echo ($reflectionMethod->isPrivate() ? 'private' : 'not private') . PHP_EOL; // private
echo $reflectionMethod->getReturnType() . PHP_EOL; // Node

Da mesma forma que ReflectionClass, também temos a ReflectionFunction:

<?php

function foo(int $bar) : int {
    return $bar + 1;
}

$reflectionFunction = new ReflectionFunction('foo');

echo $reflectionFunction->getName() . PHP_EOL; // foo
echo $reflectionFunction->getReturnType() . PHP_EOL; // int

$parameter = $reflectionFunction->getParameters()[0];
echo $parameter->getName() . PHP_EOL; // bar
echo $parameter->getType() . PHP_EOL; // int

var_dump($reflectionFunction->getClosure()); // class Closure#2 (1) {

Outra classe muito importante do “combo” é a ReflectionObject, que trabalha diretamente em uma instância de objeto:

<?php

final class Node
{
    private int $value;
    private ?Node $next;

    public function __construct(int $value, ?Node $next = null)
    {
        $this->value = $value;
        $this->next = $next;
    }

    private function next(): Node
    {
        return $this->next;
    }

    public function value(): int
    {
        return $this->value;
    }
}

$node = new Node(1, new Node(2));

$reflectionObject = new ReflectionObject($node);
echo $reflectionObject->getName() . PHP_EOL; // Node
echo $reflectionObject->getConstructor()->getNumberOfParameters() . PHP_EOL; // 2
echo $reflectionObject->getMethod('next')->getReturnType() . PHP_EOL; // Node
echo $reflectionObject->getMethod('value')->getReturnType() . PHP_EOL; // int

Existem outras classes e elas possuem outros vários métodos a serem explorados. Mas o que vimos até aqui é o essencial para que avancemos um pouco mais em exemplos palpáveis, do mundo real.

Um caso prático pra uso de reflexão

Criaremos uma classe transporter base. Um transporter é um conceito para transacionar dados (do input do usuário, por exemplo) entre objetos. Se você já ouviu falar de DTO (Data Transfer Objects), é basicamente isso, mas com outro nome, só pra causar confusão mesmo. 😛

Mas deixando a ladainha de lado, show me the code:

<?php

declare(strict_types=1);

abstract class Transporter
{
    public function __construct(array $properties = [])
    {
        $reflection = new ReflectionObject($this);
        foreach ($properties as $name => $value) {
            $property = $reflection->getProperty($name);
            if ($property->isPublic() || !$property->isStatic()) {
                $this->$name = $value;
            }
        }
    }

    public function toArray(): array
    {
        $reflection = new ReflectionObject($this);
        return \array_map(
            function (ReflectionProperty $property) {
                return [
                    $property->getName() => $property->getValue($this)
                ];
            },
            $reflection->getProperties(ReflectionProperty::IS_PUBLIC)
        );
    }
}

final class UserTransporter extends Transporter
{
    public int $age;
    public string $firstName;
    public string $lastName;
}

$transporter = new UserTransporter([
    'age' => 29,
    'firstName' => 'Kennedy',
    'lastName' => 'Parreira',
]);

echo $transporter->age . PHP_EOL;
echo $transporter->firstName . PHP_EOL;
echo $transporter->lastName . PHP_EOL;

var_dump($transporter->toArray());

O resultado:

29
Kennedy
Parreira
/home/kennedy/Documents/www/php-reflection/index.php:50:
array(3) {
  [0] =>
  array(1) {
    'age' =>
    int(29)
  }
  [1] =>
  array(1) {
    'firstName' =>
    string(7) "Kennedy"
  }
  [2] =>
  array(1) {
    'lastName' =>
    string(8) "Parreira"
  }
}

Esse exemplo usou basicamente o que já tínhamos visto anteriormente. Ele permite que recebamos os dados via array (no construtor) e então alimenta os atributos do objeto dinamicamente. O método toArray() itera nos atributos públicos e não estáticos da classes e os retorna em um array.

Queue jobs do Laravel

Outro caso de uso interessante de se passar aqui, pra mostrar como reflexão pode ser útil em código de framework, refere-se às Queue Jobs do Laravel:

<?php

namespace App\Jobs;

use App\Podcast;
use App\AudioProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class ProcessPodcast implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $podcast;

    public function __construct(Podcast $podcast)
    {
        $this->podcast = $podcast;
    }

    public function handle(AudioProcessor $processor)
    {
        // Process uploaded podcast...
    }
}

Veja que essa classe implementa a interface ShouldQueue. No Dispacher de jobs tem um método que recebe o nome da classe pra decidir se ela deve entrar na fila ou não, e esse método usa reflexão para verificar se a classe implementa a interface ShouldQueue:

protected function handlerShouldBeQueued($class)
{
    try {
        return (new ReflectionClass($class))->implementsInterface(
            ShouldQueue::class
        );
    } catch (Exception $e) {
        return false;
    }
}

Nesse contexto específico é melhor usar reflexão que futilmente gerar uma instância da classe para depois verificar se o objeto gerado implementa a requerida interface. Se fôssemos implementar instanciando a classe, ficaria mais ou menos assim:

protected function handlerShouldBeQueued($class)
{
    $object = new $class();

    return $object instanceof ShouldQueue;
}

(E note que nesse protótipo nem nos preocupamos com as possíveis dependências que o construtor de $class poderia precisar receber).

Anotações

Você já deve ter visto libraries/componentes que estendem seus comportamentos a partir de anotações. Uma delas é a Annotations, para Laravel, que permite definir comportamentos diretamente por anotações nas classes, por exemplo:


/** * @Middleware("guest", except={"logout"}, prefix="/your/prefix") */ class AuthController extends Controller { /** * @Get("logout", as="logout") * @Middleware("auth") */ public function logout() { $this->auth->logout(); return redirect(route('login')); } }

E usa-se reflexão para obter essas anotações.

Palavras finais

É bem provável que no dia a dia de desenvolvimento no código de aplicação não usemos reflexão, mas são uma importante ferramenta, principalmente para libraries e frameworks.

Até a próxima!

Desenvolvedor PHP Pleno
Formação: Desenvolvedor PHP Pleno
Nesta formação você aprenderá aspectos mais avançados da linguagem PHP, indo adiante nas características da linguagem, orientação a objetos, práticas de mercado, além da parte de integração com banco de dados. Ao final, estará apto a desenvolver sistemas usando banco de dados relacionais.
CONHEÇA A FORMAÇÃO

Generators no PHP

iterGenerators foram adicionados no PHP na versão 5.5 (meados de 2013) e aqui estamos nós, anos depois, falando sobre eles. É que esse ainda não é um assunto muito difundido, digo, não é trivial encontrar casos e mais casos de uso para eles no contexto padrão de desenvolvimento para o PHP. Normalmente o uso deles fica abstraído em libraries e frameworks.

Mas, antes de tudo, é importante entendermos o que são Iterators, ademais, Iterators e Generators são assuntos intrinsecamente relacionados.

Para isso, recomendo a leitura do artigo: Iterators no PHP

Todos os exemplos desse artigo estão disponíveis no repositório: https://github.com/KennedyTedesco/php-iterators-generators

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

O que é um Generator?

Numa explicação acessível, Generators são uma forma prática de se implementar Iterators. Isso quer dizer que, com Generators, podemos implementar complexos Iterators sem que precisemos criar objetos, implementar interfaces e tudo mais que vimos anteriormente. Generators dão para uma função a capacidade de retornar uma sequência de valores.

Para criar uma função Generator, basta que ela possua a palavra reservada yield. O operador yield é uma espécie de return, só que com algumas particularidades. E uma função desse tipo retorna um objeto da classe Generator, que é uma classe especial e específica para esse contexto, não sendo possível utilizá-la de outra forma. Esse objeto retornado pode ser iterado. É aí que entra a nossa base sobre Iterators que aprendemos anteriormente. A classe Generator implementa a interface Iterator.

Vejamos o exemplo mais elementar possível de uma função generator:

<?php

declare(strict_types=1);

function getBooks(): Generator
{
    yield 'Book 1';
    yield 'Book 2';
    yield 'Book 3';
    yield 'Book 4';
    yield 'Book 5';
}

foreach (getBooks() as $book) {
    echo $book . \PHP_EOL;
}

O resultado:

Book 1
Book 2
Book 3
Book 4
Book 5

Sempre que uma função usar o operador yield ela vai retornar um objeto do tipo Generator. E, se Generator é um Iterator, logo, ele pode ser iterado.

Não precisamos de um yield para cada registro da nossa coleção. Normalmente o que vamos ver é um yield dentro de um laço:

<?php

declare(strict_types=1);

function getBooks(): Generator
{
    for ($i = 0; $i < 100; $i++) {
        yield "Book {$i}";
    }
}

foreach (getBooks() as $book) {
    echo $book . \PHP_EOL;
}

E é possível que o yield retorne um conjunto de chave => valor:

<?php

declare(strict_types=1);

function getBooks(): Generator
{
    for ($i = 0; $i < 10; $i++) {
        yield $i * 2 => "Book {$i}";
    }
}

foreach (getBooks() as $key => $value) {
    echo "{$key} -> {$value}" . \PHP_EOL;
}

O resultado:

0 -> Book 0
2 -> Book 1
4 -> Book 2
6 -> Book 3
8 -> Book 4
10 -> Book 5
12 -> Book 6
14 -> Book 7
16 -> Book 8
18 -> Book 9

E a parte que “toca” a memória?

Um dos benefícios de se usar generators está no baixo consumo de memória associado. Com um generator recuperamos a informação sob demanda, sem alocar toda a coleção na memória. Trabalhamos com um dado por vez no buffer.

É relativamente comum aplicações que precisam trabalhar em grandes massas de dados. Vamos emular uma situação onde possamos visualizar essa relação do uso eficiente de memória?

<?php

declare(strict_types=1);

function getRecords(): array
{
    $records = [];
    for ($i = 0; $i < 100_000; $i++) {
        $records[] = "Record $i";
    }
    return $records;
}

function formatRecords(array $records): array
{
    $new = [];
    foreach ($records as $index => $record) {
        $new[] = "[$index] -> {$record}";
    }
    return $new;
}

$registros = formatRecords(getRecords());
foreach ($registros as $registro) {
    echo $registro . \PHP_EOL;
}

echo 'Used memory: ~' . \round((\memory_get_peak_usage()/1024/1024), 2) . 'MB';

A getRecords() aloca 100k registros na memória e os retorna. A formatRecords() recebe uma coleção de dados, formata-os, aloca-os na memória (em um novo array) e então os retorna.

O resultado da memória consumida por esse script foi:

Used memory: ~16.78MB

Agora, refatorando o exemplo para que as duas funções se transformem em generators:

<?php

declare(strict_types=1);

function getRecords(): Generator
{
    for ($i = 0; $i < 100_000; $i++) {
        yield "Record $i";
    }
}

function formatRecords(Generator $records): Generator
{
    foreach ($records as $index => $record) {
        yield "[$index] -> {$record}";
    }
}

$registros = formatRecords(getRecords());
foreach ($registros as $registro) {
    echo $registro . \PHP_EOL;
}

echo 'Used memory: ~' . \round((\memory_get_peak_usage()/1024/1024), 2) . 'MB';

O resultado:

Used memory: ~0.41MB

Uma diferença muito grande. Quanto maior o dataset, maior a diferença será.

Lendo grandes arquivos

Vamos supor que precisamos ler um documento de texto linha a linha. Uma das formas tradicionais de lidar com isso seria assim:

<?php

declare(strict_types=1);

function getLines(string $filePath): array
{
    $file = \fopen($filePath, 'rb');

    $lines = [];
    while (!\feof($file)) {
        $lines[] = \fgets($file);
    }

    \fclose($file);

    return $lines;
}

$lines = getLines(__DIR__.'/file.txt');

foreach ($lines as $line) {
    echo $line;
}

echo PHP_EOL . 'Used memory: ~' . \round((\memory_get_peak_usage()/1024/1024), 2) . 'MB';

Se o seu sistema é unix-based você pode gerar um arquivo com dados randômicos de 10MB (para testar o exemplo) executando:

base64 /dev/urandom | head -c 10000000 > file.txt

O resultado dessa execução no meu ambiente foi:

Used memory: ~18.26MB

Se refatorarmos o exemplo para:

<?php

declare(strict_types=1);

function getLines(string $filePath): Generator
{
    $file = \fopen($filePath, 'rb');

    while (!\feof($file)) {
        yield \fgets($file);
    }

    \fclose($file);
}

$lines = getLines(__DIR__.'/file.txt');

foreach ($lines as $line) {
    echo $line;
}

echo PHP_EOL . 'Used memory: ~' . \round((\memory_get_peak_usage()/1024/1024), 2) . 'MB';

O resultado será:

Used memory: ~0.41MB

Veja que a diferença do consumo de memória entre as duas abordagens é enorme.

Usando iterators e generators

No artigo sobre Iterators vimos sobre a interface SeekableIterator. Agora, vamos criar um Iterator personalizado que lê linha a linha de um arquivo e que também nos dará a opção de pular para uma linha arbitrária qualquer.

A classe se chama LineFileIterator:

<?php

declare(strict_types=1);

namespace Iterators;

use Generator;
use OutOfBoundsException;
use SeekableIterator;

final class LineFileIterator implements SeekableIterator
{
    private string $filePath;
    private Generator $generator;

    public function __construct(string $filePath)
    {
        $this->filePath = $filePath;
        $this->rewind();
    }

    public function rewind(): void
    {
        $this->generator = $this->getGenerator();
    }

    public function current()
    {
        return $this->generator->current();
    }

    public function key(): int
    {
        return (int) $this->generator->key();
    }

    public function next(): void
    {
        $this->generator->next();
    }

    public function valid(): bool
    {
        return $this->generator->valid();
    }

    public function seek($position): void
    {
        while ($this->valid()) {
            if ($this->generator->key() === $position) {
                return;
            }

            $this->generator->next();
        }

        throw new OutOfBoundsException("Invalid position ($position)");
    }

    private function getGenerator(): Generator
    {
        $file = \fopen($this->filePath, 'rb');

        while (!\feof($file)) {
            yield \fgets($file);
        }

        \fclose($file);
    }
}

Usando o mesmo arquivo de texto que criamos anteriormente, podemos testar essa implementação assim:

<?php

declare(strict_types=1);

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

use Iterators\LineFileIterator;

$iterator = new LineFileIterator(__DIR__ . '/file.txt');

// Set the pointer to the line 50.000
$iterator->seek(50_000);

// Get the current line
echo $iterator->current();

// Move to the next line (50.001)
$iterator->next();

// Get the current line
echo $iterator->current();

echo 'Used memory: ~' . \round((\memory_get_peak_usage()/1024/1024), 2) . 'MB';

Nesse nosso exemplo construímos um iterator que por debaixo dos panos usa generators para acessar sob demanda linhas de um arquivo.

Palavras finais

O uso de Generators é indicado e muitas vezes se faz necessário quando é preciso iterar uma grande coleção de dados. Eles são, inclusive, base para programação assíncrona e um framework que se destaca utilizando-os é o AMP:

$iterator = new Producer(function (callable $emit) {
    yield $emit(1);
    yield $emit(new Delayed(500, 2));
    yield $emit(3);
    yield $emit(4);
});

Inclusive, também escrevi um artigo sobre Corrotinas e código assíncrono em PHP usando Generators, recomendo a leitura.

E ainda sobre programação assíncrona, recomendo a leitura dos artigos:

Até a próxima!

Desenvolvedor PHP Pleno
Formação: Desenvolvedor PHP Pleno
Nesta formação você aprenderá aspectos mais avançados da linguagem PHP, indo adiante nas características da linguagem, orientação a objetos, práticas de mercado, além da parte de integração com banco de dados. Ao final, estará apto a desenvolver sistemas usando banco de dados relacionais.
CONHEÇA A FORMAÇÃO

Iterators no PHP

Esse é o primeiro artigo de dois sobre Iterators e Generators no PHP. Este será sobre Iterators, que é base para que possamos entender e usar Generators.

Todos os exemplos desse artigo e do artigo de Generators estão disponíveis nesse repositório: https://github.com/KennedyTedesco/php-iterators-generators

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

Iterators

Nós, como desenvolvedores PHP, estamos acostumados a usar arrays para trabalhar e iterar sobre listas. E se pudermos encapsular esse tipo de comportamento em um objeto? No contexto prático do PHP, Iterator é um mecanismo que permite que um objeto seja iterado e ele próprio fica no controle granular dessa iteração. Mas o seu uso não limita a essa “feature”.

Antes, entretanto, é importante destacar o conceito do que é “iterável” no PHP. Existe, inclusive, um pseudo-tipo chamado iterable que aceita arrays e objetos que implementem a interface Traversable, mas isso veremos mais pra frente.

Então, de forma prática, tudo o que você pode iterar num foreach é considerável iterável. E existe até uma curiosidade aqui, de algo não tão comum, é possível iterar até sobre os atributos públicos de um objeto arbitrário qualquer:

<?php

declare(strict_types=1);

function iterateAndPrint($list): void
{
    foreach ($list as $key => $value) {
        echo "{$key} -> {$value}\n";
    }
}

iterateAndPrint(['foo', 'bar', 'baz']);

iterateAndPrint(new class() {
    public int $foo = 1;
    public string $bar = 'bar';
    protected string $baz = 'baz';
});

O resultado:

0 -> foo
1 -> bar
2 -> baz
foo -> 1
bar -> bar

No caso desse exemplo iteramos tanto sobre um array quanto sobre uma instância de uma classe anônima. Então, podemos dizer que essas duas entidades são iteráveis.

A interface Iterator

Essa interface declara cinco métodos e, quando implementados, os objetos provenientes ganham a capacidade de serem iterados. Mais que isso, ganham o poder sobre o que e como iterar.

Iterator extends Traversable {
    abstract public current ( void ) : mixed
    abstract public key ( void ) : scalar
    abstract public next ( void ) : void
    abstract public rewind ( void ) : void
    abstract public valid ( void ) : bool
}
  • current() – Retorna elemento atual;
  • key() – Obtêm a chave atual;
  • next() – Avança o cursor para o próximo elemento;
  • rewind() – Retorna o cursor para o início (ponto zero);
  • valid() – Retorna true se o ponteiro (posição) encontra-se numa faixa acessível dos dados (se não passou da posição máxima disponível, por exemplo). Caso contrário, false é retornado.

Vamos então a um exemplo:

<?php

declare(strict_types=1);

namespace Iterators;

use Iterator;

final class BookStoreIterator implements Iterator
{
    /** @var string[] $books */
    private array $books;
    private int $index = 0;

    /**
     * @param string[] $books
     */
    public function __construct(array $books)
    {
        $this->books = $books;
    }

    public function current(): string
    {
        return $this->books[$this->index];
    }

    public function key(): int
    {
        return $this->index;
    }

    public function next(): void
    {
        $this->index++;
    }

    public function rewind(): void
    {
        $this->index = 0;
    }

    public function valid(): bool
    {
        return \array_key_exists($this->index, $this->books);
    }
}

Observe que os atributos são privados e que mantemos em $index o “cursor” atual da iteração.

Uma possível forma de iterar sobre esse objeto BookStoreIterator:

<?php

declare(strict_types=1);

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

use Iterators\BookStoreIterator;

$books = new BookStoreIterator([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
    'Book 5',
    'Book 6',
    'Book 7',
    'Book 8',
    'Book 9',
    'Book 10',
]);

while ($books->valid()) {
    echo "{$books->key()} -> {$books->current()} \n";

    $books->next();
}

Resultado:

0 -> Book 1 
1 -> Book 2 
2 -> Book 3 
3 -> Book 4 
4 -> Book 5 
5 -> Book 6 
6 -> Book 7 
7 -> Book 8 
8 -> Book 9 
9 -> Book 10

Essa interface faz com que o objeto tenha a capacidade de decidir como e o que iterar. Destaco novamente esses termos em negrito pois isso é essência dos iterators.

Enquanto existir um índice válido a ser recuperado os valores serão impressos. Na última iteração, $books->next() incrementa em mais um o valor de $index chegando ao valor 10, no entanto, no array $books o maior índice existente é o 9, logo, o while é interrompido.

Outra forma de iterar esse objeto usando a estrutura for:

for ($books->rewind(); $books->valid(); $books->next()) {
    echo "{$books->key()} -> {$books->current()} \n";
}

Essas são as formas mais “primitivas” de iterar esse objeto iterável. Estamos manualmente cuidando de avançar o cursor usando $books->next();.

Podemos facilitar isso se usarmos a estrutura foreach, ela reconhece quando o objeto implementa a interface Traversable e trata de avançar o cursor e recuperar os valores automaticamente.

É bom lembrar que a interface Iterator estende a Traversable:

Iterator extends Traversable {
    // ...
}

A iteração poderia ser simplificada para:

foreach ($books as $key => $value) {
    echo "{$key} -> {$value} \n";
}

Esse é um exemplo simples, que usa arrays. Mas a grande sacada dos iterators é nos dar o poder sobre como e o que iterar, ou seja, ficamos no poder de definir o comportamento de toda a iteração.

Refatorando para ArrayIterator

O lado chato da abordagem de implementar diretamente a interface Iterator é a necessidade de implementar todos os seus métodos. É útil quando a lógica para se iterar ou avançar o cursor precisa ser específica. Agora, se ela é trivial, como é o caso do nosso exemplo anterior, podemos usar a classe ArrayIterator, que faz parte do conjunto de Iterators da Standard PHP Library (SPL) do PHP.

Ela recebe no seu construtor um array e itera sobre ele, abstraindo de nós a necessidade de implementar os métodos necessários da interface Iterator.

Podemos simplificar e alterar a nossa classe para implementar a interface IteratorAggregate que assina um único método, chamado getIterator(). E, nele, tudo o que temos que retornar é um Iterator, no caso, vamos retornar a instância da ArrayIterator:

<?php

declare(strict_types=1);

namespace Iterators;

use ArrayIterator;
use IteratorAggregate;

final class BookStoreArrayIterator implements IteratorAggregate
{
    /** @var string[] $books */
    private array $books;

    /**
     * @param string[] $books
     */
    public function __construct(array $books)
    {
        $this->books = $books;
    }

    public function getIterator(): ArrayIterator
    {
        return new ArrayIterator($this->books);
    }
}

Exemplo de uso:

<?php

declare(strict_types=1);

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

use Iterators\BookStoreArrayIterator;

$books = new BookStoreArrayIterator([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
    'Book 5',
    'Book 6',
    'Book 7',
    'Book 8',
    'Book 9',
    'Book 10',
]);

foreach ($books as $key => $value) {
    echo "{$key} -> {$value} \n";
}

A classe ArrayObject

Nos exemplos anteriores vimos como encapsulamos arrays em objetos com iterators. Mas a forma mais simples e elementar de implementar um array como objeto é usando a classe ArrayObject:

<?php

declare(strict_types=1);

$books = new ArrayObject([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
    'Book 5',
    'Book 6',
    'Book 7',
    'Book 8',
    'Book 9',
    'Book 10',
]);

foreach ($books as $key => $value) {
    echo "{$key} -> {$value} \n";
}

Essa classe implementa diversas interfaces e ela já abstrai tudo pra gente:

ArrayObject implements IteratorAggregate , Traversable , ArrayAccess , Countable {

Não é o assunto do nosso artigo, mas a interface ArrayAccess nos fornece os seguintes métodos:

ArrayAccess {
    abstract public offsetExists ( mixed $offset ) : bool
    abstract public offsetGet ( mixed $offset ) : mixed
    abstract public offsetSet ( mixed $offset , mixed $value ) : void
    abstract public offsetUnset ( mixed $offset ) : void
}

Por exemplo, na instância do ArrayObject podemos acessar:

$books->offsetExists(1) // true

A ArrayObject também encapsula várias funções úteis para manipulação do Iterator:

  • append()
  • asort()
  • count()
  • ksort()
  • natcasesort()
  • natsort()
  • serialize()
  • uasort()
  • uksort()
  • unserialize()

Exemplo:

<?php

declare(strict_types=1);

$list = new ArrayObject([
    5, 4, 3, 2, 1,
]);

for ($i = 5; $i <= 10; $i++) {
    $list->append($i);
}

// Ordena os valores
$list->natsort();

foreach ($list as $key => $value) {
    echo "{$key} -> {$value}\n";
}

Resultado:

4 -> 1
3 -> 2
2 -> 3
1 -> 4
0 -> 5
5 -> 5
6 -> 6
7 -> 7
8 -> 8
9 -> 9
10 -> 10

Podemos, ainda, no segundo argumento da ArrayObject passar a flag ArrayObject::ARRAY_AS_PROPS para permitir que acessemos as chaves do array na forma de objeto:

<?php

declare(strict_types=1);

$list = new ArrayObject(
    [
        'nome' => 'Pedro',
        'idade' => 20,
        'nascimento' => '1990-10-10',
    ],
    ArrayObject::ARRAY_AS_PROPS
);

echo "{$list->nome}, {$list->idade} ({$list->nascimento})";

Resultado:

Pedro, 20 (1990-10-10)

A função iterator_to_array()

Essa função faz uma cópia dos itens do Iterator para um array:

<?php

declare(strict_types=1);

$books = new ArrayObject([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
    'Book 5',
    'Book 6',
    'Book 7',
    'Book 8',
    'Book 9',
    'Book 10',
]);

var_dump(iterator_to_array($books));

O pseudo-tipo iterable

O PHP implementou na versão 7.1 o pseudo-tipo iterable, que espera receber um array ou algum objeto que implementa a interface Traversable.

Exemplo:

<?php

declare(strict_types=1);

function iterateAndPrint(iterable $list): void
{
    foreach ($list as $key => $value) {
        echo "{$key} -> {$value}\n";
    }
}

iterateAndPrint(['foo', 'bar', 'baz']);

$books = new ArrayObject([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
]);

iterateAndPrint($books);

Observe que a função iterateAndPrint() agora espera receber uma entidade do pseudo-tipo iterable.

O resultado:

0 -> foo
1 -> bar
2 -> baz
0 -> Book 1
1 -> Book 2
2 -> Book 3
3 -> Book 4

A interface SeekableIterator

Vimos anteriormente que a interface Iterator declara cinco métodos:

Iterator extends Traversable {
    abstract public current ( void ) : mixed
    abstract public key ( void ) : scalar
    abstract public next ( void ) : void
    abstract public rewind ( void ) : void
    abstract public valid ( void ) : bool
}

A interface SeekableIterator é uma extensão da Iterator que adiciona o método seek():

SeekableIterator extends Iterator {
    /* Novo método */
    abstract public seek (int $position) : void

    /* Métodos herdados */
    abstract public Iterator::current(void) : mixed
    abstract public Iterator::key(void) : scalar
    abstract public Iterator::next(void) : void
    abstract public Iterator::rewind(void) : void
    abstract public Iterator::valid(void) : bool
}

Esse método espera receber um valor para mudar a posição do ponteiro interno do iterator. Vamos supor que o iterator está na posição 0 e arbitrariamente queremos pular para a posição 4, esse método seek() pode ser usado para isso.

Vejamos um exemplo. A classe:

<?php

declare(strict_types=1);

namespace Iterators;

use OutOfBoundsException;
use SeekableIterator;

final class BookStoreSeekableIterator implements SeekableIterator
{
    /** @var string[] $books */
    private array $books;
    private int $index = 0;

    /**
     * @param string[] $books
     */
    public function __construct(array $books)
    {
        $this->books = $books;
    }

    public function seek($index): void
    {
        if (! isset($this->books[$index])) {
            throw new OutOfBoundsException("Invalid position ({$index}).");
        }

        $this->index = $index;
    }

    public function current(): string
    {
        return $this->books[$this->index];
    }

    public function key(): int
    {
        return $this->index;
    }

    public function next(): void
    {
        $this->index++;
    }

    public function rewind(): void
    {
        $this->index = 0;
    }

    public function valid(): bool
    {
        return \array_key_exists($this->index, $this->books);
    }
}

O uso da classe:

<?php

declare(strict_types=1);

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

use Iterators\BookStoreSeekableIterator;

$books = new BookStoreSeekableIterator([
    'Book 1',
    'Book 2',
    'Book 3',
    'Book 4',
    'Book 5',
    'Book 6',
    'Book 7',
    'Book 8',
    'Book 9',
    'Book 10',
]);

echo $books->current() . \PHP_EOL; // Book 1

$books->seek(4);

echo $books->current() . \PHP_EOL; // Book 5

O resultado:

Book 1
Book 5

O seek() permite que arbitrariamente pulemos a posição do ponteiro interno.

Esse é um exemplo puramente didático para termos acesso à essência de como o seek() pode ser usado. Mas no artigo sobre Generators (que muito recomendo a leitura), temos um exemplo onde o seek() tem um sentido de uso real bem mais amplo.

Iterators da SPL

Temos na Standard PHP Library (SPL) dezenas de iterators disponíveis para uso, cada qual com seu caso de uso. Você pode ver a relação deles clicando aqui.

Um exemplo de uso do FilesystemIterator:

<?php

declare(strict_types=1);

$iterator = new FilesystemIterator(__DIR__.'/../');

/** @var SplFileInfo $file */
foreach ($iterator as $file) {
    echo $file->getFilename() . ' -> ' . ($file->isFile() ? 'file' : 'dir') . PHP_EOL;
}

O resultado da iteração:

.php_cs.cache -> file
.php_cs -> file
.gitignore -> file
composer.json -> file
.git -> dir
src -> dir
psalm.xml -> file
examples -> dir
.idea -> dir
vendor -> dir
composer.lock -> file

Outro exemplo é o RegexIterator que aplica um filtro de regex em cima de outro Iterator, por exemplo:

<?php

declare(strict_types=1);

$iterator = new RegexIterator(
    new FilesystemIterator(__DIR__),
    '/^.+\.php$/'
);

/** @var SplFileInfo $file */
foreach ($iterator as $file) {
    echo $file->getFilename() . PHP_EOL;
}

Esse exemplo vai retornar o nome de todos os arquivos que terminam com a extensão .php.

Outra opção seria usar FilterIterator que permite que criemos filtros e os apliquemos em cima de Iterators. Para isso, vamos criar uma classe que estende a FilterIterator:

<?php

declare(strict_types=1);

namespace Iterators;

use FilterIterator;
use Iterator;
use SplFileInfo;

class FileExtensionFilter extends FilterIterator
{
    private string $extension;

    public function __construct(Iterator $iterator, string $extension)
    {
        parent::__construct($iterator);

        $this->extension = $extension;
    }

    public function accept(): bool
    {
        /** @var SplFileInfo $file */
        $file = $this->getInnerIterator()->current();

        return $this->extension === $file->getExtension();
    }
}

E então podemos usar:

<?php

declare(strict_types=1);

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

use Iterators\FileExtensionFilter;

$iterator = new FileExtensionFilter(new FilesystemIterator(__DIR__), 'php');

/** @var SplFileInfo $file */
foreach ($iterator as $file) {
    echo $file->getFilename() . \PHP_EOL;
}

O resultado desse exemplo é exatamente o mesmo do exemplo anterior.

A classe FileExtensionFilter estende a FilterIterator que é um OuterIterator. A ideia é que um OuterIterator possui um InnerIterator (um iterator interno). Por esse motivo dentro do método accept() da classe de filtro conseguimos acessar o InnerIterator que é o FilesystemIterator e então obter a extensão do arquivo do ponteiro atual.

Palavras finais

Um iterator pode iterar recursivamente sobre outros iterators. As possibilidades são muitas quando se trata de agregar iterators. Não é possível em um artigo colocar todos os exemplos possíveis, por isso eu recomendo navegar na documentação e usar na prática os iterators que mais lhe chamam atenção.

Não se esqueça de ler o artigo sobre Generators, lá você terá exemplos bem legais do mundo real e verá como eles são eficientes para lidar com a memória.

Desenvolvedor PHP Pleno
Formação: Desenvolvedor PHP Pleno
Nesta formação você aprenderá aspectos mais avançados da linguagem PHP, indo adiante nas características da linguagem, orientação a objetos, práticas de mercado, além da parte de integração com banco de dados. Ao final, estará apto a desenvolver sistemas usando banco de dados relacionais.
CONHEÇA A FORMAÇÃO

Estrutura interna de valor, referências e gerenciamento de memória no PHP

A forma com o que o PHP trabalha com referências e gerenciamento de memória não é um assunto muito popular e é compreensível que não seja, se levarmos em conta que na maior parte do tempo como desenvolvedores não precisamos nos preocupar com isso, ademais, o PHP implementa um garbage collector que cuida da contagem das referências e decide o melhor momento de liberar a memória, sem impactar na experiência do desenvolvedor.

Quando falo em “experiência do desenvolvedor”, me refiro ao fato de não precisarmos a todo momento usar unset() para destruir variáveis não mais utilizadas, tampouco passar estruturas mais complexas como objetos ou arrays por referência a todo momento etc. Há raros e específicos casos em que usar referência faz sentido. Como também há raros e específicos casos em que liberar a memória manualmente (sem aguardar o trabalho do garbage collector) faz sentido como, por exemplo, a liberação de um resource para alguma operação subsequente importante que vai consumir um tanto considerável de memória.

A ideia aqui é tentar ao máximo ter uma abordagem leve, de desenvolvedor para desenvolvedor, no sentido de usuários da linguagem. Não tenho a pretenção e nem o conhecimento necessário para explicar o funcionamento interno do PHP e, além do mais, existe gente muito boa e mais preparada pra isso (compartilharei links no final do artigo).

Em suma, esse artigo se propõe a responder as seguintes questões:

  • Como um valor é representado internamente no PHP?
  • Valores em PHP possuem que semântica? De valor ou de referência?
  • Referências? O que são?
  • Objetos são passados por referência?
  • Se tenho um array com milhões de registros e preciso passá-lo para o argumento de uma função, e agora? Vou dobrar o tanto de memória que estou usando?
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

A estrutura de dado que representa os valores do PHP

No PHP existe uma estrutura de dado elementar chamada zval (zend value), ela é uma estrutura container para qualquer valor arbitrário que se trabalha no PHP:

struct _zval_struct {
    zend_value value;
    union {
        // ...
        uint32_t type_info;
    } u1;
    // ...
};

Para a nossa noção, o mais importante é notarmos que essa estrutura armazena o valor e o tipo desse valor. E, pelo fato de o PHP ser uma linguagem de tipagem dinâmica, o tipo é conhecido apenas em tempo de execução e não em tempo de compilação. Além disso, esse tipo pode mudar durante a execução, por exemplo, um inteiro pode simplesmente virar uma string em um dado momento.

O valor propriamente dito é armazenado na union zend_value (uma union define múltiplos membros de diferentes tipos mas só um deles pode ser ativo por vez):

typedef union _zend_value {
    zend_long         lval;             /* long value */
    double            dval;             /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

Por essa estrutura já podemos inferir os tipos internos das variáveis que declaramos. Objetos são armazenados numa estrutura separada do tipo zend_object, arrays são do tipo zend_array numa implementação de HashTable e assim por diante. Valores simples como inteiros e float são armazenados diretamente na zval, enquanto que valores complexos como strings e objetos são armazenados em estruturas separadas e representados na zval por ponteiros (como pode ser visto em zend_string *str;).

Outra noção refere-se ao fato de que internamente o PHP faz a contagem da quantidade de vezes que um valor é usado por outras variáveis (symbols) e isso é essencial para o gerenciamento da memória por parte do garbage collector, uma vez que esse contador estiver zerado, indica pro GC que a memória desse valor pode ser liberada.

A semântica dos valores

No PHP os valores possuem a semântica de valor, desde que explicitamente não peçamos por uma referência. Ou seja, quando passamos um valor para um argumento de uma função, ou quando atribuímos uma variável a outra, estamos sempre trabalhando em cópias do mesmo valor. Vamos esclarecer esse conceito com exemplos, para que depois possamos adentrar no assunto de referências.

Exemplo:

<?php

$foo = 1;
$bar = $foo;
$foo++;

var_dump($foo, $bar); // int(2), int(1)

$bar aponta para uma zval diferente, apenas com a cópia do valor de $foo. $foo foi incrementada e isso não refletiu em $bar.

Outro exemplo:

<?php

function increment(int $value) : void {
    $value++;
}

$value = 1;

increment($value);

echo $value; // 1

O valor de $value dentro do escopo da função é um e fora dela, é outro. Por esse motivo, o que está sendo incrementado é o zval do escopo da função.

Mas, veja bem, cópias não são feitas arbitrariamente sem necessidade. Para valores comuns como inteiros, números de ponto flutuante etc, elas são imediatamente feitas. No entanto, o mesmo não acontece com valores complexos como strings e arrays, por exemplo. O PHP usa o princípio copy-on-write, onde uma deep-copy (duplicação) dos valores é feita apenas antes desses dados serem alterados.

O melhor exemplo para ilustrar isso:

<?php

// Etapa 1: Consumo atual de memória
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2: Criando um grande array dinâmicamente
$foo = [];
for ($i = 0; $i < 1000000; $i++) {
    $foo[] = random_int(1, 100);
}
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3: Atribuindo o array a duas novas variáveis
$bar = $foo;
$baz = $foo;
echo sprintf("3) Mem: %s \n", memory_get_usage());

// Etapa 4: Fizemos uma alteração de escrita no array $bar
$bar[] = 1;
echo sprintf("4) Mem: %s \n", memory_get_usage());

// Etapa 5: Zerando os arrays previamente criados
$foo = [];
$bar = [];
$baz = [];
echo sprintf("5) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 395280 
2) Mem: 33953920 
3) Mem: 33953920 
4) Mem: 67512528 
5) Mem: 395312 

Veja que nas etapas 2 e 3 o consumo de memória permaneceu o mesmo. O senso comum seria imaginar que ele fosse triplicar, uma vez que atribuímos dois novos arrays com o valor de $foo. No entanto, o consumo de memória duplicou apenas na etapa 4, pois alteramos o array $bar, essa operação de escrita fez com que o PHP duplicasse o array para esse zval.

O mesmo comportamento pode ser visto com strings:

<?php

// Etapa 1
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2
$string = str_repeat('FOO', 100000);
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3
$string2 = $string;
$string3 = $string;
echo sprintf("3) Mem: %s \n", memory_get_usage());

// Etapa 4
$string2 .= 'BAR';
$string3 .= 'BAR';
echo sprintf("4) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 393504 
2) Mem: 696640 
3) Mem: 696640 
4) Mem: 1302848

Apenas na etapa 4 tivemos aumento da memória, pois as strings foram modificadas, forçando deep-copy.

Outro exemplo:

<?php

// Etapa 1
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2
$string = str_repeat('FOO', 100000);
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3
function foo(string $string) {
    $string2 = $string;
}

foo($string);

echo sprintf("3) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 393400 
2) Mem: 696536 
3) Mem: 696536
Desenvolvedor PHP Pleno
Formação: Desenvolvedor PHP Pleno
Nesta formação você aprenderá aspectos mais avançados da linguagem PHP, indo adiante nas características da linguagem, orientação a objetos, práticas de mercado, além da parte de integração com banco de dados. Ao final, estará apto a desenvolver sistemas usando banco de dados relacionais.
CONHEÇA A FORMAÇÃO

Referências no PHP

Referência é um mecanismo que permite que múltiplas variáveis (símbolos) apontem pro mesmo container interno de valor (zval). Referências não são ponteiros como vemos em C ou em Go, por exemplo.

Voltando ao exemplo anterior de $foo e $bar, como podemos fazer para $bar apontar para o mesmo zval que $foo de tal forma que o incremento em $foo reflita em $bar? Aí que entra o operador de referência & na atribuição:

<?php

$foo = 1;
$bar =& $foo;
$foo++;

var_dump($foo, $bar); // int(2), int(2)

Agora, na prática, temos dois símbolos ($foo e $bar) que apontam para o mesmo valor, alterações em um ou em outro refletirão no mesmo container interno de valor. Por esse motivo o incremento de $foo agora refletiu em $bar.

Esse é o exemplo mais elementar para distinguir a semântica de valor da de referência.

Objetos são passados por referência?

Existe a noção entre desenvolvedores e artigos na web de que objetos são passados por referência, mas isso não reflete a realidade. Eles também são passados pela semântica de valor, como qualquer outro valor. Eles apenas possuem um comportamento parecido com “referência” mas que internamente não se trata de referência.

Vejamos esse exemplo:

<?php

class Node
{
    // Todo
}

$node1 = new Node();
$node2 = $node1;

xdebug_debug_zval('node1'); // node1: (refcount=2, is_ref=0)=class Node {  }
xdebug_debug_zval('node2'); // node2: (refcount=2, is_ref=0)=class Node {  }

Nota: A função xdebug_debug_zval() retorna detalhes internos da zval e para ela funcionar, é preciso ter a extensão xdebug instalada.

Na prática, o que temos nesse exemplo, é que as duas variáveis são estruturas (zval) distintas, mas internamente apontam pro mesmo objeto.

O que é diferente de:

<?php

class Node
{
    // Todo
}

$node1 = new Node();
$node2 = &$node1;

xdebug_debug_zval('node1'); // node1: (refcount=2, is_ref=1)=class Node {  }
xdebug_debug_zval('node2'); // node2: (refcount=2, is_ref=1)=class Node {  }

Nesse exemplo, além de ambas as variáveis apontarem pro mesmo objeto, temos explicitamente uma referência, observe que o valor da flag is_ref agora é 1. Isso quer dizer que, $node1 e $node2 apontam para o mesmo zval.

A diferença, a princípio, parece sutil, mas tentemos ver isso na prática. Primeiro um exemplo onde explicitamente pedimos para receber um valor por referência:

<?php

$a = 1;

function change(&$a) {
    $a = 2;
}

change($a);

echo $a; // 2

A alteração dentro do escopo da função refletiu no mesmo container interno de valor (zval) da variável $a do escopo do script. Como era esperado.

Agora a dúvida é: se objetos são sempre passados por referência (como é o senso comum), não precisamos usar o operador de referência para receber um objeto nessa mesma função change(), certo?

Vejamos:

<?php

class Node
{
    // Todo
}

$node = new Node();

function change($node) {
    $node = 10;
}

change($node);

var_dump($node); // class Node#1 (0) { }

Pois bem, se os objetos realmente fossem passados por referência, o valor da variável $node deveria ser 10 e não uma instância da classe Node. Ou seja, quando um objeto é passado para o argumento de uma função/método ou quando é atribuído em outra variável, não temos passagem por referência, o que fica caracterizado é que essas variáveis apenas estão apontando para a mesma estrutura de dado referente ao objeto, o que dá a sensação que está acontecendo passagem por referência.

O que causa essa confusão é esse comportamento:

<?php

class Node
{
    public int $count;
}

$node = new Node();
$node->count = 1;

function change(Node $node) {
    $node->count = 2;
}

change($node);

echo $node->count; // 2

O resultado é 2, mesmo sem passagem por referência, por causa do comportamento de estarmos trabalhando com o mesmo objeto em memória.

O exemplo abaixo demonstra que estamos lidando com duas estruturas (zval) diferentes, mas que ambas referenciam a mesma estrutura de dado do objeto criado:

<?php

class Node
{
    // TODO
}

$node = new Node();

function change(Node $node) {
    $nodeFoo = &$node;

    xdebug_debug_zval('node'); // node: (refcount=2, is_ref=1)=class Node {  }
}

change($node);

xdebug_debug_zval('node'); // node: (refcount=1, is_ref=0)=class Node {  }

Veja que dentro do escopo da função já ficou caracterizado que existe referência, mas fora dela, não. Ou seja, se $nodeFoo dentro de change() fosse alterada para 10, refletiria no mesmo zval de $node. Veja:

<?php

class Node
{
    // TODO
}

$node = new Node();

function change(Node $node) {
    $nodeFoo = &$node;

    $nodeFoo = 10; // Reflete no mesmo zval de $node

    var_dump($node); // int(10)
}

change($node);

var_dump($node); // class Node#1 (0) { }

Agora, a história muda quando explicitamente recebemos a variável por referência:

<?php

class Node
{
    // Todo
}

$node = new Node();

function change(&$node) {
    $node = 10; // Reflete no mesmo zval da variável $node fora desse escopo
}

change($node);

var_dump($node); // int(10)

Por fim, um exemplo de variáveis que não se referenciam, mas que apontam para o mesmo valor interno em memória até que uma operação de escrita seja realizada, aí é feita uma deep-copy:

<?php

$string = str_repeat('FOO ', 4);

xdebug_debug_zval('string'); // string: (refcount=1, is_ref=0)='FOO FOO FOO FOO '

$string2 = $string;

xdebug_debug_zval('string'); // string: (refcount=2, is_ref=0)='FOO FOO FOO FOO '

$string3 = $string;

xdebug_debug_zval('string'); // string: (refcount=3, is_ref=0)='FOO FOO FOO FOO '

$string3 .= 'BAR';

xdebug_debug_zval('string'); // string: (refcount=2, is_ref=0)='FOO FOO FOO FOO '

Observe que depois que $string3 fez uma operação de escrita o refcount do valor de $string foi decrementado.

Outro exemplo genérico:

<?php

class Foo {
    public $index;
}

$a = new Foo();
$b = $a;
$c = &$b;

$c->index = 10;

echo "({$a->index}, {$b->index}, {$c->index})" . PHP_EOL; // (10, 10, 10)

$c = "Hello";

echo "({$a->index}, {$b}, {$c})"; // (10, Hello, Hello)

Na prática temos:

Todos os símbolos apontam pro mesmo objeto, no entanto, $c é referência de $b, alterações feitas em $c refletirão na mesma zval de $b, mantendo $a intacta.

Em que momento devo usar referências?

As funções de sorting de arrays do PHP os recebem por referência, por exemplo:

sort ( array &$array [, int $sort_flags = SORT_REGULAR ] ) : bool

Sempre que você ver na assinatura de uma função &$var, significa que está recebendo por referência e que, portanto, vai trabalhar na mesma zval da variável informada.

E usar referências não é sobre performance, dependendo do uso, pode piorá-la. Usar referências é mais sobre lidar melhor com a memória, como no caso da função acima, não faria sentido duplicar o array inteiro só para aplicar o algoritmo de sorting.

Mas, no geral, a regra é que você dificilmente vai usar com frequência operações com referências. Usa-se referências quando realmente existe um objetivo muito bem definido, portanto, não se preocupe em querer “otimizar” o seu código pra usar referências, pois é bem possível que você não precisa delas.

O que mais posso fazer com referências?

Você pode ver todas as sintaxe possíveis para se usar referências no PHP através desse link da documentação oficial.

Palavras finais

Sumarizando o que foi visto nesse artigo:

  • PHP usa a semântica de valor;
  • Objetos não são passados por referência;
  • Valores complexos como arrays e strings são apenas duplicados quando alterados (usa-se o princípio copy-on-write);
  • Referências não deixam o código mais performático, se mal utilizadas, podem é deixar mais lento, devido aos vários apontamentos que precisam ser feitos. Referências são usadas em casos bem específicos em que o desenvolvedor tem a exata noção do comportamento que ele espera ter;
  • O desenvolvedor não precisa na maior parte do tempo se preocupar com memória (a não ser que vá trabalhar com grandes data sets, mas aí pode-se estudar o uso de generators no PHP ou alguma outra estratégia);

Não é nada fácil entender a figura completa desse conjunto de pecinhas, mas caso você tenha interesse em se aprofundar, recomendo a leitura dos artigos:

Até a próxima!

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

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.

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200