Izvorni PHP 8.3: automatsko popunjavanje novih radnih prostora klijenata pomoću API-ja Brand Kit Extractor
Novi klijentski radni prostor trebao bi se činiti spremnim pri prvom prijavljivanju, a ne kao prazan obrazac koji traži od nekoga da transkribira vlastitu web stranicu. Praktičan tijek uvođenja može prihvatiti javni URL klijenta, izdvojiti njegov vizualni identitet, potvrditi rezultat i prednapunjati radni prostor upotrebljivim logotipima, bojama i fontovima.
Ovaj vodič gradi taj tijek u izvornom PHP 8.3 koristeći cURL, strogu granicu domene, ograničene pokušaje ponavljanja, atomsko pohranjivanje, strukturirane pogreške i determinističke PHPUnit testove. API za izdvajanje kompleta robne marke ostaje izoliran iza jednog klijenta, tako da kontroleri i kod za pohranu nikada ne ovise o transportnim detaljima ili neprovjerenom udaljenom JSON-u.
Dobijte pristup i kopirajte token opsega usluge
Započnite stvaranjem računa na https://ai.mihajlo.mk/register. Ako ga već imate, prijavite se na https://ai.mihajlo.mk/login.
- Otvorite stranicu za izdvajanje kompleta robne marke.
- Odaberite dostupni besplatni, Plus ili Pro plan koji odgovara vašoj očekivanoj upotrebi, a zatim dovršite aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Servisni token i kopirajte token opsega usluge.
- Pohranite ga u konfiguraciju projekta koja se temelji na okruženju, nikada u PHP izvorni kod.
Ova usluga nije bez tokena: svaki zahtjev za izdvajanje mora se autentificirati pomoću Bearer tokena, X-API-Token zaglavlja ili token parametra upita. Mi ćemo koristiti oblik Bearer jer čuva vjerodajnice izvan URL-ova i redovitih zapisnika pristupa.
Ponovno generiranje servisnog tokena opoziva prethodno aktivni token. Tretirajte ponovno generiranje kao rotaciju vjerodajnica: ažurirajte tajnu implementacije, ponovno pokrenite ili ponovno implementirajte sve procese koji je čitaju, provjerite zahtjev novim tokenom, a tek onda smatrajte implementaciju dovršenom.
Potvrdite HTTP ugovor prije pisanja značajke
Točna operacija je POST https://ai.mihajlo.mk/api/brand-kit-extractor/v1/extract-brand-kit. Njegovo JSON tijelo zahtjeva sadrži url. Pokrenite jedan minimalni zahtjev s javnom web-stranicom koju ste ovlašteni obraditi:
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"}'
Nemojte ovo prosljeđivati kroz verbose otklanjanje pogrešaka u dijeljenim terminalima ili CI jer se argumenti naredbe i zaglavlja mogu snimiti. Stvarna aplikacija će poslati istu metodu, krajnju točku, zaglavlja i tijelo dok zadržava token u svom okruženju.
Organizirajte izvorni PHP projekt
Dizajn je namjerno mali. Transport rukuje cURL-om, API klijent upravlja ponovnim pokušajima i provjerom valjanosti odgovora, pružatelj primjenjuje pravila domene, a kontroler prevodi pogreške aplikacije u HTTP odgovore. Izdvajanje ostaje sinkrono jer stvaranje radnog prostora zahtijeva rezultat odmah; ako latencija uvođenja kasnije postane neprihvatljiva, pružatelj je šav za premještanje iza pokretača poslova.
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
Zahtijeva PHP 8.3 i cURL, konfigurira PSR-4 automatsko učitavanje i instalira PHPUnit 11 za razvoj:
{
"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
# Uredite .env lokalno, a zatim ga izvezite prije pokretanja PHP-a:
set -a
. ./.env
set +a
php -S 127.0.0.1:8080 -t public
Izvorni PHP ne učitava .env automatski. Gornje naredbe ljuske prikladne su za lokalni razvoj; produkcija bi trebala ubrizgavati varijable putem svog upravitelja procesa, pokretača spremnika ili pohrane tajnih podataka. Isključite .env iz kontrole verzija.
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
Izgradite uski, zamjenjivi cURL transport
Transport vraća status, zaglavlja i tijelo bez tumačenja podataka o robnoj marki. Ovo razdvajanje daje testovima determinističku lažnu verziju i drži ponašanje specifično za cURL izvan sloja domene.
<?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,
);
}
}
Provjerite udaljene podatke na granici aplikacije
Usluga vraća naziv robne marke plus logotipove, boje, fontove, slike, društvene profile i CSS varijable. Udaljeni JSON ne smije postati pouzdano stanje radnog prostora samo zato što je uspješno dekodiran. DTO zahtijeva svaki odjeljak ugovora, odbacuje pogrešne tipove i zadržava nizove temeljene na dokazima bez nagađanja o njihovom unutarnjem obliku.
<?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,
];
}
}
Klijent vrši najviše tri pokušaja. Ponovno pokušava mrežne greške, 429 i greške poslužitelja s ograničenim kašnjenjem. Autentifikacija, druge klijentske greške, neispravan JSON i greške sheme se ne pokušavaju ponovno jer ih ponavljanje ne može popraviti.
<?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);
}
}
Atomski prednapuni radni prostor
Pružatelj prihvaća samo HTTPS URL-ove s normalnim nazivom hosta i odbija IP literale i localhost. Za proizvod s otvorenom prijavom, također dodajte tijek odobrenih domena ili jače kontrole DNS-a; samo provjera valjanosti sintakse URL-a ne može spriječiti svaki scenarij privatne mreže ili DNS ponovnog vezivanja. Šaljite samo javne web-stranice koje klijent ima pravo obraditi.
<?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;
}
}
Javni kontroler mora biti iza autentifikacije, autorizacije i zaštite od CSRF-a vaše aplikacije. Vraća stabilne kodove pogrešaka dok bilježi operativni kontekst bez vjerodajnica, tijela odgovora ili izdvojenog sadržaja robne marke.
<?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']);
}
Testirajte ponovne pokušaje i mapiranje bez mrežnih poziva
Lažni transport čini sekvence pogrešaka točnim i brzim. Ovaj test dokazuje da se ponovno pokušava s ograničenjem brzine prvog pokušaja i da je potvrđeni rezultat ispravno mapiran.
<?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"}'
Upravljajte integracijom u produkciji
Stvorite direktorij za pohranu tijekom implementacije i dodijelite pravo pisanja samo identitetu PHP procesa. U aplikaciji utemeljenoj na bazi podataka, zamijenite JSON spremište transakcijom koja ažurira novi radni prostor tek nakon što provjera valjanosti uspije. Ostavite API klijenta i DTO nepromijenjene.
Pratite latenciju izdvajanja, brojače uspjeha, brojače ponovnih pokušaja, kodove pogrešaka i starost radnih prostora zaglavljenih u stanju čekanja. Nikada ne bilježite Authorization zaglavlje, token, sirovi odgovor ili potpuni URL kupca kada njegov put može sadržavati osjetljive podatke. Normalizirani naziv hosta i interni ID korelacije obično su dovoljni za dijagnozu.
Uobičajene pogreške imaju različite lijekove: authentication_failed znači provjeru aktivacije i rotacije tokena; quota_or_rate_limit znači poštivanje kapaciteta plana ili odgodu posla; request_rejected ukazuje na probleme s unosom ili ugovorom; invalid_json i invalid_response ukazuju na nesklad ugovora s gornje strane koji bi trebao upozoriti operatore umjesto da zagađuje pohranjene radne prostore.
Završni popis provjere
- Token dolazi iz konfiguracije utemeljene na okruženju i nikada se ne pojavljuje u kontroli verzija ili zapisnicima.
- Aplikacija šalje JSON s
urlna točnu POST krajnju točku koristeći Bearer autentifikaciju. - Samo javni HTTPS URL-ovi web-mjesta dolaze do klijenta za izdvajanje.
- Naziv robne marke, logotipovi, boje, fontovi, slike, društveni profili i CSS varijable provjeravaju se prije pohranjivanja.
- Mrežne greške, greške ograničenja brzine i greške poslužitelja primaju ograničene pokušaje ponavljanja; greške autentifikacije i provjere valjanosti ne primaju.
- Pisanja radnog prostora su atomska, odredište je moguće pisati, a pogreške vraćaju strukturirana stanja.
- PHPUnit testovi prolaze s determinističkim lažnim transportom i bez stvarne ovisnosti o usluzi.
- Stvarni zahtjev za uvođenje stvara gotov radni prostor čiji su logotip, paleta i fontovi već dostupni klijentu.
Vidljivi rezultat je ugodno jednostavan: klijent isporučuje web-mjesto i otvara radni prostor koji već nalikuje njihovoj robnoj marki. Inženjerstvo ispod trebalo bi biti jednako disciplinirano. Uski API granica, obrambeno mapiranje, namjerni pokušaji ponavljanja, sigurno pohranjivanje i uočljive pogreške pretvaraju prikladan poziv za izdvajanje u pouzdanu značajku uvođenja.