Laravel prijedlozi: Automatizirajte uvoz imovine robne marke pomoću API-ja Brand Kit Extractor
Generator ponuda može izraditi besprijekorne cjenike, a ipak izgledati nedovršeno kada logotip, boje i tipografija korisnika stignu kao raštrkani privici. Ručno kopiranje je sporo, nedosljedno i posebno nezgrapno kada se isti brend mora pojaviti u ponudama, PDF izvještajima i dokumentima za daljnje praćenje.
Ovaj vodič izrađuje produkcijski Laravelov uvozni proces oko API-ja Brand Kit Extractor. Korisnik šalje URL javne web-stranice, Laravel stavlja ekstrakciju u red čekanja, provjerava svaku obaveznu kategoriju podataka o brendu i pohranjuje verzionirani snimak koji generator ponuda ili izvještaja može sigurno koristiti.
Ovdje provjereno znači da je odgovor prošao dokumentiranu granicu aplikacije i ostao povezan sa svojim izvornim URL-om. To ne znači da su utvrđeni vlasništvo nad žigom ili pravno odobrenje.
Dobijte pristup i testirajte API
Dovršite uvođenje u uslugu prije pisanja integracijskog koda:
- Registrirajte se na https://ai.mihajlo.mk/register ili se prijavite na https://ai.mihajlo.mk/login.
- Otvorite stranicu usluge Brand Kit Extractor.
- Odaberite dostupan Free, Plus ili Pro plan i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte njezin token ograničen na uslugu.
Ova usluga zahtijeva autentikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Implementacija u nastavku koristi oblik Bearer jer vjerodajnice ostaju izvan URL-ova, zapisnika pristupa i povijesti preglednika.
Ponovno generiranje tokena usluge opoziva prethodno aktivni token. Rotaciju tretirajte kao promjenu implementacije: ažurirajte spremište tajni, ponovno implementirajte radnike i web-procese, provjerite promet i tek tada zaključite rotaciju.
Točan zahtjev je POST https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit. Pošaljite JSON objekt koji sadrži url. Prije izrade značajke, pošaljite jedan minimalni zahtjev iz pouzdanog terminala:
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"}'
Nemojte uvrstiti token u repozitorij. Stavite ga u Laravelovu konfiguraciju okruženja:
# .env
BRAND_KIT_TOKEN=YOUR_SERVICE_TOKEN
Dodajte definiciju usluge u config/services.php. Pristup okruženju ostaje u konfiguraciji kako bi predmemorirana produkcijska konfiguracija radila predvidljivo.
'brand_kit' => [
'token' => env('BRAND_KIT_TOKEN'),
'base_url' => 'https://ai.mihajlo.mk/api/brand-kit-extractor',
],
Odaberite malu, trajnu arhitekturu
Uvoz bi trebao biti asinkron. Ekstrakcija web-stranice ovisi o vanjskoj mreži, može trajati dulje od uobičajenog zahtjeva preglednika i može biti ograničena. Web-zahtjev zato bilježi namjeru i šalje posao u red čekanja. Namjenski klijent upravlja HTTP ponašanjem, dok mapper domene odbacuje nepotpune ili prevelike odgovore prije pohrane.
Rezultirajući tijek je:
- Kontroler prihvaća URL javne web-stranice i stvara uvoz na čekanju.
- Posao reda čekanja poziva krajnju točku za ekstrakciju.
- Mapper provjerava naziv brenda, logotipe, boje, fontove, slike, društvene profile i CSS varijable.
- Posao atomski pohranjuje spreman snimak ili strukturirani neuspjeh.
- Generator ponuda čita samo snimke čiji je status
ready.
Identifikator zahtjeva sprječava stariji posao u redu čekanja da prepiše noviji uvoz za istu web-stranicu. Ta mala zaštita važna je kada korisnik dvaput klikne „osvježi brend”.
Stvorite granicu perzistencije
Započnite s Laravel aplikacijom konfiguriranom s podržanom bazom podataka i pozadinom reda čekanja. PHP 8.3 ili noviji, Composer i funkcionalan radnik reda čekanja preduvjeti su. Generirajte osnovne klase naredbama prve strane:
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
Za jedinstvenost koristite hash umjesto indeksiranja potencijalno dugog URL-a. JSON snimak čuva dokaze koje vraća usluga bez prisiljavanja nestabilnih ugniježđenih podataka u relacijske stupce.
<?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');
}
};
U app/Models/BrandKit.php dopustite samo ova polja u vlasništvu aplikacije i pretvorite snimak:
<?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'];
}
}
Izradite strogi API klijent
Neuspjehe prijenosa držite odvojenima od trajnih neuspjeha. Istek vremena, HTTP 429 i pogreške poslužitelja mogu uspjeti kasnije. Neuspjehe autentikacije i odbijene zahtjeve ne treba naslijepo ponovno pokušavati.
<?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('Token usluge nije konfiguriran.');
}
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(
'Povezivanje s uslugom ekstrakcije nije uspjelo.',
previous: $exception
);
}
if ($response->status() === 429 || $response->serverError()) {
throw new RetryableBrandKitFailure(
'Usluga ekstrakcije privremeno nije dostupna.'
);
}
if ($response->status() === 401 || $response->status() === 403) {
throw new PermanentBrandKitFailure(
'Token usluge je odbijen.'
);
}
if ($response->clientError()) {
throw new PermanentBrandKitFailure(
'Zahtjev za ekstrakciju je odbijen.'
);
}
$payload = $response->json();
if (! is_array($payload)) {
throw new RetryableBrandKitFailure(
'Usluga ekstrakcije vratila je neispravan JSON.'
);
}
return BrandKitData::fromApi($payload)->toArray();
}
}
Provjerite odgovor domene
Donja granica zahtijeva sedam isporučenih kategorija odgovora i primjenjuje lokalna sigurnosna ograničenja. Ne nagađa nedokumentirana podpolja logotipa ili fontova. Ako službena dokumentacija definira dublje sheme stavki, dodajte ta pravila ovdje umjesto raspršivanja pretpostavki po predlošcima.
<?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(
'Odgovor kompleta brenda nije prošao provjeru ugovora.'
);
}
$data = $validator->validated();
try {
$encoded = json_encode($data, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
throw new PermanentBrandKitFailure(
'Odgovor kompleta brenda nije valjan JSON podatak.',
previous: $exception
);
}
if (strlen($encoded) > 1_000_000) {
throw new PermanentBrandKitFailure(
'Odgovor kompleta brenda premašuje lokalno ograničenje pohrane.'
);
}
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,
];
}
}
Sigurno stavite ekstrakciju u red čekanja
Posao ponovno pokušava samo prolazne neuspjehe i koristi ograničeno odgađanje. Bilježi konačni neuspjeh bez izlaganja tokena, tijela odgovora ili potencijalno osjetljivih zaglavlja u zapisnicima.
<?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('Ekstrakcija kompleta brenda odgođena', [
'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('Ekstrakcija kompleta brenda odbijena', [
'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' => 'Resursi brenda nisu se mogli uvesti.',
]);
}
}
Prihvatite uvoze iz aplikacije za ponude
Kontroler dopušta samo HTTP i HTTPS URL-ove, blokira očite lokalne ciljeve i zamjenjuje identifikator zahtjeva pri osvježavanju. Za privatni proizvod s više zakupaca također autorizirajte pristup ponudi i razmislite o ograničavanju uvoza na domene koje je odobrio korisnik.
<?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);
}
}
Registrirajte autentificiranu rutu u routes/web.php ili routes/api.php:
use App\Http\Controllers\BrandKitImportController;
use Illuminate\Support\Facades\Route;
Route::post('/brand-kits/import', [BrandKitImportController::class, 'store'])
->middleware('auth');
Prikazivač ponuda treba upitati komplet sa status = ready, escapirati tekst i URL-ove te mapirati samo izričito podržane oblike resursa u svoj prikazni model. Nikada nemojte vraćene CSS varijable izravno umetati u stylesheet. Prije prikazivanja provjerite nazive prilagođenih svojstava i ograničite vrijednosti; vanjski sadržaj inače može postati površina za CSS injekciju.
Testirajte ugovor bez pozivanja produkcije
Laravelov HTTP fake čini autentikaciju, oblik zahtjeva i klasifikaciju neuspjeha determinističkima.
<?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');
}
}
Implementirajte i upravljajte uvoznikom
Pokrenite migracije prije dopuštanja uvoza, predmemorirajte produkcijsku konfiguraciju nakon umetanja tokena i ponovno pokrenite dugotrajne radnike kako bi primili novo okruženje:
php artisan migrate --force
php artisan config:cache
php artisan queue:restart
php artisan test
Nadzirite php artisan queue:work upraviteljem procesa platforme. Postavite vremenska ograničenja radnika iznad vremenskog ograničenja posla od 40 sekundi, ali neka ostanu ograničena. Pratite broj uvoza na čekanju, spremnih, s privremenim neuspjehom i s trajnim neuspjehom, kao i trajanje posla i starost reda čekanja. Upozorite na kontinuirane odgovore 429 ili rastuće neuspjehe poslužitelja umjesto zapisivanja potpunih odgovora pružatelja usluge.
Uobičajeni obrasci neuspjeha
- Svaki zahtjev vraća 401 ili 403: potvrdite aktivaciju plana i token ograničen na uslugu. Ponovno generirani token odmah poništava stari.
- Razvoj radi, ali produkcija odbija autentikaciju: očistite i ponovno izgradite Laravelovu predmemoriju konfiguracije nakon ažuriranja tajne.
- Uvozi ostaju na čekanju: provjerite radi li konfigurirani radnik reda čekanja i prati li ispravnu vezu reda čekanja.
- Odgovori ne prolaze provjeru: usporedite granični mapper sa službenom dokumentacijom. Nemojte tiho pohranjivati djelomične podatke.
- Česti odgovori 429: smanjite broj istodobnih radnika ili učestalost uvoza. Zadržite ograničeno odgađanje umjesto stvaranja agresivne petlje ponovnih pokušaja.
- PDF prikazivanje ne uspijeva: držite podatke ekstrakcije odvojene od prikaznog modela za renderiranje i prije upotrebe provjerite pojedinačne URL-ove resursa, formate fontova, boje i CSS vrijednosti.
Završni kontrolni popis za provjeru
- Točna POST krajnja točka prima JSON koji sadrži samo predviđeni javni
url. - Token usluge postoji samo u konfiguraciji koja se oslanja na okruženje i upravljanju tajnama.
- Uspješni odgovori sadrže i provjeravaju svih sedam obaveznih kategorija podataka o brendu.
- Neuspjesi autentikacije i provjere ne pokušavaju se ponovno.
- Veze, odgovori, ponovni pokušaji, veličina payloada i izvršavanje reda čekanja ograničeni su.
- Zastarjeli poslovi ne mogu prepisati novije uvoze.
- Zapisnici sadrže identifikatore i klase neuspjeha, nikada vjerodajnice ili tijela odgovora.
- Generator ponuda i izvještaja čita samo spremne snimke i sigurno mapira vanjske resurse.
- Testovi prolaze s
Http::fake(), bez trošenja kvote ili kontaktiranja aktivne usluge.
Vidljivi rezultat je jednostavan: unesite web-stranicu korisnika i primite koherentan komplet brenda za sljedeću ponudu. Inženjering ispod toga namjerno je manje glamurozan — stroge granice, trajni poslovi, odmjereni ponovni pokušaji, sigurno prikazivanje i uočljivi neuspjesi. Ti detalji pretvaraju privlačnu demonstraciju API-ja u pouzdanu svakodnevnu značajku.