Туториали

Laravel Proposals: Automate Brand Asset Imports with the Brand Kit Extractor API

Laravel Proposals: Автоматизирајте го увозот на брендирани средства со API-то за извлекување Brand Kit

Генераторот на понуди може да создава беспрекорни табели со цени, а сепак да изгледа недовршено кога логото, боите и типографијата на клиентот пристигнуваат како расфрлани прилози. Рачното копирање е бавно, недоследно и особено незгодно кога истиот бренд мора да се појави во понуди, PDF извештаи и документи за понатамошна комуникација.

Овој туторијал создава продукциски Laravel процес за увоз околу API-то Brand Kit Extractor. Корисникот доставува јавна URL-адреса на веб-локација, Laravel ја става екстракцијата во редица, ја валидира секоја задолжителна категорија на податоци за брендот и складира верзионирана снимка што рендерерот за понуди или извештаи може безбедно да ја користи.

Тука, потврдено значи дека одговорот ја поминал документираната граница на апликацијата и останува поврзан со својата изворна URL-адреса. Тоа не значи дека е утврдена сопственост на трговска марка или правно одобрување.

Добијте пристап и тестирајте го API-то

Завршете го воведувањето во услугата пред да пишувате интеграциски код:

  1. Регистрирајте се на https://ai.mihajlo.mk/register, или најавете се на https://ai.mihajlo.mk/login.
  2. Отворете ја страницата на услугата Brand Kit Extractor.
  3. Изберете достапен Free, Plus или Pro план и завршете ја неговата активација.
  4. Отворете ја официјалната документација за услугата.
  5. Пронајдете го панелот Service token и копирајте го неговиот токен со опсег на услугата.

Оваа услуга бара автентикација. Прифаќа Bearer токен, заглавие X-API-Token или параметар за пребарување token. Имплементацијата подолу ја користи Bearer формата бидејќи ги задржува ингеренциите надвор од URL-адресите, дневниците за пристап и историјата на прелистувачот.

Повторното генерирање на токенот за услугата го поништува претходно активниот токен. Третирајте ја ротацијата како промена при распоредување: ажурирајте го складиштето за тајни, повторно распоредете ги worker-ите и веб-процесите, потврдете го сообраќајот и дури потоа сметајте дека ротацијата е завршена.

Точното барање е POST https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit. Испратете JSON објект што содржи url. Пред да ја изградите функционалноста, направете едно минимално барање од доверлив терминал:

curl --fail-with-body \
  --request POST \
  --url https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit \
  --header "Authorization: Bearer YOUR_SERVICE_TOKEN" \
  --header "Accept: application/json" \
  --header "Content-Type: application/json" \
  --data '{"url":"https://example.com"}'

Не го поставувајте токенот во commit. Ставете го во конфигурацијата на околината на Laravel:

# .env
BRAND_KIT_TOKEN=YOUR_SERVICE_TOKEN

Додајте ја дефиницијата на услугата во config/services.php. Пристапот до околината останува во конфигурацијата, така што кешираната продукциска конфигурација се однесува предвидливо.

'brand_kit' => [
    'token' => env('BRAND_KIT_TOKEN'),
    'base_url' => 'https://ai.mihajlo.mk/api/brand-kit-extractor',
],

Изберете мала, издржлива архитектура

Увозот треба да биде асинхрон. Екстракцијата на веб-локација зависи од надворешна мрежа, може да трае подолго од вообичаено барање од прелистувач и може да биде ограничена. Затоа веб-барањето ја евидентира намерата и испраќа задача во редица. Посебен клиент го поседува HTTP однесувањето, додека доменски мапер ги отфрла нецелосните или преголемите одговори пред складирањето.

Резултирачкиот тек е:

  1. Контролерот прифаќа јавна URL-адреса на веб-локација и создава увоз во исчекување.
  2. Задача во редицата го повикува крајниот извлекувачки endpoint.
  3. Маперот ги валидира името на брендот, логоата, боите, фонтовите, сликите, социјалните профили и CSS променливите.
  4. Задачата атомски складира подготвена снимка или структурирана грешка.
  5. Генераторот на понуди чита само снимки чиј статус е ready.

Идентификатор на барање спречува постара задача во редицата да презапише понов увоз за истата веб-локација. Таа мала заштита е важна кога корисникот двапати ќе кликне „освежи бренд“.

Создадете ја границата за перзистенција

Започнете со Laravel апликација конфигурирана со поддржана база на податоци и backend за редици. PHP 8.3 или понов, Composer и функционален worker за редици се предуслови. Генерирајте ги основните класи со првични команди:

php artisan make:model BrandKit -m
php artisan make:controller BrandKitImportController
php artisan make:job ExtractBrandKit
php artisan make:test BrandKitClientTest
php artisan queue:table
php artisan migrate

Користете хаш за единственост наместо индексирање на потенцијално долга URL-адреса. JSON снимката ги зачувува доказите вратени од услугата без да присилува нестабилни вгнездени податоци во релациски колони.

<?php

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

return new class extends Migration {
    public function up(): void
    {
        Schema::create('brand_kits', function (Blueprint $table): void {
            $table->id();
            $table->string('source_hash', 64)->unique();
            $table->text('source_url');
            $table->uuid('request_id');
            $table->string('status', 20)->index();
            $table->string('brand_name')->nullable();
            $table->json('assets')->nullable();
            $table->string('failure_code', 40)->nullable();
            $table->text('failure_message')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('brand_kits');
    }
};

Во app/Models/BrandKit.php, дозволете ги само овие полиња во сопственост на апликацијата и конвертирајте ја снимката:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

final class BrandKit extends Model
{
    protected $fillable = [
        'source_hash', 'source_url', 'request_id', 'status',
        'brand_name', 'assets', 'failure_code', 'failure_message',
    ];

    protected function casts(): array
    {
        return ['assets' => 'array'];
    }
}

Изградете строг API клиент

Чувајте ги транспортните неуспеси одвоени од трајните неуспеси. Истекувањата на време, HTTP 429 и грешките на серверот може да успеат подоцна. Неуспесите на автентикацијата и одбиените барања не треба слепо да се повлекуваат повторно.

<?php

namespace App\Services;

use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use RuntimeException;

final class RetryableBrandKitFailure extends RuntimeException {}
final class PermanentBrandKitFailure extends RuntimeException {}

final class BrandKitClient
{
    public function extract(string $url): array
    {
        $token = (string) config('services.brand_kit.token');

        if ($token === '') {
            throw new PermanentBrandKitFailure('Токенот за услугата не е конфигуриран.');
        }

        try {
            $response = Http::baseUrl(config('services.brand_kit.base_url'))
                ->withToken($token)
                ->acceptJson()
                ->asJson()
                ->connectTimeout(5)
                ->timeout(25)
                ->post('/v1/extract-brand-kit', ['url' => $url]);
        } catch (ConnectionException $exception) {
            throw new RetryableBrandKitFailure(
                'Поврзувањето со услугата за екстракција не успеа.',
                previous: $exception
            );
        }

        if ($response->status() === 429 || $response->serverError()) {
            throw new RetryableBrandKitFailure(
                'Услугата за екстракција е привремено недостапна.'
            );
        }

        if ($response->status() === 401 || $response->status() === 403) {
            throw new PermanentBrandKitFailure(
                'Токенот за услугата беше одбиен.'
            );
        }

        if ($response->clientError()) {
            throw new PermanentBrandKitFailure(
                'Барањето за екстракција беше одбиено.'
            );
        }

        $payload = $response->json();

        if (! is_array($payload)) {
            throw new RetryableBrandKitFailure(
                'Услугата за екстракција врати невалиден JSON.'
            );
        }

        return BrandKitData::fromApi($payload)->toArray();
    }
}

Валидирајте го доменскиот одговор

Границата подолу ги бара седумте доставени категории на одговор и применува локални безбедносни ограничувања. Таа не претпоставува недокументирани подполиња за логоа или фонтови. Ако официјалната документација дефинира подлабоки шеми на ставки, додајте ги тие правила тука наместо да распрскувате претпоставки низ шаблоните.

<?php

namespace App\Services;

use Illuminate\Support\Facades\Validator;
use JsonException;

final readonly class BrandKitData
{
    private function __construct(
        public string $brandName,
        public array $logos,
        public array $colors,
        public array $fonts,
        public array $imagery,
        public array $socialProfiles,
        public array $cssVariables,
    ) {}

    public static function fromApi(array $payload): self
    {
        $validator = Validator::make($payload, [
            'brand_name' => ['required', 'string', 'max:200'],
            'logos' => ['required', 'array', 'max:100'],
            'colors' => ['required', 'array', 'max:100'],
            'fonts' => ['required', 'array', 'max:100'],
            'imagery' => ['required', 'array', 'max:200'],
            'social_profiles' => ['required', 'array', 'max:100'],
            'css_variables' => ['required', 'array', 'max:300'],
        ]);

        if ($validator->fails()) {
            throw new PermanentBrandKitFailure(
                'Одговорот за комплетот на брендот не ја помина валидацијата на договорот.'
            );
        }

        $data = $validator->validated();

        try {
            $encoded = json_encode($data, JSON_THROW_ON_ERROR);
        } catch (JsonException $exception) {
            throw new PermanentBrandKitFailure(
                'Одговорот за комплетот на брендот не е валиден JSON податок.',
                previous: $exception
            );
        }

        if (strlen($encoded) > 1_000_000) {
            throw new PermanentBrandKitFailure(
                'Одговорот за комплетот на брендот го надминува локалното ограничување за складирање.'
            );
        }

        return new self(
            $data['brand_name'],
            $data['logos'],
            $data['colors'],
            $data['fonts'],
            $data['imagery'],
            $data['social_profiles'],
            $data['css_variables'],
        );
    }

    public function toArray(): array
    {
        return [
            'brand_name' => $this->brandName,
            'logos' => $this->logos,
            'colors' => $this->colors,
            'fonts' => $this->fonts,
            'imagery' => $this->imagery,
            'social_profiles' => $this->socialProfiles,
            'css_variables' => $this->cssVariables,
        ];
    }
}

Безбедно ставете ја екстракцијата во редица

Задачата повторува само привремени неуспеси и користи ограничено повлекување. Го евидентира конечниот неуспех без да изложува токени, тела на одговори или потенцијално чувствителни заглавија во дневниците.

<?php

namespace App\Jobs;

use App\Models\BrandKit;
use App\Services\BrandKitClient;
use App\Services\PermanentBrandKitFailure;
use App\Services\RetryableBrandKitFailure;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;

final class ExtractBrandKit implements ShouldQueue
{
    use Queueable;

    public int $tries = 4;
    public int $timeout = 40;

    public function __construct(
        public int $brandKitId,
        public string $requestId,
    ) {}

    public function handle(BrandKitClient $client): void
    {
        $kit = BrandKit::findOrFail($this->brandKitId);

        if ($kit->request_id !== $this->requestId) {
            return;
        }

        try {
            $assets = $client->extract($kit->source_url);
        } catch (RetryableBrandKitFailure $exception) {
            Log::warning('Екстракцијата на комплетот на брендот е одложена', [
                'brand_kit_id' => $kit->id,
                'attempt' => $this->attempts(),
            ]);

            if ($this->attempts() >= $this->tries) {
                $this->markFailed($kit, 'temporary_failure');
                return;
            }

            $delays = [10, 30, 90];
            $this->release($delays[$this->attempts() - 1]);
            return;
        } catch (PermanentBrandKitFailure $exception) {
            Log::notice('Екстракцијата на комплетот на брендот е одбиена', [
                'brand_kit_id' => $kit->id,
            ]);
            $this->markFailed($kit, 'permanent_failure');
            return;
        }

        $kit->refresh();

        if ($kit->request_id !== $this->requestId) {
            return;
        }

        $kit->update([
            'status' => 'ready',
            'brand_name' => $assets['brand_name'],
            'assets' => $assets,
            'failure_code' => null,
            'failure_message' => null,
        ]);
    }

    private function markFailed(BrandKit $kit, string $code): void
    {
        $kit->update([
            'status' => 'failed',
            'failure_code' => $code,
            'failure_message' => 'Средствата на брендот не можеа да се увезат.',
        ]);
    }
}

Прифаќајте увози од апликацијата за понуди

Контролерот дозволува само HTTP и HTTPS URL-адреси, блокира очигледни локални цели и го заменува идентификаторот на барањето при освежување. За приватен производ со повеќе закупци, исто така овластете пристап до понудата и разгледајте ограничување на увозите на домени одобрени од клиентот.

<?php

namespace App\Http\Controllers;

use App\Jobs\ExtractBrandKit;
use App\Models\BrandKit;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

final class BrandKitImportController extends Controller
{
    public function store(Request $request): JsonResponse
    {
        $url = $request->validate([
            'url' => ['required', 'url:http,https', 'max:2048'],
        ])['url'];

        $host = strtolower(parse_url($url, PHP_URL_HOST) ?? '');
        $blockedName = $host === 'localhost' || str_ends_with($host, '.local');
        $blockedIp = filter_var($host, FILTER_VALIDATE_IP)
            && ! filter_var(
                $host,
                FILTER_VALIDATE_IP,
                FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
            );

        abort_if($host === '' || $blockedName || $blockedIp, 422);

        $requestId = (string) Str::uuid();

        $kit = BrandKit::updateOrCreate(
            ['source_hash' => hash('sha256', $url)],
            [
                'source_url' => $url,
                'request_id' => $requestId,
                'status' => 'pending',
                'brand_name' => null,
                'assets' => null,
                'failure_code' => null,
                'failure_message' => null,
            ]
        );

        ExtractBrandKit::dispatch($kit->id, $requestId)->afterCommit();

        return response()->json([
            'id' => $kit->id,
            'status' => 'pending',
        ], 202);
    }
}

Регистрирајте ја автентицираната рута во routes/web.php или routes/api.php:

use App\Http\Controllers\BrandKitImportController;
use Illuminate\Support\Facades\Route;

Route::post('/brand-kits/import', [BrandKitImportController::class, 'store'])
    ->middleware('auth');

Рендерерот на понуди треба да бара комплет со status = ready, да избегнува текст и URL-адреси и да мапира само изречно поддржани облици на средства во својот view model. Никогаш не вметнувајте директно вратени CSS променливи во stylesheet. Валидирајте ги имињата на приспособените својства и ограничете ги вредностите пред рендерирање; надворешната содржина во спротивно може да стане површина за CSS инјектирање.

Тестирајте го договорот без повикување на продукција

HTTP fake на Laravel ги прави автентикацијата, обликот на барањето и класификацијата на неуспесите детерминистички.

<?php

namespace Tests\Feature;

use App\Services\BrandKitClient;
use App\Services\PermanentBrandKitFailure;
use App\Services\RetryableBrandKitFailure;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

final class BrandKitClientTest extends TestCase
{
    public function test_it_imports_a_complete_brand_kit(): void
    {
        config(['services.brand_kit.token' => 'test-token']);

        Http::fake([
            'https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit'
                => Http::response([
                    'brand_name' => 'Example',
                    'logos' => [], 'colors' => [], 'fonts' => [],
                    'imagery' => [], 'social_profiles' => [],
                    'css_variables' => [],
                ], 200),
        ]);

        $result = app(BrandKitClient::class)
            ->extract('https://example.com');

        $this->assertSame('Example', $result['brand_name']);

        Http::assertSent(fn ($request) =>
            $request->method() === 'POST'
            && $request['url'] === 'https://example.com'
            && $request->hasHeader(
                'Authorization',
                'Bearer test-token'
            )
        );
    }

    public function test_rate_limits_are_retryable(): void
    {
        config(['services.brand_kit.token' => 'test-token']);
        Http::fake(fn () => Http::response([], 429));

        $this->expectException(RetryableBrandKitFailure::class);

        app(BrandKitClient::class)->extract('https://example.com');
    }

    public function test_missing_fields_are_rejected(): void
    {
        config(['services.brand_kit.token' => 'test-token']);
        Http::fake(fn () => Http::response([
            'brand_name' => 'Incomplete',
        ], 200));

        $this->expectException(PermanentBrandKitFailure::class);

        app(BrandKitClient::class)->extract('https://example.com');
    }
}

Распоредете и управувајте со увозникот

Извршете миграции пред да дозволите увози, кеширајте ја продукциската конфигурација по внесувањето на токенот и рестартирајте ги долготрајните worker-и за да ја добијат новата околина:

php artisan migrate --force
php artisan config:cache
php artisan queue:restart
php artisan test

Надгледувајте го php artisan queue:work со процесниот менаџер на платформата. Поставете ги временските ограничувања на worker-ите над временското ограничување на задачата од 40 секунди, но задржете ги ограничени. Следете го бројот на увози во исчекување, подготвени, со привремен неуспех и со траен неуспех, како и траењето на задачите и староста на редицата. Алармирајте при продолжени одговори 429 или растечки неуспеси на серверот, наместо да евидентирате цели одговори од провајдерот.

Чести обрасци на неуспех

  • Секое барање враќа 401 или 403: потврдете ја активацијата на планот и токенот со опсег на услугата. Повторно генериран токен веднаш го поништува стариот.
  • Развојот работи, но продукцијата ја одбива автентикацијата: исчистете го и повторно изградете го конфигурацискиот кеш на Laravel по ажурирањето на тајната.
  • Увозите остануваат во исчекување: проверете дали конфигурираниот worker за редица работи и ја следи точната конекција за редицата.
  • Одговорите не ја поминуваат валидацијата: споредете го граничниот мапер со официјалната документација. Не складирајте тивко делумни податоци.
  • Чести одговори 429: намалете го бројот на истовремени worker-и или фреквенцијата на увоз. Зачувајте ограничено повлекување наместо да создавате агресивна јамка за повторување.
  • PDF рендерирањето се расипува: чувајте ги податоците за екстракција одвоени од view model за рендерирање и валидирајте ги поединечните URL-адреси на средства, формати на фонтови, бои и CSS вредности пред употреба.

Конечна контролна листа за потврда

  • Точниот POST endpoint прима JSON што ја содржи само наменетата јавна url.
  • Токенот за услугата постои само во конфигурација поддржана од околината и управување со тајни.
  • Успешните одговори ги содржат и валидираат сите седум задолжителни категории на податоци за брендот.
  • Неуспесите на автентикацијата и валидацијата не се повторуваат.
  • Конекциите, одговорите, повторувањата, големината на payload-от и извршувањето на редицата се ограничени.
  • Застарените задачи не можат да презапишат понови увози.
  • Дневниците содржат идентификатори и класи на неуспех, никогаш ингеренции или тела на одговори.
  • Генераторот на понуди и извештаи чита само подготвени снимки и безбедно мапира надворешни средства.
  • Тестовите поминуваат со Http::fake(), без трошење квота или контактирање на активната услуга.

Видливиот резултат е едноставен: внесете ја веб-локацијата на клиентот и добијте кохерентен комплет на брендот за следната понуда. Инженерството под него намерно е помалку гламурозно — строги граници, издржливи задачи, воздржани повторувања, безбедно рендерирање и видливи неуспеси. Тие детали се она што една привлечна API демонстрација ја претвора во сигурна секојдневна функционалност.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.