Туториали

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

Native PHP 8.3: Извлечете бренд-комплети за инстант нацрти на целни страници

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

Тешкиот дел не е повикувањето API. Тој е во одлучувањето кои извлечени податоци можат да станат тема, во валидацијата на надворешен одговор без да му се верува и во обезбедувањето URL-адреса на лого или CSS-вредност никогаш да не стане патека за инјектирање. Овој туторијал ја гради таа граница во Native PHP 8.3 со cURL, непроменливи доменски објекти, ограничени повторни обиди, атомско складирање и детерминистички PHPUnit тестови.

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

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

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

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

Потврдете го точниот барање

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

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

Сега ставете ја акредитацијата во непотврдената .env датотека на проектот. Native PHP не ја вчитува оваа датотека автоматски; нашето подигнување на конфигурацијата ќе го направи тоа. Потврдете само .env.example.

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

# .gitignore
.env
/storage/*.json

Изберете намерно мала архитектура

Воведувањето бара една синхрона операција: прифатете URL-адреса на јавна веб-страница, повикајте го извлекувачот, пресликајте ги неговите докази во ограничен доменски објект, изведете безбеден нацрт и зачувајте JSON. Редицата би го усложнила корисничкото искуство без да е потребна овде. Ако забележаната латентност на извлекувањето подоцна го надмине буџетот за барањето при воведување, истиот клиент и пресликувач може да се преместат зад worker без да им се менуваат договорите.

Важната граница е помеѓу извлечените докази и податоците за темата што може да се прикажат. Логоа, бои, фонтови, слики, социјални профили и CSS-променливи се валидираат и задржуваат, но суровиот CSS никогаш не се инјектира во страница. Нацртот избира само одобрено HTTPS-средство и синтаксички валидна хексадецимална боја; имињата на фонтовите и другите докази остануваат предлози сè додека рендерер не примени сопствено ескепирање и allowlist.

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

Создадете го проектот со PHP 8.3, екстензијата cURL, Composer и PHPUnit 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

Изградете ограничена HTTP-граница

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

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

Клиентот повторува транспортни неуспеси, HTTP 429 одговори и 5xx одговори најмногу двапати по почетниот обид. Не повторува неуспеси при автентикација или валидација на барањето. Нумеричка вредност Retry-After се почитува, но е ограничена за едно барање при воведување да не може неограничено да зафаќа PHP worker.

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

Валидирајте ја веб-страницата и одбранбено пресликајте ги доказите

Одбијте акредитиви, не-HTTP шеми, невообичаени порти, локални имиња и IP-адреси во приватни или резервирани опсези. DNS-проверката ги намалува случајните внатрешни цели, иако не е целосна одбрана од DNS rebinding. Одржувајте ја политиката за појдовна мрежа ограничена како дополнителен слој.

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

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

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

Создадете и зачувајте го безбедниот нацрт

Складиштето чува нормализирани докази заедно со намерно скромна тема. Никогаш не ја претвора вратената мапа на CSS-променливи во извршлив CSS. Атомското преименување ги спречува читателите да забележат делумно запишан 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;
    }
}

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

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

Тестирајте повторни обиди и валидација на границата

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

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

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

Распоредете со недопрена граница

Извршувајте ја апликацијата зад PHP-FPM и насочете /onboarding/theme-draft кон public/index.php. Внесете го BRAND_KIT_TOKEN преку управувачот со тајни на хостинг-платформата; продукцијата не треба да зависи од распоредена .env датотека. Осигурете се PHP-FPM да ги зачува потребните променливи на околината и монтирајте DRAFT_STORAGE надвор од јавниот корен на документи со пристап за запишување само за корисникот на апликацијата.

Оставете ја овозможена cURL-потврдата на сертификати. Ограничете го појдовниот сообраќај кон хостот на услугата каде што платформата за распоредување го поддржува тоа. Заштитете ја крајната точка за воведување со автентикацијата на корисниците на вашата апликација, CSRF-политиката и ограничувањето на стапката по сметка. Дефинирајте задржување за изворните URL-адреси и извлечените докази бидејќи јавните податоци сè уште можат да ги идентификуваат клиентите.

Следете ги броењата и латентноста за успешни извлекувања, транспортни неуспеси, 429 одговори, upstream 5xx одговори, невалидни договори за одговор и неуспеси при складирање на нацрти. Поставете предупредување за трајни неуспеси при автентикација бидејќи тие најчесто укажуваат на истечен, поништен или нецелосно распореден токен, а не на внес од клиентот.

Чести неуспеси

  • 401 или 403: потврдете дека токенот ограничен на услугата е тековен и дека ниту една инстанца сè уште не го користи токенот поништен при повторното генерирање.
  • 422 од апликацијата: прегледајте ја испратената URL-адреса или отфрлање на upstream барање; не повторувајте го автоматски.
  • 429: клиентот извршува ограничено повлекување, но повторените ограничувања треба да станат состојба „обидете се подоцна“ видлива за корисникот, наместо бесконечна јамка.
  • Невалиден одговор: задржете ја класификацијата на неуспехот и прегледајте ги редигираните дијагностики. Никогаш тивко не чувајте делумен комплет кога недостасува задолжително поле за брендот.
  • Нема употребливо лого или боја: ова не е нужно API-неуспех. Безбедниот нацрт намерно се враќа на без лого и #222222, додека ги зачувува валидираните докази за преглед.

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

  • Планот за услугата е активен и токенот доаѓа од панелот Service token на страницата со документација.
  • Акредитацијата постои само во конфигурација поддржана од околината и складирање на тајни.
  • Апликацијата ја повикува точната POST крајна точка со JSON url.
  • Името на брендот, логоата, боите, фонтовите, сликите, социјалните профили и CSS-променливите се валидираат пред складирање.
  • Суровиот CSS и невалидирани URL-адреси на средства никогаш не се прикажуваат.
  • Тајмаутите и повторните обиди се ограничени, додека неуспесите при автентикација и валидација не се повторуваат.
  • PHPUnit тестовите покриваат успех, ограничување на стапката, неуспех при автентикација, полиња што недостигаат и небезбедни вредности.
  • Вистинско барање при воведување враќа HTTP 201 и атомски создава еден целосен JSON-нацрт.

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

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

Mihajlo

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