Symfony: Автоматизирајте увоз на бренд-материјали за понуди со Brand Kit Extractor API
Генераторот на понуди станува многу покорисен кога „направи да одговара на брендот на клиентот“ престанува да биде рачна контролна листа. Тешкиот дел не е поставувањето лого над наслов. Тоа е прибавување на правилните средства, зачувување на нивното потекло, отфрлање на неисправни податоци и обезбедување прекин во надворешна услуга да не може да го наруши генерирањето понуди.
Ова упатство ја гради таа граница во Symfony и PHP 8.3. Конзолна команда испраќа URL на јавна веб-страница до API-то Brand Kit Extractor, го мапира одговорот во валидиран доменски објект и складира атомска снимка за обичен рендерер на понуди и извештаи. Патеката за рендерирање никогаш не го повикува API-то, така што претходно увезените брендови остануваат достапни при прекини или исцрпување на квотата.
Добијте пристап и создадете сервисен токен
Започнете со регистрирање сметка, или користете ја страницата за најава ако веќе имате сметка.
- Отворете ја страницата на услугата Brand Kit Extractor.
- Изберете достапен Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата.
- Најдете го панелот Service token и копирајте го токенот ограничен на услугата што е прикажан таму.
- Чувајте го тој токен во проектна конфигурација поддржана од околински променливи. Никогаш не го зачувувајте во репозиториумот.
Повторното генерирање на сервисниот токен го поништува претходно активниот токен, па ротацијата на токени мора да ја ажурира секоја распоредена околина што го користи. Оваа услуга бара автентикација. Прифаќа Bearer токен, заглавие X-API-Token или параметар за пребарување 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 \
--connect-timeout 5 \
--max-time 30 \
--request POST \
--header "Authorization: Bearer ${BRAND_KIT_TOKEN}" \
--header 'Content-Type: application/json' \
--data '{"url":"https://example.com"}' \
'https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit'
unset BRAND_KIT_TOKEN
Прегледајте ја тековната документација и вратениот товар при интеграцијата. Границата на апликацијата подолу намерно избегнува да претпостави недокументирана обвивка на одговор. Таа ги лоцира документираните семантички полиња по нормализирање на имиња како brand_name и brandName, а потоа бара по една недвосмислена појава на секое.
Поставете ја акредитацијата во .env.local, која Symfony проектите обично ја исклучуваат од контрола на верзии:
BRAND_KIT_TOKEN=YOUR_SERVICE_TOKEN
BRAND_KIT_ENDPOINT=https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit
Изберете намерно мала архитектура
На проектот му треба PHP 8.3 или понов, Composer, компонентите HttpClient и Console на Symfony, Twig и поддршка за PHPUnit. Создадете ја апликацијата со:
composer create-project symfony/skeleton brand-proposals
cd brand-proposals
composer require symfony/http-client symfony/console symfony/twig-bundle symfony/monolog-bundle
composer require --dev symfony/test-pack
Важните датотеки се:
src/Brand/BrandKit.php— доменско мапирање и валидација.src/Brand/BrandKitException.php— структурирани неуспеси.src/Brand/BrandKitExtractor.php— HTTP границата.src/Brand/BrandKitStore.php— атомски локални снимки.src/Command/ImportBrandKitCommand.php— увоз наменет за оператори.src/Controller/ProposalController.php— рендерирање понуди.templates/proposal/show.html.twig— излезот со брендирање.
Овој дизајн ги одржува увозите синхрони и експлицитни. Типичното извлекување бренд е повремена операција за поставување или освежување, а не дел од секое барање за страница. Messenger би додал оперативна механизација без да го подобри овој работен тек. Ако увозите подоцна пристигнуваат преку образец наменет за клиенти, истиот извлекувач може да се повика од Messenger обработувач.
Поврзете ги вредностите од околината преку инјектирање зависности во config/services.yaml:
parameters:
app.brand_kit_storage_dir: '%kernel.project_dir%/var/brand-kits'
services:
_defaults:
autowire: true
autoconfigure: true
bind:
$brandKitEndpoint: '%env(string:BRAND_KIT_ENDPOINT)%'
$brandKitToken: '%env(string:BRAND_KIT_TOKEN)%'
$brandKitStorageDir: '%app.brand_kit_storage_dir%'
App\:
resource: '../src/'
Изградете одбранбена доменска граница
API-то обезбедува податоци за бренд засновани на докази, но надворешниот JSON сè уште е недоверлив влез. Пред складирање, барајте име на бренд плус логоа, бои, фонтови, слики, социјални профили и CSS променливи. Отфрлете дупликати семантички полиња, преголеми товари, опасни CSS фрагменти и неисправни URL-адреси на средства.
Создадете src/Brand/BrandKit.php:
<?php
namespace App\Brand;
final readonly class BrandKit
{
private function __construct(
public string $name,
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
{
$read = static fn (string $field): mixed => self::findOne($payload, $field);
$name = $read('brandname');
if (!is_string($name) || trim($name) === '' || mb_strlen($name) > 200) {
throw new \UnexpectedValueException('Invalid brand name.');
}
$arrays = [];
foreach (['logos', 'colors', 'fonts', 'imagery', 'socialprofiles'] as $field) {
$value = $read($field);
if (!is_array($value)) {
throw new \UnexpectedValueException("Invalid {$field} collection.");
}
$arrays[$field] = $value;
}
$css = self::validateCssVariables($read('cssvariables'));
$kit = new self(
trim($name),
$arrays['logos'],
$arrays['colors'],
$arrays['fonts'],
$arrays['imagery'],
$arrays['socialprofiles'],
$css,
);
self::validateUrlFields($kit->toArray());
$json = json_encode($kit->toArray(), JSON_THROW_ON_ERROR);
if (strlen($json) > 1_000_000) {
throw new \UnexpectedValueException('Brand kit exceeds the storage limit.');
}
return $kit;
}
public function toArray(): array
{
return [
'brand_name' => $this->name,
'logos' => $this->logos,
'colors' => $this->colors,
'fonts' => $this->fonts,
'imagery' => $this->imagery,
'social_profiles' => $this->socialProfiles,
'css_variables' => $this->cssVariables,
];
}
public function primaryLogoUrl(): ?string
{
$scan = function (mixed $value) use (&$scan): ?string {
if (is_string($value)
&& filter_var($value, FILTER_VALIDATE_URL)
&& parse_url($value, PHP_URL_SCHEME) === 'https') {
return $value;
}
if (is_array($value)) {
foreach ($value as $child) {
if (($found = $scan($child)) !== null) {
return $found;
}
}
}
return null;
};
return $scan($this->logos);
}
private static function findOne(array $payload, string $wanted): mixed
{
$matches = [];
$walk = function (array $node, int $depth) use (&$walk, &$matches, $wanted): void {
if ($depth > 16) {
throw new \UnexpectedValueException('Response nesting is too deep.');
}
foreach ($node as $key => $value) {
$normalized = is_string($key)
? preg_replace('/[^a-z0-9]/', '', strtolower($key))
: '';
if ($normalized === $wanted) {
$matches[] = $value;
}
if (is_array($value)) {
$walk($value, $depth + 1);
}
}
};
$walk($payload, 0);
if (count($matches) !== 1) {
throw new \UnexpectedValueException("Expected exactly one {$wanted} field.");
}
return $matches[0];
}
private static function validateCssVariables(mixed $value): array
{
if (!is_array($value) || count($value) > 128) {
throw new \UnexpectedValueException('Invalid CSS variables.');
}
foreach ($value as $name => $cssValue) {
if (!is_string($name)
|| preg_match('/^--[a-zA-Z0-9-]+$/', $name) !== 1
|| !is_string($cssValue)
|| strlen($cssValue) > 200
|| preg_match('/[\x00-\x1F;{}<>]/', $cssValue)
|| stripos($cssValue, 'url(') !== false
|| stripos($cssValue, '@import') !== false) {
throw new \UnexpectedValueException('Unsafe CSS variable.');
}
}
return $value;
}
private static function validateUrlFields(array $node): void
{
foreach ($node as $key => $value) {
$normalized = is_string($key)
? preg_replace('/[^a-z0-9]/', '', strtolower($key))
: '';
if (is_string($value) && str_ends_with($normalized, 'url')) {
$scheme = parse_url($value, PHP_URL_SCHEME);
if (!filter_var($value, FILTER_VALIDATE_URL)
|| !in_array($scheme, ['http', 'https'], true)) {
throw new \UnexpectedValueException('Invalid asset URL.');
}
}
if (is_array($value)) {
self::validateUrlFields($value);
}
}
}
}
Маперот не тврди дека секоја боја, фонт или слика е безбедна за автоматско рендерирање. Тој воспоставува ограничена снимка безбедна за JSON. Понудата избира HTTPS лого и ограничена мапа на CSS променливи; преостанатите докази остануваат достапни за прегледани шаблони и извештаи.
Повикајте го API-то со ограничено однесување при неуспех
Создадете тип за неуспех во src/Brand/BrandKitException.php:
<?php
namespace App\Brand;
final class BrandKitException extends \RuntimeException
{
public function __construct(
public readonly string $kind,
string $message,
public readonly ?int $status = null,
?\Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
}
Потоа имплементирајте src/Brand/BrandKitExtractor.php. Користи вкупно три обиди, ограничено експоненцијално чекање, петсекундно ограничување за поврзување и вкупно траење на барањето од триесет секунди. Неуспесите при автентикација и валидација на барањето никогаш не се повторуваат.
<?php
namespace App\Brand;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class BrandKitExtractor
{
public function __construct(
private HttpClientInterface $http,
private LoggerInterface $logger,
private string $brandKitEndpoint,
private string $brandKitToken,
) {}
public function extract(string $website): BrandKit
{
$parts = parse_url($website);
if (!is_array($parts)
|| !in_array($parts['scheme'] ?? null, ['http', 'https'], true)
|| empty($parts['host'])
|| isset($parts['user'])
|| isset($parts['pass'])) {
throw new BrandKitException('invalid_request', 'Use a public HTTP or HTTPS URL.');
}
if ($this->brandKitToken === '') {
throw new BrandKitException('configuration', 'The service token is not configured.');
}
$hostHash = hash('sha256', strtolower($parts['host']));
$started = hrtime(true);
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->http->request('POST', $this->brandKitEndpoint, [
'auth_bearer' => $this->brandKitToken,
'json' => ['url' => $website],
'timeout' => 20.0,
'max_duration' => 30.0,
]);
$status = $response->getStatusCode();
} catch (TransportExceptionInterface $e) {
if ($attempt < 3) {
$this->backoff($attempt, null);
continue;
}
throw new BrandKitException('network', 'The extraction service is unreachable.', null, $e);
}
$headers = $response->getHeaders(false);
$retryable = in_array($status, [429, 500, 502, 503, 504], true);
if ($retryable && $attempt < 3) {
$this->logger->warning('Brand extraction will be retried.', [
'attempt' => $attempt,
'status' => $status,
'website_host_hash' => $hostHash,
]);
$this->backoff($attempt, $headers['retry-after'][0] ?? null);
continue;
}
if ($status < 200 || $status >= 300) {
$kind = match ($status) {
401, 403 => 'authentication',
400, 422 => 'invalid_request',
429 => 'rate_limited',
default => 'upstream',
};
throw new BrandKitException(
$kind,
'Brand extraction failed without storing a snapshot.',
$status,
);
}
try {
$content = $response->getContent(false);
} catch (TransportExceptionInterface $e) {
if ($attempt < 3) {
$this->backoff($attempt, null);
continue;
}
throw new BrandKitException('network', 'The response body was interrupted.', null, $e);
}
if (strlen($content) > 1_000_000) {
throw new BrandKitException('invalid_response', 'The response is too large.');
}
try {
$payload = json_decode($content, true, 64, JSON_THROW_ON_ERROR);
if (!is_array($payload)) {
throw new \UnexpectedValueException('Expected a JSON object or array.');
}
$kit = BrandKit::fromApi($payload);
} catch (\JsonException|\UnexpectedValueException $e) {
throw new BrandKitException(
'invalid_response',
'The response did not satisfy the brand-kit contract.',
$status,
$e,
);
}
$this->logger->info('Brand extraction completed.', [
'status' => $status,
'attempt' => $attempt,
'duration_ms' => (int) ((hrtime(true) - $started) / 1_000_000),
'website_host_hash' => $hostHash,
]);
return $kit;
}
throw new BrandKitException('upstream', 'Brand extraction failed.');
}
private function backoff(int $attempt, ?string $retryAfter): void
{
$milliseconds = ctype_digit((string) $retryAfter)
? min(5_000, (int) $retryAfter * 1_000)
: min(2_000, 250 * (2 ** ($attempt - 1)) + random_int(0, 100));
usleep($milliseconds * 1_000);
}
}
Не се евидентираат тело на одговор, токен, целосна URL-адреса на веб-страницата или извлечено средство. Стабилен хеш на името на домаќинот е доволен за поврзување на повторните обиди без невнимателно изложување на домените на клиентите.
Складирајте атомска снимка и обезбедете команда за увоз
За фриленсер или мал тим што извршува една инстанца на апликацијата, JSON снимките со верзии лесно се прегледуваат и прават резервна копија. Создадете src/Brand/BrandKitStore.php:
<?php
namespace App\Brand;
final class BrandKitStore
{
public function __construct(private string $brandKitStorageDir) {}
public function save(string $client, BrandKit $kit): void
{
$path = $this->path($client);
if (!is_dir($this->brandKitStorageDir)
&& !mkdir($this->brandKitStorageDir, 0770, true)
&& !is_dir($this->brandKitStorageDir)) {
throw new \RuntimeException('Cannot create brand-kit storage.');
}
$temporary = tempnam($this->brandKitStorageDir, '.brand-kit-');
if ($temporary === false) {
throw new \RuntimeException('Cannot create a temporary snapshot.');
}
try {
$json = json_encode(
$kit->toArray(),
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES,
);
if (file_put_contents($temporary, $json, LOCK_EX) === false) {
throw new \RuntimeException('Cannot write the brand-kit snapshot.');
}
chmod($temporary, 0640);
if (!rename($temporary, $path)) {
throw new \RuntimeException('Cannot publish the brand-kit snapshot.');
}
} finally {
if (is_file($temporary)) {
unlink($temporary);
}
}
}
public function load(string $client): BrandKit
{
$json = file_get_contents($this->path($client));
if ($json === false) {
throw new \RuntimeException('No imported brand kit exists.');
}
return BrandKit::fromApi(
json_decode($json, true, 64, JSON_THROW_ON_ERROR),
);
}
public function has(string $client): bool
{
return is_file($this->path($client));
}
private function path(string $client): string
{
if (preg_match('/^[a-z0-9][a-z0-9-]{1,63}$/', $client) !== 1) {
throw new \InvalidArgumentException('Invalid client identifier.');
}
return $this->brandKitStorageDir.'/'.$client.'.json';
}
}
Повторната валидација на снимките при вчитување го штити рендерерот од рачни измени и оштетени распоредувања. Привремената датотека се наоѓа во одредишниот директориум, што овозможува конечното преименување да остане атомско во вообичаено Linux распоредување.
Создадете src/Command/ImportBrandKitCommand.php:
<?php
namespace App\Command;
use App\Brand\BrandKitException;
use App\Brand\BrandKitExtractor;
use App\Brand\BrandKitStore;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'app:brand-kit:import', description: 'Import a verified brand snapshot.')]
final class ImportBrandKitCommand extends Command
{
public function __construct(
private BrandKitExtractor $extractor,
private BrandKitStore $store,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('client', InputArgument::REQUIRED, 'Stable lowercase client identifier')
->addArgument('website', InputArgument::REQUIRED, 'Public brand website URL');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$client = (string) $input->getArgument('client');
$website = (string) $input->getArgument('website');
try {
$kit = $this->extractor->extract($website);
$this->store->save($client, $kit);
} catch (BrandKitException|\InvalidArgumentException|\RuntimeException $e) {
$kind = $e instanceof BrandKitException ? $e->kind : 'storage';
$io->error("Import failed ({$kind}): {$e->getMessage()}");
return Command::FAILURE;
}
$io->success("Imported the brand kit for {$kit->name}.");
return Command::SUCCESS;
}
}
Извршете увоз со:
php bin/console app:brand-kit:import acme https://www.example.com
Неуспешното освежување ја остава претходната снимка недопрена. Тоа својство е поважно од обидот рендерирањето на понудите да се направи „во живо“.
Користете ги увезените средства во генераторот на понуди
Создадете лесна крајна точка за рендерирање во src/Controller/ProposalController.php:
<?php
namespace App\Controller;
use App\Brand\BrandKitStore;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ProposalController extends AbstractController
{
#[Route(
'/proposals/{client}',
name: 'proposal_show',
requirements: ['client' => '[a-z0-9][a-z0-9-]{1,63}'],
methods: ['GET'],
)]
public function show(string $client, Request $request, BrandKitStore $store): Response
{
if (!$store->has($client)) {
throw $this->createNotFoundException('Import this client brand first.');
}
return $this->render('proposal/show.html.twig', [
'brand' => $store->load($client),
'subject' => $request->query->getString(
'subject',
'Services proposal',
),
]);
}
}
Соодветниот templates/proposal/show.html.twig ја демонстрира точката на интеграција што ја користат и HTML понудите и подоцнежен PDF процес:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ subject }} — {{ brand.name }}</title>
<style>
:root {
{% for name, value in brand.cssVariables %}
{{ name }}: {{ value }};
{% endfor %}
}
body { font-family: system-ui, sans-serif; margin: 3rem; }
header { border-bottom: 3px solid var(--primary-color, #222); }
img { display: block; max-height: 72px; max-width: 240px; }
</style>
</head>
<body>
<header>
{% if brand.primaryLogoUrl %}
<img src="{{ brand.primaryLogoUrl }}" alt="{{ brand.name }} logo">
{% endif %}
<h1>{{ subject }}</h1>
<p>Prepared for {{ brand.name }}</p>
</header>
<main>
<h2>Scope</h2>
<p>Replace this section with proposal or report data from your domain.</p>
</main>
</body>
</html>
Twig ги избегнува името, предметот и URL-адресата на логото. CSS вредностите добиваат дополнителна доменска валидација бидејќи само HTML избегнувањето не го прави произволниот текст безбеден во CSS контекст. Фонтовите се задржуваат во снимката, но не се преземаат автоматски; надворешно хостираните фонтови може да воведат проблеми со лиценцирање, приватност, достапност и перформанси.
Тестирајте ја границата без мрежни повици
MockHttpClient на Symfony обезбедува детерминистички транспорт. Следните тестови го проверуваат барањето, успешното мапирање и критичното правило дека неуспесите при автентикација не се повторуваат. Додадете tests/Brand/BrandKitExtractorTest.php:
<?php
namespace App\Tests\Brand;
use App\Brand\BrandKitException;
use App\Brand\BrandKitExtractor;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class BrandKitExtractorTest extends TestCase
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit';
public function testItMapsAValidResponse(): void
{
$client = new MockHttpClient(function (
string $method,
string $url,
array $options,
): MockResponse {
self::assertSame('POST', $method);
self::assertSame(self::ENDPOINT, $url);
self::assertSame(
['url' => 'https://example.com'],
json_decode($options['body'], true, flags: JSON_THROW_ON_ERROR),
);
return new MockResponse(json_encode([
'brand_name' => 'Example Studio',
'logos' => [['url' => 'https://example.com/logo.svg']],
'colors' => ['#16324f'],
'fonts' => ['Example Sans'],
'imagery' => [],
'social_profiles' => [],
'css_variables' => ['--primary-color' => '#16324f'],
], JSON_THROW_ON_ERROR), ['http_code' => 200]);
});
$extractor = new BrandKitExtractor(
$client,
new NullLogger(),
self::ENDPOINT,
'test-token',
);
$kit = $extractor->extract('https://example.com');
self::assertSame('Example Studio', $kit->name);
self::assertSame('https://example.com/logo.svg', $kit->primaryLogoUrl());
}
public function testAuthenticationFailureIsNotRetried(): void
{
$client = new MockHttpClient([
new MockResponse('{}', ['http_code' => 401]),
]);
$extractor = new BrandKitExtractor(
$client,
new NullLogger(),
self::ENDPOINT,
'invalid-test-token',
);
try {
$extractor->extract('https://example.com');
self::fail('Expected an authentication failure.');
} catch (BrandKitException $e) {
self::assertSame('authentication', $e->kind);
self::assertSame(401, $e->status);
}
self::assertSame(1, $client->getRequestsCount());
}
}
Чувајте ги фикстурите синтетички, а акредитациите очигледно лажни. Додадете тестови за неисправен JSON, дуплирани семантички полиња, небезбеден CSS, преголеми одговори, краен 429 и замена на складиштето пред да го сметате увозникот за завршена продукциска функционалност.
Безбедност, набљудливост и распоредување
Чувајте го .env.local само за локален развој. Во продукција, вметнете го BRAND_KIT_TOKEN преку управувачот со тајни на платформата или механизмот за тајни на Symfony. Ограничете го пристапот до командата за увоз на доверливи оператори, ротирајте го токенот намерно и осигурете дневниците никогаш да не содржат заглавија за авторизација или тела на одговори.
Валидаторот на влез отфрла акредитации вградени во URL-адреси, но продукциските распоредувања треба да отфрлаат и localhost, литерали на приватни адреси и домени само за внатрешна употреба според сопствената мрежна политика. Оддалечената услуга е наменета за јавни веб-страници; не ја користете како пат до внатрешни ресурси.
Структурираните полиња во дневникот овозможуваат мерење на траење, статус, обиди и зачестеност на повторувања. Поставете предупредувања за трајни неуспеси при автентикација, ограничување на стапка, неисправни одговори и невообичаено висока латентност. Еден повремен повторен обид е оперативен детал; повторените конечни неуспеси укажуваат на истечен токен, ограничување на план, променет договор за одговор, проблем со мрежата или недостапна надворешна услуга.
Дајте му на корисникот во извршувањето пристап за запишување во var/brand-kits, извршете ги тестовите, загрејте го продукцискиот кеш и извршете еден увоз:
APP_ENV=test php bin/phpunit
APP_ENV=prod APP_DEBUG=0 php bin/console cache:warmup
APP_ENV=prod APP_DEBUG=0 php bin/console app:brand-kit:import acme https://www.example.com
Локалните датотеки функционираат добро на еден сервер. На повеќе реплики им треба споделено трајно складирање или репозиториум поддржан од база на податоци; во спротивно една реплика може да увезе снимка што друга не може да ја види. Зачувајте го истиот интерфејс BrandKitStore при промена на складиштето, за извлекувачот и рендерерот да останат недопрени.
Вообичаени неуспеси и нивното значење
- 401 или 403: токенот недостасува, е неважечки, поништен или недостапен за извршувањето. Исправете ја конфигурацијата; не повторувајте.
- 400 или 422: поднесената URL-адреса е неприфатлива. Исправете ја URL-адресата наместо да ја повторувате.
- 429: активниот план или услугата ги ограничува барањата. Почитувајте ограничено чекање и закажете го освежувањето подоцна.
- 500, 502, 503 или 504: привремен проблем во надворешната услуга може да ги оправда веќе имплементираните ограничени повторни обиди.
- Неисправен одговор: задржете ја последната добра снимка, споредете ја тековната официјална документација со маперот и ажурирајте ги тестовите пред да прифатите променета структура.
- Понудата нема лого: одговорот можеби не содржи употребливо HTTPS лого. Рендерирајте ја текстуалната резерва наместо да ја ослабите URL валидацијата.
- Снимката не може да се запише: проверете ги сопственоста на директориумот, дозволите, конфигурацијата на трајниот волумен и слободниот простор на дискот.
Конечна контролна листа за проверка
- Сметката и Free, Plus или Pro планот се активни.
- Токенот ограничен на услугата доаѓа од панелот Service token на страницата со документација.
BRAND_KIT_TOKENсе вметнува преку конфигурација поддржана од околински променливи и не е присутен во Git и дневниците.- Апликацијата испраќа
POSTдо точната крајна точка за извлекување со JSONurl. - Името на брендот, логоата, боите, фонтовите, сликите, социјалните профили и CSS променливите се валидираат пред складирање.
- Неуспесите при автентикација и валидација не се повторуваат; преодните неуспеси користат ограничено чекање.
- Неуспешното освежување ја зачувува последната валидна снимка.
- Рутата за понудата рендерира безбедно кога недостига лого.
- Тестовите користат
MockHttpClientи никогаш не контактираат со вистинската услуга. - Продукциското складиште е запишливо, трајно, со резервна копија и споделено кога апликацијата има повеќе реплики.
Трајната архитектонска придобивка не е автоматското поставување лого. Таа е раздвојувањето меѓу прибавување и објавување: ненадежна надворешна операција создава мала, проверена снимка, додека секојдневниот генератор на понуди троши само локални, валидирани податоци. Таа граница го прави брендирањето практично без да дозволи акредитациите, неисправните средства, квотите или прекинот на надворешната услуга да одредуваат дали може да се достави понуда.