Vodiči

Native PHP 8.3: Extract Brand Kits for Instant Landing Page Drafts

Native PHP 8.3: Izdvojite komplete brenda za trenutačne nacrte odredišnih stranica

Prazan uređivač odredišne stranice rijetko je ugodan prvi korak. Tijekom uključivanja korisnika, bolje je iskustvo pregledati postojeću javnu web-stranicu korisnika, izdvojiti njezin vizualni identitet i pripremiti prepoznatljiv nacrt koji mogu odmah doraditi.

Težak dio nije pozivanje API-ja. Težak dio je odlučiti koji izdvojeni podaci mogu postati tema, provjeriti vanjski odgovor bez ukazivanja povjerenja te osigurati da URL logotipa ili CSS vrijednost nikada ne postanu put za umetanje zlonamjernog sadržaja. Ovaj vodič gradi tu granicu u izvornom PHP-u 8.3 upotrebom cURL-a, nepromjenjivih domenskih objekata, ograničenih ponovnih pokušaja, atomskog spremanja i determinističkih PHPUnit testova.

Dobijte pristup i izradite servisni token

  1. Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
  2. Otvorite stranicu usluge Brand Kit Extractor.
  3. Odaberite dostupan Free, Plus ili Pro plan i dovršite njegovu aktivaciju.
  4. Otvorite službenu dokumentaciju usluge.
  5. Pronađite ploču Service token i kopirajte njezin token ograničen na uslugu.

API zahtijeva autentikaciju. Podržava Bearer token, zaglavlje X-API-Token ili parametar tokena u upitu. Ovaj projekt koristi Bearer token jer je manja vjerojatnost da će se zaglavlja, za razliku od nizova upita, pojaviti u zapisnicima pristupa i analitičkim sustavima.

Ponovno generiranje servisnog tokena opoziva prethodno aktivni token. Rotaciju tretirajte kao operaciju implementacije: instalirajte zamjenu u svaku instancu aplikacije, ponovno pokrenite ili učitajte te instance, provjerite novu vjerodajnicu i tek tada uklonite zastarjele verzije tajne iz svoje infrastrukture.

Potvrdite točan zahtjev

Integracija koristi POST https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit. Njezino JSON tijelo sadrži url. Dok je token privremeno izvezen u vašoj ljusci, pošaljite jedan minimalni zahtjev:

export BRAND_KIT_TOKEN='YOUR_SERVICE_TOKEN'

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

Sada stavite vjerodajnicu u nepredanu .env datoteku projekta. Izvorni PHP ovu datoteku ne učitava automatski; to će učiniti naše pokretanje konfiguracije. Predajte samo .env.example.

# .env.example
BRAND_KIT_TOKEN="YOUR_SERVICE_TOKEN"
DRAFT_STORAGE="/var/lib/landing-drafts"

# .gitignore
.env
/storage/*.json

Odaberite namjerno malu arhitekturu

Uključivanje korisnika zahtijeva jednu sinkronu operaciju: prihvatiti URL javne web-stranice, pozvati ekstraktor, preslikati njegove dokaze u ograničeni domenski objekt, izvesti siguran nacrt i spremiti JSON. Red bi zakomplicirao korisničko iskustvo, a ovdje nije potreban. Ako opažena latencija izdvajanja kasnije premaši proračun zahtjeva za uključivanje korisnika, isti klijent i preslikavač mogu se premjestiti iza radnika bez promjene svojih ugovora.

Važna je granica između izdvojenih dokaza i podataka teme koji se mogu prikazati. Logotipi, boje, fontovi, slike, društveni profili i CSS varijable provjeravaju se i zadržavaju, ali se sirovi CSS nikada ne umeće u stranicu. Nacrt odabire samo odobreni HTTPS resurs i sintaktički ispravnu heksadecimalnu boju; nazivi fontova i ostali dokazi ostaju prijedlozi dok prikazivač ne primijeni vlastito izbjegavanje posebnih znakova i popis dopuštenih vrijednosti.

brand-draft/
  composer.json
  .env.example
  config/app.php
  public/index.php
  src/Http/Transport.php
  src/BrandKitClient.php
  src/BrandKitMapper.php
  src/PublicWebsiteUrl.php
  src/DraftRepository.php
  tests/BrandKitClientTest.php
  tests/BrandKitMapperTest.php

Izradite projekt s PHP-om 8.3, proširenjem cURL, Composerom i PHPUnitom 11:

{
  "require": {
    "php": "^8.3",
    "ext-curl": "*"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Tests\\": "tests/"
    }
  }
}
composer install
composer dump-autoload
vendor/bin/phpunit --testdox

Izgradite ograničenu HTTP granicu

Prijenos obavlja točno jednu cURL operaciju. Politika ponovnih pokušaja pripada API klijentu, što je čini neovisno testabilnom. TLS provjera ostaje omogućena, vremena povezivanja i ukupnog odgovora ograničena su, a zaglavlja odgovora bilježe se radi obrade ograničenja stope.

<?php
// src/Http/Transport.php
declare(strict_types=1);

namespace App\Http;

final readonly class Response
{
    public function __construct(
        public int $status,
        public string $body,
        public array $headers = [],
    ) {}
}

interface Transport
{
    public function post(
        string $url,
        array $headers,
        string $body,
        int $connectTimeoutMs,
        int $timeoutMs,
    ): Response;
}

final class CurlTransport implements Transport
{
    public function post(
        string $url,
        array $headers,
        string $body,
        int $connectTimeoutMs,
        int $timeoutMs,
    ): Response {
        $receivedHeaders = [];
        $handle = curl_init($url);

        if ($handle === false) {
            throw new \RuntimeException('Unable to initialize cURL');
        }

        curl_setopt_array($handle, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $body,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT_MS => $connectTimeoutMs,
            CURLOPT_TIMEOUT_MS => $timeoutMs,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_HEADERFUNCTION => static function (
                $curl,
                string $line
            ) use (&$receivedHeaders): int {
                $parts = explode(':', $line, 2);
                if (count($parts) === 2) {
                    $receivedHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
                }
                return strlen($line);
            },
        ]);

        $bodyResult = curl_exec($handle);
        if ($bodyResult === false) {
            $message = curl_error($handle);
            curl_close($handle);
            throw new \RuntimeException('Brand service transport failure: ' . $message);
        }

        $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
        curl_close($handle);

        return new Response($status, $bodyResult, $receivedHeaders);
    }
}

Klijent ponovno pokušava neuspjehe prijenosa, HTTP 429 odgovore i 5xx odgovore najviše dvaput nakon početnog pokušaja. Ne pokušava ponovno neuspjehe autentikacije ni provjere zahtjeva. Brojčana vrijednost Retry-After poštuje se, ali je ograničena kako jedan zahtjev za uključivanje korisnika ne bi neograničeno zauzimao PHP radnika.

<?php
// src/BrandKitClient.php
declare(strict_types=1);

namespace App;

use App\Http\Transport;

final class BrandKitFailure extends \RuntimeException
{
    public function __construct(public readonly string $kind, string $message)
    {
        parent::__construct($message);
    }
}

final class BrandKitClient
{
    private const ENDPOINT =
        'https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit';

    public function __construct(
        private readonly Transport $transport,
        private readonly string $token,
        private readonly \Closure $sleep,
        private readonly \Closure $log,
    ) {
        if ($token === '') {
            throw new \InvalidArgumentException('BRAND_KIT_TOKEN is missing');
        }
    }

    public function extract(string $websiteUrl): array
    {
        $requestBody = json_encode(
            ['url' => $websiteUrl],
            JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
        );

        for ($attempt = 1; $attempt <= 3; $attempt++) {
            try {
                $response = $this->transport->post(
                    self::ENDPOINT,
                    [
                        'Authorization: Bearer ' . $this->token,
                        'Content-Type: application/json',
                        'Accept: application/json',
                    ],
                    $requestBody,
                    2_000,
                    15_000,
                );
            } catch (\RuntimeException $exception) {
                ($this->log)([
                    'event' => 'brand_kit_transport_failure',
                    'attempt' => $attempt,
                ]);

                if ($attempt === 3) {
                    throw new BrandKitFailure('transport', 'Extractor unavailable');
                }

                ($this->sleep)([200, 500][$attempt - 1]);
                continue;
            }

            ($this->log)([
                'event' => 'brand_kit_response',
                'attempt' => $attempt,
                'status' => $response->status,
            ]);

            if ($response->status >= 200 && $response->status < 300) {
                try {
                    $decoded = json_decode(
                        $response->body,
                        true,
                        64,
                        JSON_THROW_ON_ERROR
                    );
                } catch (\JsonException) {
                    throw new BrandKitFailure('invalid_response', 'Invalid JSON response');
                }

                if (!is_array($decoded)) {
                    throw new BrandKitFailure('invalid_response', 'Expected a JSON object');
                }

                return $decoded;
            }

            if ($response->status === 401 || $response->status === 403) {
                throw new BrandKitFailure('authentication', 'Service authentication failed');
            }

            if ($response->status === 429 || $response->status >= 500) {
                if ($attempt === 3) {
                    throw new BrandKitFailure(
                        $response->status === 429 ? 'rate_limit' : 'upstream',
                        'Extractor temporarily unavailable'
                    );
                }

                $retryAfter = $response->headers['retry-after'] ?? null;
                $delay = ctype_digit((string) $retryAfter)
                    ? min((int) $retryAfter * 1000, 2_000)
                    : [200, 500][$attempt - 1];

                ($this->sleep)($delay);
                continue;
            }

            throw new BrandKitFailure('request', 'Extractor rejected the request');
        }

        throw new BrandKitFailure('upstream', 'Unreachable retry state');
    }
}

Provjerite web-stranicu i obrambeno preslikajte dokaze

Odbijte vjerodajnice, ne-HTTP sheme, neuobičajene priključke, lokalne nazive i IP adrese u privatnim ili rezerviranim rasponima. DNS provjera smanjuje slučajne interne ciljeve, iako nije potpuna obrana od DNS ponovnog povezivanja. Kao dodatni sloj zadržite ograničenu politiku odlazne mreže.

<?php
// src/PublicWebsiteUrl.php
declare(strict_types=1);

namespace App;

final class PublicWebsiteUrl
{
    public static function validate(string $url): string
    {
        if (strlen($url) > 2048 || filter_var($url, FILTER_VALIDATE_URL) === false) {
            throw new \InvalidArgumentException('Invalid website URL');
        }

        $parts = parse_url($url);
        $scheme = strtolower((string) ($parts['scheme'] ?? ''));
        $host = strtolower((string) ($parts['host'] ?? ''));

        if (!in_array($scheme, ['http', 'https'], true)
            || $host === ''
            || isset($parts['user'])
            || isset($parts['pass'])
            || (isset($parts['port']) && !in_array($parts['port'], [80, 443], true))
            || $host === 'localhost'
        ) {
            throw new \InvalidArgumentException('A public HTTP(S) URL is required');
        }

        $addresses = filter_var($host, FILTER_VALIDATE_IP)
            ? [$host]
            : array_column(dns_get_record($host, DNS_A | DNS_AAAA), 'ip');

        if ($addresses === []) {
            throw new \InvalidArgumentException('Website host does not resolve');
        }

        foreach ($addresses as $address) {
            if (filter_var(
                $address,
                FILTER_VALIDATE_IP,
                FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
            ) === false) {
                throw new \InvalidArgumentException('Website host is not public');
            }
        }

        return $url;
    }
}

Ugovor obećava naziv brenda, logotipe, boje, fontove, slike, društvene profile i CSS varijable, ali velika i mala slova te interpunkcija ne bi trebali postati pretpostavke na granici aplikacije. Preslikavač kanonizira samo razdjelnike naziva polja, zahtijeva svako logičko polje, izdvaja ograničene skalarne dokaze te filtrira URL-ove i boje.

<?php
// src/BrandKitMapper.php
declare(strict_types=1);

namespace App;

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

final class BrandKitMapper
{
    public function map(array $payload): BrandKit
    {
        $brandName = $this->field($payload, 'brandname');
        if (!is_string($brandName) || trim($brandName) === '' || strlen($brandName) > 200) {
            throw new BrandKitFailure('invalid_response', 'Invalid brand name');
        }

        $logos = $this->safeUrls($this->values($this->field($payload, 'logos')));
        $colors = array_values(array_filter(
            $this->values($this->field($payload, 'colors')),
            static fn (string $value): bool =>
                preg_match('/^#[0-9a-fA-F]{3,8}$/', $value) === 1
        ));
        $fonts = $this->shortStrings($this->values($this->field($payload, 'fonts')));
        $imagery = $this->safeUrls($this->values($this->field($payload, 'imagery')));
        $social = $this->safeUrls(
            $this->values($this->field($payload, 'socialprofiles'))
        );

        $css = $this->field($payload, 'cssvariables');
        if (!is_array($css)) {
            throw new BrandKitFailure('invalid_response', 'Invalid CSS variables');
        }

        $safeCss = [];
        foreach ($css as $name => $value) {
            if (is_string($name)
                && preg_match('/^--[a-zA-Z0-9_-]{1,80}$/', $name)
                && is_scalar($value)
                && strlen((string) $value) <= 200
                && !preg_match('/[\x00-\x1F\x7F]/', (string) $value)
            ) {
                $safeCss[$name] = (string) $value;
            }
        }

        return new BrandKit(
            trim($brandName),
            $logos,
            $colors,
            $fonts,
            $imagery,
            $social,
            $safeCss,
        );
    }

    private function field(array $payload, string $logicalName): mixed
    {
        foreach ($payload as $name => $value) {
            $canonical = strtolower(preg_replace('/[^a-zA-Z0-9]/', '', (string) $name));
            if ($canonical === $logicalName) {
                return $value;
            }
        }

        throw new BrandKitFailure(
            'invalid_response',
            'Missing required brand-kit field: ' . $logicalName
        );
    }

    private function values(mixed $value): array
    {
        if (!is_array($value)) {
            throw new BrandKitFailure('invalid_response', 'Expected an evidence collection');
        }

        $iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($value));
        $result = [];

        foreach ($iterator as $item) {
            if (is_scalar($item) && count($result) < 100) {
                $result[] = trim((string) $item);
            }
        }

        return array_values(array_filter($result, static fn ($item) => $item !== ''));
    }

    private function safeUrls(array $values): array
    {
        return array_values(array_unique(array_filter(
            $values,
            static fn (string $value): bool =>
                strlen($value) <= 2048
                && filter_var($value, FILTER_VALIDATE_URL) !== false
                && strtolower((string) parse_url($value, PHP_URL_SCHEME)) === 'https'
        )));
    }

    private function shortStrings(array $values): array
    {
        return array_values(array_unique(array_filter(
            $values,
            static fn (string $value): bool =>
                strlen($value) <= 200
                && !preg_match('/[\x00-\x1F\x7F]/', $value)
        )));
    }
}

Izradite i spremite siguran nacrt

Spremište pohranjuje normalizirane dokaze uz namjerno skromnu temu. Nikada ne pretvara vraćenu mapu CSS varijabli u izvršivi CSS. Atomsko preimenovanje sprječava čitatelje da vide djelomično zapisan JSON.

<?php
// src/DraftRepository.php
declare(strict_types=1);

namespace App;

final class DraftRepository
{
    public function __construct(private readonly string $directory) {}

    public function save(string $sourceUrl, BrandKit $kit): array
    {
        $id = bin2hex(random_bytes(16));
        $draft = [
            'id' => $id,
            'sourceUrl' => $sourceUrl,
            'brandName' => $kit->brandName,
            'theme' => [
                'primaryColor' => $kit->colors[0] ?? '#222222',
                'logoUrl' => $kit->logos[0] ?? null,
                'fontSuggestion' => $kit->fonts[0] ?? null,
            ],
            'evidence' => [
                'logos' => $kit->logos,
                'colors' => $kit->colors,
                'fonts' => $kit->fonts,
                'imagery' => $kit->imagery,
                'socialProfiles' => $kit->socialProfiles,
                'cssVariables' => $kit->cssVariables,
            ],
            'status' => 'draft',
        ];

        if (!is_dir($this->directory)
            && !mkdir($this->directory, 0700, true)
            && !is_dir($this->directory)
        ) {
            throw new \RuntimeException('Unable to create draft storage');
        }

        $json = json_encode($draft, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
        $temporary = $this->directory . '/' . $id . '.tmp';
        $destination = $this->directory . '/' . $id . '.json';

        if (file_put_contents($temporary, $json, LOCK_EX) === false
            || !rename($temporary, $destination)
        ) {
            throw new \RuntimeException('Unable to store draft');
        }

        return $draft;
    }
}

Prednji kontroler prihvaća samo put uključivanja korisnika, ograničava veličinu zahtjeva, preslikava neuspjehe u stabilna HTTP stanja i ne vraća tijelo uzvodnog odgovora ni tajnu. Zapisi sadrže nazive događaja, pokušaje i statuse — ne tokene, tijela odgovora ni URL-ove korisnika.

<?php
// config/app.php
declare(strict_types=1);

$root = dirname(__DIR__);
if (is_file($root . '/.env')) {
    foreach (parse_ini_file($root . '/.env', false, INI_SCANNER_RAW) ?: [] as $key => $value) {
        if (getenv((string) $key) === false) {
            putenv($key . '=' . $value);
        }
    }
}

return [
    'token' => (string) getenv('BRAND_KIT_TOKEN'),
    'storage' => (string) (getenv('DRAFT_STORAGE') ?: $root . '/storage'),
];
<?php
// public/index.php
declare(strict_types=1);

use App\BrandKitClient;
use App\BrandKitFailure;
use App\BrandKitMapper;
use App\DraftRepository;
use App\Http\CurlTransport;
use App\PublicWebsiteUrl;

require dirname(__DIR__) . '/vendor/autoload.php';
$config = require dirname(__DIR__) . '/config/app.php';

header('Content-Type: application/json');

$respond = static function (int $status, array $body): never {
    http_response_code($status);
    echo json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
    exit;
};

if ($_SERVER['REQUEST_METHOD'] !== 'POST'
    || parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) !== '/onboarding/theme-draft'
) {
    $respond(404, ['error' => 'not_found']);
}

if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 4096) {
    $respond(413, ['error' => 'request_too_large']);
}

try {
    $input = json_decode(file_get_contents('php://input'), true, 16, JSON_THROW_ON_ERROR);
    $url = PublicWebsiteUrl::validate((string) ($input['url'] ?? ''));

    $logger = static function (array $record): void {
        error_log(json_encode($record, JSON_THROW_ON_ERROR));
    };
    $sleep = static fn (int $milliseconds) => usleep($milliseconds * 1000);

    $client = new BrandKitClient(
        new CurlTransport(),
        $config['token'],
        $sleep,
        $logger
    );

    $kit = (new BrandKitMapper())->map($client->extract($url));
    $draft = (new DraftRepository($config['storage']))->save($url, $kit);

    $respond(201, $draft);
} catch (\InvalidArgumentException | \JsonException) {
    $respond(422, ['error' => 'invalid_input']);
} catch (BrandKitFailure $failure) {
    $status = match ($failure->kind) {
        'rate_limit' => 429,
        'request' => 422,
        default => 502,
    };
    $respond($status, ['error' => $failure->kind]);
} catch (\Throwable $failure) {
    error_log(json_encode(['event' => 'draft_creation_failure']));
    $respond(500, ['error' => 'internal_error']);
}

Testirajte ponovne pokušaje i provjeru granica

Deterministički lažni prijenos čini putanje neuspjeha brzima i ponovljivima. Test u nastavku dokazuje da se za 429 pokušava ponovno, da su odgode vidljive bez čekanja te da se neuspjesi autentikacije ne pokušavaju ponovno.

<?php
// tests/BrandKitClientTest.php
declare(strict_types=1);

namespace Tests;

use App\BrandKitClient;
use App\BrandKitFailure;
use App\Http\Response;
use App\Http\Transport;
use PHPUnit\Framework\TestCase;

final class FakeTransport implements Transport
{
    public int $calls = 0;

    public function __construct(private array $responses) {}

    public function post(
        string $url,
        array $headers,
        string $body,
        int $connectTimeoutMs,
        int $timeoutMs,
    ): Response {
        $this->calls++;
        return array_shift($this->responses);
    }
}

final class BrandKitClientTest extends TestCase
{
    public function testRetriesRateLimitThenReturnsPayload(): void
    {
        $transport = new FakeTransport([
            new Response(429, '{}', ['retry-after' => '1']),
            new Response(200, '{"brand_name":"Acme"}'),
        ]);
        $delays = [];

        $client = new BrandKitClient(
            $transport,
            'test-token',
            static function (int $ms) use (&$delays): void {
                $delays[] = $ms;
            },
            static function (array $record): void {},
        );

        self::assertSame(['brand_name' => 'Acme'], $client->extract('https://example.com'));
        self::assertSame(2, $transport->calls);
        self::assertSame([1000], $delays);
    }

    public function testDoesNotRetryAuthenticationFailure(): void
    {
        $transport = new FakeTransport([new Response(401, '{}')]);
        $client = new BrandKitClient(
            $transport,
            'test-token',
            static function (int $ms): void {},
            static function (array $record): void {},
        );

        try {
            $client->extract('https://example.com');
            self::fail('Expected authentication failure');
        } catch (BrandKitFailure $failure) {
            self::assertSame('authentication', $failure->kind);
            self::assertSame(1, $transport->calls);
        }
    }
}

Dodajte testove preslikavača s fixtureom koji sadrži svih sedam potrebnih logičkih polja. Potvrdite da polja koja nedostaju ne uspijevaju, da se HTTP URL-ovi logotipa odbacuju, da neispravne boje nikada ne postaju primarna boja i da se vrijednosti CSS varijabli koje sadržavaju kontrolne znakove isključuju. Fixturei trebaju koristiti očito lažni token i sintetičke podatke o brendu, nikada kopirane odgovore korisnika.

Implementirajte uz očuvanu granicu

Pokrenite aplikaciju iza PHP-FPM-a i usmjerite /onboarding/theme-draft na public/index.php. Umetnite BRAND_KIT_TOKEN putem upravitelja tajni hosting platforme; produkcija ne bi trebala ovisiti o implementiranoj .env datoteci. Osigurajte da PHP-FPM zadržava potrebne varijable okruženja te montirajte DRAFT_STORAGE izvan javnog korijena dokumenta s pristupom za pisanje samo za korisnika aplikacije.

Zadržite omogućenu provjeru cURL certifikata. Ograničite odlazni promet na host usluge gdje to platforma za implementaciju podržava. Zaštitite krajnju točku uključivanja korisnika autentikacijom korisnika svoje aplikacije, CSRF pravilima i ograničavanjem stope po računu. Definirajte zadržavanje za izvorne URL-ove i izdvojene dokaze jer i javni podaci mogu identificirati korisnike.

Pratite brojanja i latenciju uspješnih izdvajanja, neuspjeha prijenosa, odgovora 429, uzvodnih odgovora 5xx, nevaljanih ugovora odgovora i neuspjeha spremanja nacrta. Upozorite na trajne neuspjehe autentikacije jer oni često ukazuju na istekao, opozvan ili nepotpuno implementiran token, a ne na unos korisnika.

Česti neuspjesi

  • 401 ili 403: potvrdite da je token ograničen na uslugu aktualan i da nijedna instanca još ne koristi token opozvan tijekom ponovnog generiranja.
  • 422 iz aplikacije: pregledajte poslani URL ili odbijanje uzvodnog zahtjeva; nemojte ga automatski ponovno pokušavati.
  • 429: klijent izvodi ograničeno postupno čekanje, ali ponovljena ograničenja trebaju postati stanje „pokušajte kasnije” vidljivo korisniku, a ne beskonačna petlja.
  • Nevaljan odgovor: zadržite klasifikaciju neuspjeha i pregledajte redigirane dijagnostičke podatke. Nikada ne spremajte tiho djelomični komplet kada nedostaje obavezno polje brenda.
  • Nema upotrebljivog logotipa ili boje: to nije nužno API neuspjeh. Siguran nacrt namjerno se vraća na bez logotipa i #222222, uz zadržavanje provjerenih dokaza za pregled.

Završni kontrolni popis za provjeru

  • Plan usluge je aktivan, a token je preuzet iz ploče Service token na stranici dokumentacije.
  • Vjerodajnica postoji samo u konfiguraciji potkrijepljenoj okruženjem i spremištu tajni.
  • Aplikacija poziva točnu POST krajnju točku s JSON-om url.
  • Naziv brenda, logotipi, boje, fontovi, slike, društveni profili i CSS varijable provjeravaju se prije spremanja.
  • Sirovi CSS i neprovjereni URL-ovi resursa nikada se ne prikazuju.
  • Istek vremena i ponovni pokušaji su ograničeni, dok se neuspjesi autentikacije i provjere ne pokušavaju ponovno.
  • PHPUnit testovi pokrivaju uspjeh, ograničavanje stope, neuspjeh autentikacije, polja koja nedostaju i nesigurne vrijednosti.
  • Stvarni zahtjev za uključivanje korisnika vraća HTTP 201 i atomski izrađuje jedan potpuni JSON nacrt.

Korisni ishod proizvoda nije samo izdvojeni komplet brenda. To je suzdržan prvi nacrt koji djeluje poznato bez pretvaranja da su vanjski podaci pouzdani. Ta razlika — prvo dokazi, zatim prikazivanje — pretvara praktičan API poziv u produkcijsku integraciju s kojom se korisnici mogu sigurno susresti na svom prvom zaslonu.

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.