DEV Community

Jonathan Gonçalves
Jonathan Gonçalves

Posted on • Edited on

Laravel Queue, uma poderosa alternativa de fila para projetos PHP

Laravel Queues

Ao desenvolver um aplicativo web, deparar-se com tarefas demoradas, como o processamento em lote, é comum. A análise e armazenamento de grandes conjuntos de dados, por exemplo, podem impactar negativamente a resposta do aplicativo durante uma solicitação web típica. O Laravel Queue surge como uma ferramenta valiosa nesse contexto, proporcionando uma abordagem eficiente para lidar com operações que consomem tempo.

O PHP, por sua natureza, não oferece suporte nativo a paralelismo eficiente, o que pode ser um obstáculo ao lidar com tarefas intensivas. No entanto, o Laravel Queue contorna essa limitação ao permitir a execução assíncrona de tarefas em filas.

Considere um cenário em que você precisa processar grandes conjuntos de dados em lote, como a atualização de informações em massa. Em vez de sobrecarregar a solicitação web, o Laravel Queue permite enfileirar essas tarefas para execução em segundo plano.

Vamos criar um exemplo prático de um projeto que incorpora uma integração responsável por realizar a sincronização de produtos. Neste cenário, optaremos por aprimorar a eficiência do processo ao enviar o processamento dessa integração para a fila do Laravel.

Assumindo que você já tenha o PHP, Composer e um banco de dados relacional instalados em sua máquina, vamos começar.

Crie um projeto Laravel.

composer create-project laravel/laravel product-api
Enter fullscreen mode Exit fullscreen mode

Aponte a sua base de dados no env.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=product_api
DB_USERNAME=root
DB_PASSWORD=
Enter fullscreen mode Exit fullscreen mode

Em seguida, crie uma migration para sua tabela.

php artisan make:migration create_product_table

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('product', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('description', 1000)->nullable();
            $table->unsignedBigInteger('group_id');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('product');
    }
};
Enter fullscreen mode Exit fullscreen mode
php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Agora, crie uma classe de serviço para implementar o código responsável pela sincronização dos produtos. O Laravel não possui um comando para criar classe de serviço, mas você pode fazer manualmente.

<?php

namespace App\Services\Plataform1;

use App\Models\Product;
use Illuminate\Support\Facades\Http;

class ProductSync
{
    function execute()
    {
        $nextPage = 1;
        do {
            $body = $this->request($nextPage);

            $nextPage = $body['next_page'];

            $mappedData = collect($body['data'])->map(fn ($item) => Mapper::map($item))->all();

            $this->upsert($mappedData);
        }
        while ($nextPage != null);
    }

    /**
     * @return object{'data': array}
     */
    private function request($page = 1)
    {
        $plataform1 = config('integration.plataform1');

        try {            
            /**
             * @var \Illuminate\Http\Client\Response
             */
            $response = Http::withHeaders([
                'Authorization' => $plataform1['api']['token']
            ])->get($plataform1['api']['url'].'/v1/products', [
                'page' => $page
            ]);

            if ($response->status() != 200) {
                throw new ResponseStatusException($response->body(), $response->status());
            }

            return $response->json();
        }
        catch (\Throwable $th) {
            throw $th;
        }
    }

    private function upsert($data)
    {
        $chunks = array_chunk($data, 2000);

        /**
         * @var \Illuminate\Database\Eloquent\Builder
         */
        $Product = Product::class;

        foreach ($chunks as $chunk) {
            $Product::upsert(
                $chunk,
                ['id'],
                array_keys(reset($chunk))
            );
        }
    }
}

class Mapper {
    static function map($product) : array {
        return [
            'id' => $product['id'],
            'name' => $product['name'],
            'description' => $product['description'],
            'group_id' => $product['group_id'],
        ];
    }
}

class ResponseStatusException extends \Exception {}
Enter fullscreen mode Exit fullscreen mode

Vamos criar um arquivo config/integration.php para adicionar configurações da integração.

<?php

return [
    'plataform1' => [
        'api' => [
            'url' => env('PLATAFORM1_URL', 'http://localhost:8001/plataform1/api'),
            'token' => env('PLATAFORM1_TOKEN')
        ]
    ]
];
Enter fullscreen mode Exit fullscreen mode

Para simular o endpoint de produto da API, onde faremos as requisições, criaremos um novo módulo no projeto.

Adicione Modules e Modules\Plataform1\Database\Factories\ no composer.json

    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/",
            "Modules\\": "app/Modules/",
            "Modules\\Plataform1\\Database\\Factories\\": "app/Modules/Plataform1/database/factories/"
        }
    },
Enter fullscreen mode Exit fullscreen mode

Implemente o módulo Plataform1. Para testar o desempenho da nossa fila, mockaremos o retorno de 100 mil objetos. Em seguida, o serviço realizará a atualização dos dados em nosso banco.

<?php

namespace App\Modules\Plataform1\Http\Controllers;

use Illuminate\Http\Request;
use Modules\Plataform1\Models\Product;
use Illuminate\Routing\Controller;

class ProductController extends Controller {
    public function index(Request $request)
    {
        $page = $request->page ?? 1;

        $perPageDefault = pow(10, 4); // 10k

        $perPage = $request->per_page ?? $perPageDefault;

        $total = pow(10, 5); // 100k

        $pages = $total / $perPage;

        $data = Product::factory()
            ->count($perPage)
            ->make();

        $json = (object) [
            'next_page' => $page < $pages ? ++$page : null,
            'data' => $data
        ];

        return response()->json($json);
    }
}
Enter fullscreen mode Exit fullscreen mode
<?php

namespace App\Modules\Plataform1\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureTokenIsValid
{
    /**
     * Handle an incoming request.
     * 
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $token = $request->header('Authorization');

        if ($token == env('PLATAFORM1_TOKEN')) {
            return $next($request);
        }

        return response()->json(['error' => 'Token invalido'], 401);
    }
}
Enter fullscreen mode Exit fullscreen mode
<?php

namespace Modules\Plataform1\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Modules\Plataform1\Database\Factories\ProductFactory;

class Product extends Model {

    protected $table = 'product';

    use HasFactory;

    protected static function newFactory() {
        return ProductFactory::new();
    }
}
Enter fullscreen mode Exit fullscreen mode
<?php

namespace Modules\Plataform1\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Modules\Plataform1\Models\Product;

class ProductFactory extends Factory {
    protected $model = Product::class;

    public function definition()
    {
        return [
            'id' => $this->faker->unique()->numberBetween(1, pow(10, 5)), // 100k
            'name' => $this->faker->word,
            'description' => $this->faker->paragraph(),
            'group_id' => 1,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode
<?php
use App\Modules\Plataform1\Http\Controllers\ProductController;
use App\Modules\Plataform1\Http\Middleware\EnsureTokenIsValid;

Route::prefix('plataform1/api/v1')->middleware([EnsureTokenIsValid::class])->group(function () {
    Route::get('/products', [ProductController::class,'index']);
});
Enter fullscreen mode Exit fullscreen mode

Importe o routes.php do modulo Plataform1 em routes/web.php

require app_path('Modules').'/Plataform1/routes.php';
Enter fullscreen mode Exit fullscreen mode

Agora, vamos configurar a fila e implementar os jobs que serão despachados para ela.

Instale a extensão predis.

composer require predis/predis
Enter fullscreen mode Exit fullscreen mode

Adicione o Redis client e queue connection no env.

REDIS_CLIENT=predis
QUEUE_CONNECTION=redis
Enter fullscreen mode Exit fullscreen mode

Em config/app.php, adicione um alias para o Redis.

'Redis' => Illuminate\Support\Facades\Redis::class,
Enter fullscreen mode Exit fullscreen mode

Crie um job, injete o serviço responsavel pela sincronização dos produtos e chame o método execute.

php artisan make:job ProductSyncJob
Enter fullscreen mode Exit fullscreen mode
<?php

namespace App\Jobs\Plataform1;

use App\Services\Plataform1\ProductSync;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

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

    /**
     * Create a new job instance.
     * 
     *      */
    public function __construct(
        private ProductSync $productSync
    ) {}

    /**
     * Execute the job.
     * 
     *      */
    public function handle(): void
    {
        $this->productSync->execute();
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora, vamos criar um endpoint que será responsável pelo despache dos jobs.

<?php

namespace App\Http\Controllers;

use App\Services\Plataform1\ProductSync;
use Illuminate\Support\Facades\Redis;
use App\Http\Controllers\Controller;
use App\Jobs\Plataform1\ProductSyncJob;

class ProductSyncController extends Controller
{
    function __construct(
        private ProductSync $productSync
    ) {}

    public function index()
    {
        ProductSyncJob::dispatch($this->productSync)->onQueue('data_sync');

        return response()->json([
            'message' => 'Sincronização de produtos da Plataform1 iniciada'
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductSyncController;

Route::prefix('v1')->group(function () {
    Route::get('/sync/plataform1/products', [ProductSyncController::class,'index']);
});
Enter fullscreen mode Exit fullscreen mode

Agora, configuraremos nossa fila. Esta etapa é crucial, e é recomendável ter um entendimento claro dos jobs que serão despachados para montar a configuração. No nosso cenário, usaremos a seguinte configuração.

'data_sync' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => 'data_sync',
    'retry_after' => 90,
    'block_for' => null,
    'after_commit' => false,
],
Enter fullscreen mode Exit fullscreen mode

Finalmente, execute a aplicação para despachar os jobs na fila. Como o servidor de desenvolvimento local padrão não suporta requisições simultâneas, inicie com dois server workers.

PHP_CLI_SERVER_WORKERS=2 php artisan serve --port 8001
Enter fullscreen mode Exit fullscreen mode

Inicie a fila.

php artisan queue:work data_sync
Enter fullscreen mode Exit fullscreen mode

Agora, faça duas requisições ao endpoint que despacha o job de sincronização de produtos na fila. A primeira será para popular a tabela, e a segunda para testar o desempenho na atualização dos registros.

curl http://localhost:8001/api/v1/sync/plataform1/products
Enter fullscreen mode Exit fullscreen mode

E voilà, nosso job atualizou 100 mil registros em apenas 11s, tudo isso consumindo baixissímo processamento.

Jobs concluídos

Desempenho

Muito bacana, né?!

Se quiser, fique à vontade para clonar o repositório com essa implementação e brincar.

git clone git@github.com:jmgoncalves97/product-api.git
Enter fullscreen mode Exit fullscreen mode

Ao longo deste artigo, exploramos a eficácia do Laravel Queue no aprimoramento de projetos PHP, destacando a importância de mover tarefas demoradas para segundo plano por meio de filas. Exemplificamos sua aplicação em uma integração de API externa, demonstrando como essa abordagem pode significativamente melhorar a responsividade do aplicativo.

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay