Vodiči

Laravel Proposals: Injecting Brand Kit Assets for Consistent Client Reports

Laravel Proposals: Umetanje resursa kompleta robne marke za dosljedna izvješća za klijente

Prijedlog može imati besprijekorne cijene i uvjerljiv tekst, a ipak djelovati nedovršeno kada su logotip, boje i tipografija klijenta nedosljedni. Ručno kopiranje tih pojedinosti sporo je i podložno pogreškama. Bolji tijek rada jednom uvozi vizualni identitet javne web-stranice, provjerava ga na granici aplikacije i čini dobiveni komplet brenda dostupnim za svaki prijedlog i izvješće.

Ovaj vodič izrađuje taj tijek rada u Laravelu na PHP-u 8.3 ili novijem. Konzolna naredba poziva API Brand Kit Extractor, provjerava vraćeni naziv brenda, logotipe, boje, fontove, slike, društvene profile i CSS varijable, a zatim pohranjuje idempotentnu snimku za generator izvješća. Integracija koristi Laravelov ugrađeni HTTP klijent, ograničene ponovne pokušaje, strukturirane neuspjehe, sigurno bilježenje i determinističke testove.

Pristupite usluzi i izradite servisni token

  1. Izradite račun na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako ga već imate.
  2. Otvorite stranicu usluge Brand Kit Extractor. Odaberite dostupni plan Free, Plus ili Pro koji odgovara vašoj očekivanoj upotrebi i dovršite aktivaciju.
  3. Posjetite službenu dokumentaciju usluge. Pronađite ploču Service token i kopirajte ondje prikazani token ograničen na uslugu.
  4. Token odmah pohranite u upravitelj lozinki ili spremište tajni za implementaciju. Ponovno generiranje tokena opoziva prethodno aktivni token, stoga se mora ažurirati svako implementirano okruženje koje koristi staru vrijednost.

Ova usluga zahtijeva autentikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Upotrijebit ćemo Bearer token jer se parametri upita često pojavljuju u zapisnicima pristupa i tragovima nadzora.

Potvrdite krajnju točku prije pisanja Laravel koda

Točna operacija je POST https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit. Tijelo njezina JSON zahtjeva sadrži url. Uputite minimalni zahtjev s javnom web-stranicom za čiju obradu imate ovlaštenje:

curl --request POST \
  '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://client.example"}'

Nemojte odgovor izravno umetati u bazu podataka. Udaljena granica najprije mora potvrditi da sadrži upotrebljive podatke o brendu očekivanih vrsta.

Konfigurirajte Laravel aplikaciju

Projekt zahtijeva PHP 8.3 ili noviji, podržanu Laravel aplikaciju, konfiguriranu bazu podataka i odlazni HTTPS pristup. Vjerodajnicu stavite u .env, nikada u predane PHP datoteke:

BRAND_KIT_API_TOKEN=YOUR_SERVICE_TOKEN
BRAND_KIT_CONNECT_TIMEOUT=5
BRAND_KIT_RESPONSE_TIMEOUT=30

Dodajte unos potkrijepljen okruženjem u config/services.php:

'brand_kit' => [
    'base_url' => 'https://ai.mihajlo.mk/api/brand-kit-extractor',
    'token' => env('BRAND_KIT_API_TOKEN'),
    'connect_timeout' => (int) env('BRAND_KIT_CONNECT_TIMEOUT', 5),
    'timeout' => (int) env('BRAND_KIT_RESPONSE_TIMEOUT', 30),
],

Arhitektura je namjerno mala: API servis upravlja ponašanjem prijenosa, domenski objekt provjerava odgovor, naredba koordinira perzistenciju, a kontroler prijedloga čita samo provjerene snimke. Ekstrakcija je uvoz koji pokreće operater, a ne dio zahtjeva korisnika, pa red čekanja ovdje nije potreban. Ako uvozi kasnije postanu česti, orkestracija naredbe može se premjestiti u zadatak u redu čekanja bez promjene API granice.

Izradite idempotentno spremište kompleta brenda

Izradite migraciju i model brand_kits. Jedinstveni izvorni URL omogućuje da ponovljeni uvozi ažuriraju isti zapis umjesto stvaranja dvosmislenih duplikata.

<?php
// database/migrations/2026_01_01_000000_create_brand_kits_table.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_url', 2048)->unique();
            $table->string('brand_name');
            $table->json('payload');
            $table->string('payload_hash', 64);
            $table->timestamp('verified_at');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('brand_kits');
    }
};
<?php
// app/Models/BrandKit.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

final class BrandKit extends Model
{
    protected $fillable = [
        'source_url',
        'brand_name',
        'payload',
        'payload_hash',
        'verified_at',
    ];

    protected function casts(): array
    {
        return [
            'payload' => 'array',
            'verified_at' => 'immutable_datetime',
        ];
    }
}

Pokrenite php artisan migrate nakon pregleda migracije za svoju platformu baze podataka.

Provjerite odgovor na domenskoj granici

Ugovor usluge izlaže semantička polja koja se koriste u nastavku. Zadržavanje provjere u jednom objektu sprječava kontrolere i predloške da stvaraju optimistične pretpostavke o udaljenom JSON-u. Nizovi namjerno ostaju neprozirni jer pojedinačni prikazi resursa mogu sadržavati dokaze i metapodatke koje aplikacija treba sačuvati, a ne ponovno tumačiti.

<?php
// app/Data/ExtractedBrandKit.php

namespace App\Data;

use UnexpectedValueException;

final readonly class ExtractedBrandKit
{
    private const ARRAY_FIELDS = [
        'logos', 'colors', 'fonts', 'imagery', 'social_profiles',
    ];

    private function __construct(
        public string $name,
        public array $payload,
    ) {}

    public static function fromApi(array $data): self
    {
        if (!isset($data['name']) || !is_string($data['name'])
            || trim($data['name']) === '') {
            throw new UnexpectedValueException('Invalid brand name.');
        }

        foreach (self::ARRAY_FIELDS as $field) {
            if (!array_key_exists($field, $data) || !is_array($data[$field])) {
                throw new UnexpectedValueException(
                    "Invalid or missing brand field: {$field}."
                );
            }
        }

        if (!array_key_exists('css_variables', $data)
            || !is_array($data['css_variables'])) {
            throw new UnexpectedValueException(
                'Invalid or missing brand field: css_variables.'
            );
        }

        $encoded = json_encode($data, JSON_THROW_ON_ERROR);

        if (strlen($encoded) > 1_000_000) {
            throw new UnexpectedValueException('Brand kit is too large.');
        }

        return new self(trim($data['name']), $data);
    }
}

Ograničenje od jednog megabajta pravilo je aplikacije, a ne tvrdnja o ograničenju API-ja. Štiti bazu podataka izvješća od neočekivano velikih dokumenata i treba ga prilagoditi na temelju promatranih veličina učitanog sadržaja.

Izradite otporni API klijent

Klijent ponavlja pokušaje za neuspjehe povezivanja i neuspjehe na strani poslužitelja uz kratko eksponencijalno odgađanje. Ne ponavlja autentikaciju, provjeru zahtjeva ni odgovore o kvoti: neposredno ponavljanje tih poziva ne može ispraviti temeljni problem.

<?php
// app/Services/BrandKitFailure.php

namespace App\Services;

use RuntimeException;

final class BrandKitFailure extends RuntimeException
{
    public function __construct(
        public readonly string $kind,
        public readonly ?int $status = null,
    ) {
        parent::__construct("Brand kit extraction failed: {$kind}.");
    }
}

// app/Services/BrandKitApi.php

namespace App\Services;

use App\Data\ExtractedBrandKit;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;

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

        if (!is_string($token) || $token === '') {
            throw new BrandKitFailure('configuration');
        }

        for ($attempt = 1; $attempt <= 3; $attempt++) {
            try {
                $response = Http::baseUrl(
                    config('services.brand_kit.base_url')
                )
                    ->withToken($token)
                    ->acceptJson()
                    ->asJson()
                    ->connectTimeout(
                        config('services.brand_kit.connect_timeout')
                    )
                    ->timeout(config('services.brand_kit.timeout'))
                    ->post('/v1/extract-brand-kit', ['url' => $url]);
            } catch (ConnectionException $exception) {
                if ($attempt === 3) {
                    throw new BrandKitFailure('connection');
                }

                $this->backoff($attempt);
                continue;
            }

            if ($response->successful()) {
                try {
                    $json = $response->json();

                    if (!is_array($json)) {
                        throw new \UnexpectedValueException();
                    }

                    return ExtractedBrandKit::fromApi($json);
                } catch (Throwable $exception) {
                    throw new BrandKitFailure(
                        'invalid_response',
                        $response->status()
                    );
                }
            }

            $status = $response->status();

            if (in_array($status, [401, 403], true)) {
                throw new BrandKitFailure('authentication', $status);
            }

            if ($status === 429) {
                throw new BrandKitFailure('quota_or_rate_limit', $status);
            }

            if ($status >= 400 && $status < 500) {
                throw new BrandKitFailure('request_rejected', $status);
            }

            if ($attempt === 3) {
                throw new BrandKitFailure('upstream', $status);
            }

            $this->backoff($attempt);
        }

        throw new BrandKitFailure('unexpected');
    }

    private function backoff(int $attempt): void
    {
        usleep(((200 * (2 ** ($attempt - 1))) + random_int(0, 100)) * 1000);
    }
}

Uočite što nedostaje iz iznimki i zapisnika: tijela odgovora, zaglavlja zahtjeva i tokeni. Tijela udaljenih pogrešaka mogu sadržavati neočekivan sadržaj, a autorizacijska zaglavlja nikada ne smiju dospjeti u zapisnike aplikacije.

Uvezite komplet brenda web-stranice

Naredba provjerava URL prije upućivanja odlaznog zahtjeva. Ograničavanje uvoza na javne HTTPS web-stranice smanjuje slučajni pristup internim uslugama. Stroža okruženja trebala bi također razriješiti naziv glavnog računala i odbiti privatne, povratne, lokalne veze i rezervirane adrese nakon svakog preusmjeravanja.

<?php
// app/Console/Commands/ImportBrandKit.php

namespace App\Console\Commands;

use App\Models\BrandKit;
use App\Services\BrandKitApi;
use App\Services\BrandKitFailure;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

final class ImportBrandKit extends Command
{
    protected $signature = 'brand:import {url}';
    protected $description = 'Import a validated brand kit for reports';

    public function handle(BrandKitApi $api): int
    {
        $url = $this->argument('url');

        if (!is_string($url)
            || filter_var($url, FILTER_VALIDATE_URL) === false
            || parse_url($url, PHP_URL_SCHEME) !== 'https') {
            $this->error('Provide a valid public HTTPS URL.');
            return self::INVALID;
        }

        try {
            $kit = $api->extract($url);

            $encoded = json_encode($kit->payload, JSON_THROW_ON_ERROR);

            BrandKit::query()->updateOrCreate(
                ['source_url' => $url],
                [
                    'brand_name' => $kit->name,
                    'payload' => $kit->payload,
                    'payload_hash' => hash('sha256', $encoded),
                    'verified_at' => now(),
                ]
            );

            Log::info('Brand kit imported', [
                'host' => parse_url($url, PHP_URL_HOST),
                'payload_hash' => hash('sha256', $encoded),
            ]);

            $this->info("Imported brand kit: {$kit->name}");
            return self::SUCCESS;
        } catch (BrandKitFailure $failure) {
            Log::warning('Brand kit import failed', [
                'host' => parse_url($url, PHP_URL_HOST),
                'kind' => $failure->kind,
                'status' => $failure->status,
            ]);

            $this->error($failure->getMessage());
            return self::FAILURE;
        }
    }
}

Pokrenite stvarni uvoz s php artisan brand:import https://client.example. Generator prijedloga sada može učitati zapis prema URL-u klijenta i proslijediti $brandKit->payload.

Jačanje za produkciju i česti neuspjesi

  • 401 ili 403: potvrdite da token pripada ovoj usluzi i da nije opozvan ponovnim generiranjem. Nemojte automatski ponavljati pokušaj.
  • 429: zabilježite strukturirani neuspjeh, smanjite učestalost uvoza i ponovite pokušaj kasnije prema odgovoru usluge i aktivnom planu, umjesto da odmah stvarate petlju.
  • Nevažeći odgovor: zadržite prethodnu provjerenu snimku, upozorite na vrstu neuspjeha i usporedite trenutačnu službenu dokumentaciju s graničnim mapiranjem.
  • Istek vremena ili 5xx: klijent upućuje samo tri ograničena pokušaja. Trajni neuspjesi trebaju biti vidljivi u nadzoru, a ne neograničeno zadržavati radnika.
  • Nepodudaranje web-stranice: imajte na umu da se ekstrakcija temelji na dokazima s dostavljenog javnog URL-a. Pregledajte uvezene resurse prije objave prijedloga visoke vrijednosti.

Tijekom implementacije umetnite BRAND_KIT_API_TOKEN putem upravitelja tajni platforme, a zatim pokrenite php artisan config:cache tek nakon što je okruženje prisutno. Ograničite izvršavanje naredbi na pouzdane operatere, sačuvajte posljednju valjanu snimku baze podataka tijekom prekida rada i pratite brojanja prema vrsti neuspjeha i statusu bez bilježenja vjerodajnica ili tijela odgovora.

Završni kontrolni popis za provjeru

  • Aktivirani plan i token ograničen na uslugu pripadaju usluzi Brand Kit Extractor.
  • Token postoji samo u konfiguraciji potkrijepljenoj okruženjem i pohrani tajni.
  • Aplikacija šalje JSON na točnu krajnju točku za ekstrakciju uz Bearer autentikaciju.
  • Svaka obavezna kategorija brenda provjerava se prije perzistencije.
  • Neuspjesi autentikacije, provjere i kvote ne ponavljaju se naslijepo.
  • Zapisnici sadrže naziv glavnog računala, status, vrstu neuspjeha i hash učitanog sadržaja, ali ne token ni neobrađeno tijelo.
  • Sloj izvješća tretira CSS i URL-ove udaljenih resursa kao nepouzdan unos.
  • Automatizirani testovi prolaze bez kontaktiranja vanjske usluge.
  • Stvarni uvoz ažurira jedan idempotentni zapis i ostaje dostupan generatoru prijedloga.

Vrijedan dio ove integracije nije samo dohvaćanje logotipa. Riječ je o uspostavljanju kontrolirane primopredaje između javnih dokaza o brendu i ponovljivog tijeka rada s dokumentima. Kada je ta granica provjerena, vidljiva i sigurna pri neuspjehu, dosljedna izvješća za klijente više ne ovise o tome hoće li se netko sjetiti koja je nijansa plave upotrijebljena prošli put.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.