Hyperf - PHP Coroutine Framework baseado em Swoole

Hyperf - PHP Coroutine Framework baseado em Swoole

Durante muito tempo que estudei sobre Swoole, eu sempre fazia as coisas "do zero" para tentar entender a fundo sobre ele. Cheguei a pesquisar alguns frameworks baseados nele, mas não encontrei nenhum que tenha me chamado a atenção. Então acabei seguindo meus estudos por conta mesmo, até agora.

Quando conheci o Hyperf não imaginei o quanto ele era incrível até começar a descobrir tudo que ele poderia me entregar. Praticamente todos os desafios que eu estava tendo para implementar do zero com Swoole, ele resolveu.

Pode até parecer ótimo ter tudo pronto, mas do que adianta ter um carro de fórmula 1 se você não souber dirigir? Então não é tão simples assim. É preciso ter uma base de Swoole, programação concorrente, aplicação stateful, etc., para conseguir extrair todo o poder que ele pode te dar.

A ideia aqui é te ajudar com os primeiros passos e ter um projeto mínimo rodando, pois, só de utilizar o Hyperf como framework, você já conseguirá sentir uma diferença absurda em performance.

A documentação é bacana, aborda bastante coisa e tem vários exemplos, então vou passar por alguns tópicos e ir referenciando tudo aqui. Alguns conteúdos da documentação ainda estão em chinês, mas estão sendo traduzidos, enquanto isso, é só traduzir que da tudo certo!

Minha missão aqui é:

Te incentivar a construir as suas próximas aplicações utilizando Hyperf e Swoole.

Então, bora lá!

Tópicos:

  1. O que é?
  2. Instalação
  3. Hot Reload
  4. Estrutura
  5. Melhorando o ambiente
  6. Database
  7. Rotas e Controllers
  8. Parallel
  9. Benchmark

1. O que é?

O Hyperf é um framework de alto desempenho e altamente flexível baseado em Swoole 4.5+ com PHP CLI. Ele possui um servidor de corrotina integrado com diversos componentes comumente usados.

Durante cerca de meio ano, o Hyperf foi testado em diversas companhias pelo mundo, após ótimos resultados, ele foi liberado para a comunidade em 20/06/2019.

Os dois trechos acima foram retirados e traduzidos da documentação oficial

Caso você não conheça o Swoole PHP, recomendo dar uma olhada antes, pois vou assumir que você já conhece a forma com que ele funciona, já que o Hyperf é baseado nele.

Algumas recomendações de leitura para que você conheça e aprenda mais sobre Swoole:

Uma coisa bem interessante, é que o Hyperf é muito similar ao Laravel, então se você já trabalhou com Laravel, vai se adaptar muito bem ao Hyperf.

Por ser baseado em Swoole, ele roda em cima do PHP CLI, sendo stateful, e é extremamente performático comparado ao "PHP tradicional", utilizado via php-fpm, e também comparado a outras linguagens. :eyes:

A proposta do Hyperf foi trazer todas as features do Swoole de forma simples para a comunidade, fazendo com que não fosse preciso implementar tudo do zero. Grandes exemplos são as pools de conexões, servidor HTTP, TCP, gRPC, entre outros.

Para exemplificar como funciona, ao utilizar o client pronto de Redis do Hyperf, você já tem uma pool criada por baixo com o client de Redis do Swoole, o que já ajuda muito na hora de fazer um projeto. A mesma coisa para o Eloquent ORM, ao utiliza-lo no Hyperf, por baixo já está tudo resolvido.

Ou seja, não tem motivos para não achar isso incrível <3

Para entender o Lifecycle do Hyperf, é muito importante entender o Lifecycle do Swoole. Como o Swoole é executado via PHP CLI, o Hyperf se inicia também via PHP CLI, utilizando o symfony/console como command.


2. Instalação

A maneira mais fácil de iniciar é utilizando Docker, como em tudo rs. Com isso, você não precisa se preocupar com a versão do PHP, da extensão do Swoole na sua máquina, etc.

Primeiro, crie uma pasta para iniciar o projeto:

mkdir hyperf-skeleton && cd hyperf-skeleton

Inicie o container mapeando o diretório que criamos como um volume para receber o projeto:

docker run -v ${PWD}:/app -p 9501:9501 -it --entrypoint /bin/sh hyperf/hyperf:8.0-alpine-v3.13-swoole

Já dentro do container, instale o Hyperf na pasta app que mapeamos anteriormente. O framework é bastante modular, então ele vai perguntar se você quer utilizar algumas coisas, inicialmente, escolha todas as opções padrão que já vai nos atender.

composer create-project hyperf/hyperf-skeleton app

Caso você queira iniciar esse projeto novamente de maneira mais fácil e já cair na raiz do projeto, crie o arquivo do docker-compose.yaml com o seguinte conteúdo:

version: '3.9'

services:

  hyperf-skeleton:
    container_name: hyperf-skeleton
    image: hyperf/hyperf:8.0-alpine-v3.13-swoole
    working_dir: /app
    entrypoint: sh
    volumes:
      - ./:/app
    ports:
      - 9501:9501

Sobre o entrypoint ser direto o sh e não o start do projeto, veremos mais tarde o motivo disso. Depois disso, para iniciar o container basta usar o seguinte comando:

docker compose run --rm --service-ports hyperf-skeleton

Agora é só iniciar o webserver:

php bin/hyperf.php start

Sua aplicação está pronta para responder em: http://0.0.0.0:9501

{
    "method": "GET",
    "message": "Hello Hyperf."
}

Mais detalhes na documentação oficial


3. Hot Reload

Como já vimos que o Swoole é executado via PHP CLI, em ambiente de desenvolvimento isso pode ser ruim, pois podemos perder muito tempo parando e iniciando o servidor. Bom, felizmente o Hyperf resolveu isso de forma fácil via o componente Watcher.

Importante: não utilize esse componente em ambiente produtivo, ele é destinado ao ambiente de desenvolvimento!

Primeiro precisamos instalar esse modulo no nosso projeto:

composer require hyperf/watcher --dev

Publicar o arquivo de configurações:

php bin/hyperf.php vendor:publish hyperf/watcher

Um arquivo foi criado em: config/autoload/watcher.php, nele você pode definir as configurações que achar melhor, aqui vamos seguir com as que já estão lá mesmo.

Agora, para rodar nosso projeto em ambiente de desenvolvimento, nosso comando passa a ser:

php bin/hyperf.php server:watch

Assim, sempre que fizermos alguma alteração no projeto, esse componente vai recarregar nosso código automaticamente sem ser preciso reiniciar manualmente.

magic.gif


4. Estrutura

A estrutura é bem simples, como comentei é bem similar ao Laravel, então não tem muita complexidade. As principais pastas são:

  • Arquivo de start: bin/hyperf.php
  • Centro de configurações: config/*
  • Rotas: config/routes.php
  • Server: config/autoload/server.php
  • Injeção de dependências: config/autoload/dependencies.php
  • Design do projeto: config/autoload/devtool.php
  • Aplicação: app/*

Para conhecer mais sobre o centro de configurações, acesse este link.

Eu queria destacar um dos itens acima que achei super interessante, o arquivo devtool.php. Com ele é possível montar todo o design da sua arquitetura da forma que você achar melhor. Ou seja, em um único arquivo você consegue definir sua estrutura de pastas para utilizar DDD com clean architecture, hexagonal architecture, ou qualquer outra que desejar, te dando muito poder de trabalhar da forma que preferir.


5. Melhorando o ambiente

Na próxima seção, vamos iniciar com uma ideia básica de CRUD. Mas antes, vamos fazer uma melhoria no nosso docker-compose.yaml para ajudar no nosso desenvolvimento, adicionando o nosso banco de dados e o Redis (que vamos utilizar mais adiante).

Atualize o arquivo docker-compose.yaml para:

version: '3.9'

services:

  hyperf-skeleton:
    container_name: hyperf-skeleton
    image: hyperf/hyperf:8.0-alpine-v3.13-swoole
    working_dir: /app
    entrypoint: ["php", "bin/hyperf.php", "server:watch"]
    volumes:
      - ./:/app
    ports:
      - 9501:9501
    depends_on:
      - hyperf-skeleton-mariadb
      - hyperf-skeleton-redis

  hyperf-skeleton-mariadb:
    container_name: hyperf-skeleton-mariadb
    image: mariadb:latest
    volumes:
      - ./.docker/mariadb:/var/lib/mysql
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: "secret"
      MYSQL_DATABASE: "hyperf-skeleton"

  hyperf-skeleton-redis:
    container_name: hyperf-skeleton-redis
    image: redis:latest
    ports:
      - 6379:6379

Agora para iniciar nosso projeto completo basta rodar o seguinte comando:

docker-compose up -d

E claro, para remover tudo:

docker-compose down

6. Database

Caso queira entender um pouco mais sobre o que vamos utilizar agora no Hyperf, acesse a documentação oficial e veja sobre: Database, Model e Migration.

Dica: evite copiar e colar o código, tente escrever tudo, assim seu aprendizado será muito maior (:

Com nosso banco de dados rodando, vamos criar uma migration para a tabela de usuários:

docker container exec -it hyperf-skeleton php bin/hyperf.php gen:migration create_users_table

Com o nosso arquivo gerado em migrations/*_create_users_table.php, vamos inserir os campos básicos para serem criados:

Schema::create('users', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('name');
    $table->string('email');
    $table->timestamps();
});

Sim, vamos usar uuid como chave primaria, espero que em todos os seus projetos você também esteja usando :eyes:, se ainda não estiver, da uma olhada aqui.

O próximo passo é criar o nosso modelo, então na pasta App/Model vamos criar o nosso User:

<?php declare(strict_types=1);

namespace App\Model;

use Hyperf\DbConnection\Model\Model;

/**
 * @property $id
 * @property $name
 * @property $email
 * @property $created_at
 * @property $updated_at
 */
class User extends Model
{
    /**
     * @var string
     */
    public $keyType = 'string';

    /**
     * @var bool
     */
    public $incrementing = false;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['id', 'name', 'email', 'created_at', 'updated_at'];
}

Antes de rodar a nossa migration, normalmente é preciso verificar se os parâmetros de conexão do banco estão devidamente configurados. Porém, no nosso caso vamos manter tudo como está, mas vale a pena dar uma olhada no conteúdo do arquivo, ele fica em config/autoload/databases.php.

O que vamos precisar fazer é trocar as envs no arquivo .env para realizar a conexão ao nosso container. Só vamos precisar alterar as seguintes envs:

DB_HOST=hyperf-skeleton-mariadb
DB_DATABASE=hyperf-skeleton
DB_USERNAME=root
DB_PASSWORD=secret

Agora é só rodar a migration no container da aplicação:

docker container exec -it hyperf-skeleton php bin/hyperf.php migrate

Resultado de sucesso:

[INFO] Migration table created successfully.
Migrating: *_create_users_table
Migrated:  *_create_users_table

Importante, o hot reload não funciona para recarregar variaveis de ambiente, então é preciso dar um restart manual no container.


7. Rotas e Controllers

Caso queira entender um pouco mais sobre o que vamos utilizar agora no Hyperf, acesse a documentação oficial e veja sobre: Controller e Router.

Agora vamos criar o nosso UserController, na pasta app/Controller inicialmente vazio:

<?php declare(strict_types=1);

namespace App\Controller;

class UserController extends AbstractController
{

}

Depois, precisamos definir nossas rotas. Então faremos algo assim:

use App\Controller\UserController;

Router::addGroup('/users', function () {
    Router::get('', [UserController::class, 'index']);
    Router::get('/{id}', [UserController::class, 'show']);
    Router::post('', [UserController::class, 'store']);
    Router::delete('/{id}', [UserController::class, 'delete']);
});

Voltando para o nosso Controller, vamos criar os métodos conforme nossas rotas já utilizando a classe User e fazendo as devidas operações com o ORM:

<?php declare(strict_types=1);

namespace App\Controller;

use App\Model\User;
use Hyperf\HttpServer\Contract\RequestInterface;

class UserController extends AbstractController
{
    public function index(RequestInterface $request)
    {
        return User::get();
    }

    public function show(string $id)
    {
        return User::find($id);
    }

    public function store(RequestInterface $request)
    {
        return User::create($request->all());
    }

    public function delete(string $id)
    {
        return User::destroy($id);
    }
}

Essa é uma estrutura extremamente básica, e com alguns problemas... Contudo, a ideia não é ensinar tudo aqui, e sim te direcionar para explorar e construir coisas interessantes. Para isso, vou deixar alguns links aqui para você melhorar este código:

Todos os itens acima têm uma boa documentação e com bons exemplos. Você consegue implementar tudo isso nesse simples CRUD para exercitar os recursos do Hyperf.

Ainda tenho uma ideia bônus: crie uma interface e uma implementação para a Repository Pattern refatorando este código e injete via Dependency Injection, tenho certeza que será bacana entender este fluxo.

E claro, não esqueça de criar testes. O Hyperf criou o co-phpunit, que é basicamente o phpunit com corrotinas. Maneiro de mais né? Para conhecer mais acesse a documentação.


8. Parallel

Outra coisa que achei muito bacana, foi a implementação que o Hyperf fez do gerenciamento via channels de corrotinas usando wait groups e handle de exceptions. Basicamente ele encapsulou toda essa implementação na classe Hyperf\Utils\Parallel.

Usando o exemplo do que fizemos acima, para criar vários usuários em paralelo e só continuar depois que todos finalizarem, podemos fazer assim no nosso UserController@store:

public function store(RequestInterface $request)
{
    $parallel = new Parallel();

    foreach ($request->input('users') as $user) {
        $parallel->add(
            function () use ($user) {
                User::create($user);
            },
            $user['id']
        );
    }

    return $parallel->wait();
}

Claro que não fariamos esta implementação no mundo real, é apenas um exemplo rsrs.

É importante destacar que ao disparar uma corrotina, tudo que for utilizado de I/O dentro do seu contexto precisa ser compatível com corrotinas, caso contrário seu código será bloqueante.

Todos os componentes do Hyperf já são compatíveis, então você pode utilizar sem problemas, tenha atenção apenas com bibliotecas como a do mongodb, que, até o momento não tem suporte dos Hooks do Swoole.


9. Benchmark

Falamos muito sobre performance, diferenças entre o Swoole e o "PHP Tradicional", mas agora vamos aos números!

Realizei um teste na minha própria máquina, então os resultados podem ser diferentes de um servidor em produção ou da sua máquina. Não vamos focar nos números absolutos, vamos focar nas referências entre um e outro, pois mesmo mudando de ambiente, provavelmente serão mantidas similares.

Para realizar os testes utilizei o wrk na rota de retorno base dos frameworks.

Laravel 8 com PHP 8.0 (93 rq/s)

Running 10s test @ http://0.0.0.0:8000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   550.01ms  335.24ms   1.12s    63.01%
    Req/Sec    12.38      8.80    70.00     84.72%
  938 requests in 10.10s, 16.64MB read
  Socket errors: connect 155, read 1214, write 12, timeout 0
Requests/sec:     92.83
Transfer/sec:      1.65MB

Lumen 8 com PHP 8.0 (279 rq/s)

Running 10s test @ http://0.0.0.0:8000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   108.93ms   41.13ms 193.70ms   76.31%
    Req/Sec    73.96     40.36   184.00     69.49%
  2820 requests in 10.10s, 751.82KB read
  Socket errors: connect 157, read 3987, write 4, timeout 0
Requests/sec:    279.10
Transfer/sec:     74.41KB

Laravel Octane com PHP 8.0 e Swoole (1.273 rq/s)

Running 10s test @ http://0.0.0.0:8000
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   110.06ms  190.13ms   1.65s    84.78%
    Req/Sec   118.57     96.64   550.00     69.97%
  12796 requests in 10.05s, 13.09MB read
  Socket errors: connect 157, read 101, write 4, timeout 0
Requests/sec:   1273.75
Transfer/sec:      1.30MB

Hyperf 2.2 com PHP 8.0 (95.515 rq/s)

Running 10s test @ http://0.0.0.0:9502
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.55ms    1.37ms  21.38ms   90.08%
    Req/Sec    12.02k     1.77k   15.27k    58.88%
  956407 requests in 10.01s, 173.30MB read
  Socket errors: connect 779, read 95, write 0, timeout 0
Requests/sec:  95515.68
Transfer/sec:     17.31MB

Swoole puro com PHP 8.0 (159.581 rq/s)

Running 10s test @ http://0.0.0.0:9501
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.47ms  431.57us  29.87ms   97.10%
    Req/Sec    13.37k     5.49k   25.36k    59.74%
  1612224 requests in 10.10s, 267.53MB read
  Socket errors: connect 157, read 107, write 0, timeout 0
Requests/sec: 159581.95
Transfer/sec:     26.48MB

Comparações usando o mais lento como referência:

Frameworkrq/sref.
Laravel92x1
Lumen279+3x
Laravel Octane1.273+13x
Hyperf95.5151.038x
Swoole puro159.581+1.734x

Ou seja, o Hyperf é mais de 986 vezes mais rápido do que o Laravel e o Swoole puro 1.734, isso sem considerar a latência, que também teve uma diferença enorme. Incrível né? Claro, este é um benchmark realizado localmente em uma rota extremamente simples, mas a diferença realmente é insana, mesmo em outros ambientes.


O resultado deste projeto está no meu github:

Bom, acredito que passamos pelo básico do Hyperf, agora você já consegue explorar tudo que ele pode fornecer com este ambiente que iniciamos.

Pretendo escrever novos artigos aprofundando mais em assuntos específicos, principalmente sobre os diversos clients que ele implementa e sobre as connections pools.

Espero que você tenha gostado e que esse guia possa ter te ajudado. Apesar de algumas partes da documentação ainda não ter sido traduzida, ela é bem completa de exemplos, então da para implementar praticamente tudo que está disponível.

E aí, já conhecia o Hyperf? Me conta o que achou dele nos comentários. (:


Agradecimento especial ao @ronieneubauer pela revisão.

Did you find this article valuable?

Support Leonardo do Carmo by becoming a sponsor. Any amount is appreciated!