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
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=
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');
}
};
php artisan migrate
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 {}
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')
]
]
];
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/"
}
},
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);
}
}
<?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);
}
}
<?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();
}
}
<?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,
];
}
}
<?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']);
});
Importe o routes.php do modulo Plataform1 em routes/web.php
require app_path('Modules').'/Plataform1/routes.php';
Agora, vamos configurar a fila e implementar os jobs que serão despachados para ela.
Instale a extensão predis.
composer require predis/predis
Adicione o Redis client e queue connection no env.
REDIS_CLIENT=predis
QUEUE_CONNECTION=redis
Em config/app.php, adicione um alias para o Redis.
'Redis' => Illuminate\Support\Facades\Redis::class,
Crie um job, injete o serviço responsavel pela sincronização dos produtos e chame o método execute.
php artisan make:job ProductSyncJob
<?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();
}
}
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'
]);
}
}
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductSyncController;
Route::prefix('v1')->group(function () {
Route::get('/sync/plataform1/products', [ProductSyncController::class,'index']);
});
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,
],
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
Inicie a fila.
php artisan queue:work data_sync
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
E voilà, nosso job atualizou 100 mil registros em apenas 11s, tudo isso consumindo baixissímo processamento.
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
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)