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.

Top comments (0)