Native PHP 8.3: Автоматско пополнување на работните простори за нови клиенти со API-то Brand Kit Extractor
Новиот работен простор за клиентот треба да изгледа подготвен уште при првото најавување, а не како празен формулар што бара од некого да ја препише сопствената веб-страница. Практичен тек на воведување може да ја прифати јавната URL-адреса на клиентот, да го извлече неговиот визуелен идентитет, да го потврди резултатот и однапред да го пополни работниот простор со употребливи логоа, бои и фонтови.
Ова упатство го гради тој тек во Native PHP 8.3 со користење cURL, строга граница на доменот, ограничени повторни обиди, атомско складирање, структурирани грешки и детерминистички PHPUnit тестови. API-то Brand Kit Extractor останува изолирано зад еден клиент, така што контролерите и кодот за складирање никогаш не зависат од детали за транспортот или од непотврден далечински JSON.
Добијте пристап и копирајте токен со опфат на услугата
Започнете со создавање сметка на https://ai.mihajlo.mk/register. Ако веќе имате сметка, најавете се на https://ai.mihajlo.mk/login.
- Отворете ја страницата на услугата Brand Kit Extractor.
- Изберете го достапниот Free, Plus или Pro план што одговара на очекуваната употреба, а потоа завршете ја активацијата.
- Отворете ја официјалната документација за услугата.
- Пронајдете го панелот Service token и копирајте го токенот со опфат на услугата.
- Зачувајте го во проектна конфигурација поддржана од променливи на околината, никогаш во PHP изворниот код.
Оваа услуга не е без токени: секое барање за извлекување мора да се автентицира со Bearer токен, заглавие X-API-Token или параметар за пребарување token. Ќе ја користиме Bearer формата бидејќи ги задржува акредитивите надвор од URL-адресите и рутинските дневници за пристап.
Повторното генерирање на токенот за услугата го поништува претходно активниот токен. Третирајте го повторното генерирање како ротација на акредитиви: ажурирајте ја тајната за распоредување, рестартирајте или повторно распоредете го секој процес што ја чита, потврдете барање со новиот токен и дури потоа сметајте дека пуштањето е завршено.
Потврдете го HTTP договорот пред да ја напишете функционалноста
Точната операција е POST https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit. Нејзиното JSON тело на барањето содржи url. Извршете едно минимално барање со јавна страница што сте овластени да ја обработувате:
curl --request POST \
--url 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://example.com"}'
Не го пренасочувајте ова низ детално дебагирање во споделени терминали или CI, бидејќи аргументите на командата и заглавијата може да бидат зачувани. Вистинската апликација ќе ги испрати истиот метод, крајна точка, заглавија и тело, додека токенот ќе остане во нејзината околина.
Поставете ја Native PHP проектната структура
Дизајнот е намерно мал. Транспортот се справува со cURL, API клиентот ги поседува повторните обиди и валидацијата на одговорот, провизионерот ги применува доменските правила, а контролерот ги преведува неуспесите на апликацијата во HTTP одговори. Извлекувањето останува синхроно бидејќи создавањето работен простор веднаш го бара резултатот; ако доцнењето при воведување подоцна стане неприфатливо, провизионерот е местото каде што треба да се премести зад извршувач на задачи.
brand-workspace/
├── .env
├── .env.example
├── composer.json
├── public/
│ └── create-workspace.php
├── src/
│ ├── BrandKit.php
│ ├── BrandKitClient.php
│ ├── CurlTransport.php
│ ├── Transport.php
│ └── WorkspaceProvisioner.php
├── storage/
│ └── workspaces/
└── tests/
└── BrandKitClientTest.php
Побарајте PHP 8.3 и cURL, конфигурирајте PSR-4 автоматско вчитување и инсталирајте PHPUnit 11 за развој:
{
"require": {
"php": "^8.3",
"ext-curl": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"Workspace\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Workspace\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit tests"
}
}
composer install
composer dump-autoload
cp .env.example .env
# Edit .env locally, then export it before starting PHP:
set -a
. ./.env
set +a
php -S 127.0.0.1:8080 -t public
Native PHP не вчитува .env автоматски. Горенаведените shell команди се погодни за локален развој; продукцијата треба да инјектира променливи преку својот управувач со процеси, извршувачко опкружување за контејнери или складиште за тајни. Исклучете го .env од контрола на верзии.
BRAND_KIT_TOKEN=YOUR_SERVICE_TOKEN
BRAND_KIT_CONNECT_TIMEOUT=3
BRAND_KIT_RESPONSE_TIMEOUT=20
WORKSPACE_STORAGE=/absolute/path/to/brand-workspace/storage/workspaces
Изградете тесен, заменлив cURL транспорт
Транспортот враќа статус, заглавија и тело без да ги толкува податоците за брендот. Ова раздвојување им дава на тестовите детерминистичка имитација и го задржува однесувањето специфично за cURL надвор од доменскиот слој.
<?php
// src/Transport.php
namespace Workspace;
final readonly class HttpResponse
{
public function __construct(
public int $status,
public array $headers,
public string $body,
) {}
}
interface Transport
{
public function post(
string $url,
array $headers,
string $body,
float $connectTimeout,
float $responseTimeout,
): HttpResponse;
}
final class TransportException extends \RuntimeException {}
final class ApiException extends \RuntimeException {}
<?php
// src/CurlTransport.php
namespace Workspace;
final class CurlTransport implements Transport
{
public function post(
string $url,
array $headers,
string $body,
float $connectTimeout,
float $responseTimeout,
): HttpResponse {
$handle = curl_init($url);
if ($handle === false) {
throw new TransportException('Could not initialize cURL');
}
$responseHeaders = [];
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => (int) ($connectTimeout * 1000),
CURLOPT_TIMEOUT_MS => (int) ($responseTimeout * 1000),
CURLOPT_HEADERFUNCTION => static function (
\CurlHandle $handle,
string $line
) use (&$responseHeaders): int {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$responseHeaders[strtolower(trim($parts[0]))] =
trim($parts[1]);
}
return strlen($line);
},
]);
$bodyResult = curl_exec($handle);
if ($bodyResult === false) {
throw new TransportException(curl_error($handle));
}
return new HttpResponse(
(int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE),
$responseHeaders,
$bodyResult,
);
}
}
Валидирајте ги далечинските податоци на границата на апликацијата
Услугата враќа име на бренд плус логоа, бои, фонтови, слики, социјални профили и CSS променливи. Далечинскиот JSON не смее да стане доверлива состојба на работниот простор само затоа што успешно се декодирал. DTO го бара секој дел од договорот, отфрла погрешни типови и ги зачувува низите засновани на докази без да ја нагаѓа нивната внатрешна форма.
<?php
// src/BrandKit.php
namespace Workspace;
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,
) {}
public static function fromApi(array $data): self
{
if (!isset($data['brand_name'])
|| !is_string($data['brand_name'])
|| trim($data['brand_name']) === '') {
throw new ApiException('invalid_response: brand_name');
}
$arrayFields = [
'logos', 'colors', 'fonts', 'imagery',
'social_profiles', 'css_variables',
];
foreach ($arrayFields as $field) {
if (!array_key_exists($field, $data) || !is_array($data[$field])) {
throw new ApiException('invalid_response: ' . $field);
}
}
return new self(
trim($data['brand_name']),
$data['logos'],
$data['colors'],
$data['fonts'],
$data['imagery'],
$data['social_profiles'],
$data['css_variables'],
);
}
public function toArray(): array
{
return [
'brand_name' => $this->brandName,
'logos' => $this->logos,
'colors' => $this->colors,
'fonts' => $this->fonts,
'imagery' => $this->imagery,
'social_profiles' => $this->socialProfiles,
'css_variables' => $this->cssVariables,
];
}
}
Клиентот прави најмногу три обиди. Тој повторува при мрежни неуспеси, 429 и серверски неуспеси со ограничено одложување. Автентикацијата, другите грешки на клиентот, неправилниот JSON и неуспесите во шемата не се повторуваат бидејќи повторувањето не може да ги поправи.
<?php
// src/BrandKitClient.php
namespace Workspace;
final class BrandKitClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit';
private \Closure $sleep;
private \Closure $log;
public function __construct(
private Transport $transport,
private string $token,
private float $connectTimeout = 3.0,
private float $responseTimeout = 20.0,
?\Closure $sleep = null,
?\Closure $log = null,
) {
if (trim($token) === '') {
throw new \InvalidArgumentException('Missing BRAND_KIT_TOKEN');
}
$this->sleep = $sleep
?? static fn(int $milliseconds) => usleep($milliseconds * 1000);
$this->log = $log ?? static function (array $event): void {};
}
public function extract(string $url): BrandKit
{
$payload = json_encode(
['url' => $url],
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
);
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->transport->post(
self::ENDPOINT,
[
'Authorization: Bearer ' . $this->token,
'Accept: application/json',
'Content-Type: application/json',
],
$payload,
$this->connectTimeout,
$this->responseTimeout,
);
} catch (TransportException $exception) {
if ($attempt === 3) {
throw new ApiException(
'network_failure',
0,
$exception
);
}
$this->backoff($attempt, 'network');
continue;
}
if ($response->status === 401 || $response->status === 403) {
throw new ApiException('authentication_failed');
}
if ($response->status === 429
|| $response->status >= 500) {
if ($attempt === 3) {
throw new ApiException(
$response->status === 429
? 'quota_or_rate_limit'
: 'upstream_unavailable'
);
}
$this->backoff($attempt, (string) $response->status);
continue;
}
if ($response->status < 200 || $response->status >= 300) {
throw new ApiException(
'request_rejected:' . $response->status
);
}
try {
$decoded = json_decode(
$response->body,
true,
512,
JSON_THROW_ON_ERROR
);
} catch (\JsonException $exception) {
throw new ApiException('invalid_json', 0, $exception);
}
if (!is_array($decoded)) {
throw new ApiException('invalid_response');
}
return BrandKit::fromApi($decoded);
}
throw new ApiException('unreachable_failure');
}
private function backoff(int $attempt, string $reason): void
{
$delay = min(1000, 250 * (2 ** ($attempt - 1)));
($this->log)([
'event' => 'brand_kit_retry',
'attempt' => $attempt,
'reason' => $reason,
'delay_ms' => $delay,
]);
($this->sleep)($delay);
}
}
Однапред пополнете го работниот простор атомски
Провизионерот прифаќа само HTTPS URL-адреси со нормално име на домаќин и отфрла IP литерали и localhost. За производ со отворена регистрација, додајте и тек за одобрени домени или посилни DNS контроли; само валидацијата на URL синтаксата не може да го спречи секое сценарио со приватна мрежа или повторно врзување на DNS. Испраќајте само јавни веб-страници што клиентот има право да ги обработува.
<?php
// src/WorkspaceProvisioner.php
namespace Workspace;
final class WorkspaceProvisioner
{
public function __construct(
private BrandKitClient $client,
private string $storageDirectory,
) {}
public function provision(string $workspaceId, string $siteUrl): BrandKit
{
if (!preg_match('/\A[a-zA-Z0-9_-]{1,64}\z/', $workspaceId)) {
throw new \InvalidArgumentException('Invalid workspace ID');
}
$parts = parse_url($siteUrl);
$host = is_array($parts) ? ($parts['host'] ?? '') : '';
if (filter_var($siteUrl, FILTER_VALIDATE_URL) === false
|| ($parts['scheme'] ?? '') !== 'https'
|| $host === ''
|| strtolower($host) === 'localhost'
|| filter_var($host, FILTER_VALIDATE_IP) !== false) {
throw new \InvalidArgumentException('Use a public HTTPS URL');
}
$kit = $this->client->extract($siteUrl);
$target = $this->storageDirectory . '/' . $workspaceId . '.json';
$temporary = tempnam($this->storageDirectory, 'brand-');
if ($temporary === false) {
throw new \RuntimeException('Could not create temporary file');
}
try {
$json = json_encode([
'workspace_id' => $workspaceId,
'source_url' => $siteUrl,
'brand_kit' => $kit->toArray(),
], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
if (file_put_contents($temporary, $json, LOCK_EX) === false
|| !rename($temporary, $target)) {
throw new \RuntimeException('Could not store workspace');
}
} finally {
if (is_file($temporary)) {
unlink($temporary);
}
}
return $kit;
}
}
Јавниот контролер мора да биде зад автентикацијата, авторизацијата и CSRF заштитата на вашата апликација. Тој враќа стабилни кодови на неуспех, додека евидентира оперативен контекст без акредитиви, тела на одговори или извлечената содржина на брендот.
<?php
// public/create-workspace.php
declare(strict_types=1);
use Workspace\{
ApiException, BrandKitClient, CurlTransport, WorkspaceProvisioner
};
require dirname(__DIR__) . '/vendor/autoload.php';
header('Content-Type: application/json');
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
throw new RuntimeException('method_not_allowed');
}
$input = json_decode(
file_get_contents('php://input'),
true,
512,
JSON_THROW_ON_ERROR
);
$logger = static function (array $event): void {
error_log(json_encode($event, JSON_THROW_ON_ERROR));
};
$client = new BrandKitClient(
new CurlTransport(),
getenv('BRAND_KIT_TOKEN') ?: '',
(float) (getenv('BRAND_KIT_CONNECT_TIMEOUT') ?: 3),
(float) (getenv('BRAND_KIT_RESPONSE_TIMEOUT') ?: 20),
null,
$logger,
);
$provisioner = new WorkspaceProvisioner(
$client,
getenv('WORKSPACE_STORAGE') ?: ''
);
$kit = $provisioner->provision(
(string) ($input['workspace_id'] ?? ''),
(string) ($input['url'] ?? ''),
);
http_response_code(201);
echo json_encode([
'status' => 'ready',
'brand_kit' => $kit->toArray(),
], JSON_THROW_ON_ERROR);
} catch (InvalidArgumentException | JsonException $exception) {
http_response_code(422);
echo json_encode(['status' => 'invalid_input']);
} catch (ApiException $exception) {
error_log(json_encode([
'event' => 'brand_kit_failure',
'code' => $exception->getMessage(),
]));
http_response_code(503);
echo json_encode([
'status' => 'extraction_failed',
'code' => $exception->getMessage(),
]);
} catch (Throwable $exception) {
error_log(json_encode(['event' => 'workspace_failure']));
http_response_code(500);
echo json_encode(['status' => 'internal_error']);
}
Тестирајте повторни обиди и мапирање без мрежни повици
Лажен транспорт ги прави низите на неуспеси точни и брзи. Овој тест докажува дека првиот обид ограничен по стапка се повторува и дека валидираниот резултат е правилно мапиран.
<?php
// tests/BrandKitClientTest.php
namespace Workspace\Tests;
use PHPUnit\Framework\TestCase;
use Workspace\{
BrandKitClient, HttpResponse, Transport
};
final class FakeTransport implements Transport
{
public int $calls = 0;
public function __construct(private array $responses) {}
public function post(
string $url,
array $headers,
string $body,
float $connectTimeout,
float $responseTimeout,
): HttpResponse {
return $this->responses[$this->calls++];
}
}
final class BrandKitClientTest extends TestCase
{
public function testRetriesRateLimitAndMapsBrandKit(): void
{
$valid = json_encode([
'brand_name' => 'Example',
'logos' => [['url' => 'https://example.com/logo.svg']],
'colors' => [['value' => '#112233']],
'fonts' => [['family' => 'Example Sans']],
'imagery' => [],
'social_profiles' => [],
'css_variables' => ['--brand-primary' => '#112233'],
], JSON_THROW_ON_ERROR);
$transport = new FakeTransport([
new HttpResponse(429, [], '{}'),
new HttpResponse(200, [], $valid),
]);
$client = new BrandKitClient(
$transport,
'test-token',
sleep: static function (int $milliseconds): void {}
);
$kit = $client->extract('https://example.com');
self::assertSame(2, $transport->calls);
self::assertSame('Example', $kit->brandName);
self::assertSame('#112233', $kit->colors[0]['value']);
}
}
composer test
curl --request POST \
--url http://127.0.0.1:8080/create-workspace.php \
--header "Content-Type: application/json" \
--data '{"workspace_id":"client_acme","url":"https://example.com"}'
Управувајте со интеграцијата во продукција
Создадете го директориумот за складирање при распоредувањето и доделете пристап за запишување само на идентитетот на PHP процесот. Во апликација поддржана од база на податоци, заменете го JSON складиштето со трансакција што го ажурира новиот работен простор само по успешно завршена валидација. Задржете ги API клиентот и DTO непроменети.
Следете ја латентноста на извлекувањето, бројот на успешни извлекувања, бројот на повторни обиди, кодовите на неуспех и староста на работните простори заглавени во состојба на чекање. Никогаш не евидентирајте го заглавието Authorization, токенот, суровиот одговор или целосната URL-адреса на клиентот кога нејзината патека може да содржи чувствителни податоци. Нормализирано име на домаќин и внатрешен идентификатор за корелација обично се доволни за дијагностика.
Вообичаените неуспеси имаат различни решенија: authentication_failed значи проверка на активацијата и ротацијата на токенот; quota_or_rate_limit значи почитување на капацитетот на планот или одложување на работата; request_rejected упатува на проблеми со влезот или договорот; invalid_json и invalid_response укажуваат на несовпаѓање со договорот нагоре по синџирот што треба да ги предупреди операторите, наместо да ги загади зачуваните работни простори.
Конечна листа за потврда
- Токенот доаѓа од конфигурација поддржана од променливи на околината и никогаш не се појавува во контрола на изворниот код или дневници.
- Апликацијата испраќа JSON со
urlдо точната POST крајна точка со Bearer автентикација. - Само URL-адреси на јавни HTTPS веб-страници стигнуваат до клиентот за извлекување.
- Името на брендот, логоата, боите, фонтовите, сликите, социјалните профили и CSS променливите се валидираат пред складирањето.
- Мрежните, ограничените по стапка и серверските неуспеси добиваат ограничени повторни обиди; неуспесите во автентикацијата и валидацијата не добиваат.
- Запишувањата на работниот простор се атомски, одредиштето е достапно за запишување, а неуспесите враќаат структурирани состојби.
- PHPUnit тестовите поминуваат со детерминистички лажен транспорт и без зависност од активна услуга.
- Вистинско барање за воведување создава подготвен работен простор чие лого, палета и фонтови веќе му се достапни на клиентот.
Видливиот резултат е пријатно едноставен: клиентот доставува веб-страница и отвора работен простор што веќе наликува на неговиот бренд. Инженерството под тоа треба да биде подеднакво дисциплинирано. Тесна API граница, одбранбено мапирање, намерни повторни обиди, безбедно складирање и воочливи неуспеси претвораат еден пригоден повик за извлекување во сигурна функционалност за воведување.