Туториали

Laravel Proposals: Injecting Brand Kit Assets for Consistent Client Reports

Laravel предлози: Вметнување ресурси од бренд-комплетот за доследни извештаи за клиентите

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

Овој туторијал го гради тој работен тек во Laravel на PHP 8.3 или понова верзија. Конзолна команда го повикува API-то Brand Kit Extractor, ги валидира вратените име на брендот, логоа, бои, фонтови, слики, социјални профили и CSS-променливи, а потоа зачувува идемпотентна снимка за генераторот на извештаи. Интеграцијата го користи вградениот HTTP-клиент на Laravel, ограничени повторни обиди, структурирани неуспеси, безбедно логирање и детерминистички тестови.

Добијте пристап и создадете сервисен токен

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

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

Потврдете ја крајната точка пред да пишувате Laravel-код

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

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"}'

Не лепете го одговорот директно во базата на податоци. Оддалечената граница прво мора да потврди дека содржи употребливи податоци за брендот со очекуваните типови.

Конфигурирајте ја Laravel-апликацијата

На проектот му се потребни PHP 8.3 или понова верзија, поддржана Laravel-апликација, конфигурирана база на податоци и излезен HTTPS-пристап. Ставете ги акредитивите во .env, никогаш во PHP-датотеки што се зачувани во репозиториумот:

BRAND_KIT_API_TOKEN=YOUR_SERVICE_TOKEN
BRAND_KIT_CONNECT_TIMEOUT=5
BRAND_KIT_RESPONSE_TIMEOUT=30

Додајте запис поддржан од околината во 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),
],

Архитектурата намерно е мала: API-услуга го поседува однесувањето на транспортот, доменски објект го валидира одговорот, команда ја координира трајната меморија, а контролерот за предлози чита само валидирани снимки. Екстракцијата е увоз активиран од оператор, наместо дел од барање насочено кон клиентот, па редица не е потребна тука. Ако увозите подоцна станат чести, оркестрацијата на командата може да се премести во задача во редица без да се промени API-границата.

Создадете идемпотентно складиште за комплети за бренд

Создадете миграција и модел brand_kits. Единствената изворна URL-адреса им овозможува на повторените увози да го ажурираат истиот запис наместо да создаваат двосмислени дупликати.

<?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',
        ];
    }
}

Извршете php artisan migrate откако ќе ја прегледате миграцијата за вашата платформа за бази на податоци.

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

Договорот за услугата ги изложува семантичките полиња што се користат подолу. Одржувањето на валидацијата во еден објект ги спречува контролерите и шаблоните да прават оптимистички претпоставки за оддалечениот JSON. Низите намерно остануваат нетранспарентни бидејќи поединечните претставувања на средства може да содржат докази и метаподатоци што апликацијата треба да ги зачува наместо повторно да ги толкува.

<?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);
    }
}

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

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

Клиентот повторно се обидува при неуспеси на поврзувањето и неуспеси на страната на серверот со кратко експоненцијално одложување. Не се обидува повторно при автентикација, валидација на барање или одговори за квота: непосредното повторување на тие повици не може да ја поправи основната состојба.

<?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);
    }
}

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

Увезете комплет за бренд од веб-страница

Командата ја валидира URL-адресата пред да направи излезно барање. Ограничувањето на увозите на јавни HTTPS-веб-страници го намалува случајниот пристап до внатрешни услуги. Построгите околини треба и да го разрешат името на домаќинот и да одбијат приватни, loopback, link-local и резервирани адреси по секое пренасочување.

<?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;
        }
    }
}

Извршете го вистинскиот увоз со php artisan brand:import https://client.example. Генераторот на предлози сега може да го вчита записот според URL-адресата на клиентот и да го проследи $brandKit->payload.

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

  • 401 или 403: потврдете дека токенот припаѓа на оваа услуга и не е поништен со регенерирање. Не обидувајте се повторно автоматски.
  • 429: запишете го структурираниот неуспех, намалете ја зачестеноста на увозот и обидете се повторно подоцна според одговорот на услугата и активниот план, наместо веднаш да влегувате во циклус.
  • Невалиден одговор: зачувајте ја претходната потврдена снимка, известете за видот на неуспехот и споредете ја тековната официјална документација со маперот на границата.
  • Истекување на времето или 5xx: клиентот прави само три ограничени обиди. Постојаните неуспеси треба да се појават во мониторингот, а не да држат работник на неодредено време.
  • Несовпаѓање на веб-страницата: запомнете дека екстракцијата се заснова на докази од доставената јавна URL-адреса. Прегледајте ги увезените средства пред да објавите предлог со висока вредност.

При распоредување, внесете го BRAND_KIT_API_TOKEN преку менаџерот за тајни на платформата, а потоа извршете php artisan config:cache само откако околината е присутна. Ограничете го извршувањето на командата на доверливи оператори, зачувајте ја последната валидна снимка од базата на податоци при прекини и следете ги броењата според видот на неуспех и статусот без да логирајте акредитиви или тела на одговори.

Конечна контролна листа за верификација

  • Активираниот план и токенот ограничен на услугата припаѓаат на услугата Brand Kit Extractor.
  • Токенот постои само во конфигурација поддржана од околината и во складиште за тајни.
  • Апликацијата испраќа JSON до точната крајна точка за екстракција со Bearer-автентикација.
  • Секоја задолжителна категорија на брендот се валидира пред трајното зачувување.
  • Неуспесите на автентикација, валидација и квота не се повторуваат неселективно.
  • Логовите содржат име на домаќин, статус, вид на неуспех и хеш на товарот, но немаат токен или сурово тело.
  • Слојот за извештаи ги третира CSS и URL-адресите на оддалечените средства како недоверлив влез.
  • Автоматизираните тестови поминуваат без контакт со надворешната услуга.
  • Вистински увоз ажурира еден идемпотентен запис и останува достапен за генераторот на предлози.

Вредниот дел од оваа интеграција не е само преземање лого. Тоа е воспоставување контролирано предавање помеѓу јавни докази за брендот и повторлив работен тек за документи. Откако таа граница е валидирана, набљудлива и безбедна при неуспех, конзистентните извештаи за клиентите престануваат да зависат од тоа некој да запомни која нијанса на сино била употребена последниот пат.

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

Mihajlo

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