Izvorni PHP 8.3: Generirajte objedinjene kartice profila autora iz poveznica na društvenim mrežama
Upravitelj kontakata kreatora brzo postaje neuredan kada jedna osoba stiže kao Instagram URL, druga kao LinkedIn identifikator, a treća kao Facebook korisničko ime. Izravno uspoređivanje tih nizova stvara duplicirane kontakte, nedosljedne prikaze i krhak kod specifičan za pojedinu platformu.
Bolja granica je normalizirani objekt identiteta. U ovom vodiču izradit ćemo izvornu PHP 8.3 aplikaciju koja javne društvene reference šalje alatu Identity Resolver, defenzivno validira njegov odgovor i preslikava rezultat u stabilnu shemu kartice profila. Implementacija uključuje ograničena vremenska ograničenja, selektivna ponavljanja, determinističke testove, strukturirane neuspjehe, sigurno zapisivanje u logove i praktične smjernice za implementaciju.
Pribavite pristup prije pisanja integracijskog koda
Započnite sa službenom dokumentacijom za Identity Resolver. Trenutačni javni krajnji punkt ne zahtijeva token računa ni API ključ, stoga prije prvog zahtjeva nema obvezne registracije, prijave, aktivacije plana ni koraka kopiranja vjerodajnica.
- Pregledajte podržane ulaze u službenoj dokumentaciji.
- Provjerite stranicu usluge i planova za trenutačni status usluge i informacije o planovima.
- Za ovaj javni krajnji punkt trenutačno nisu potrebni korak registracije ni korak prijave. Ako se model uvođenja promijeni, slijedite poveznice koje objavljuje službena dokumentacija umjesto da nagađate URL-ove portala.
- Nema tokena za kopiranje. Nemojte izmišljati API ključ, dodavati prazno bearer zaglavlje ni pohranjivati zamjensku vjerodajnicu koju usluga ne zahtijeva.
Točan zahtjev je GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. Pošaljite platform zajedno s jednim podržanim parametrom username, id, identifier, profile ili url.
Izradite minimalni testni zahtjev prije izrade aplikacije:
curl --get \
'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve' \
--data-urlencode 'platform=instagram' \
--data-urlencode 'url=https://www.instagram.com/example/' \
--header 'Accept: application/json'
Zamijenite primjer URL-a podržanom javnom referencom koju smijete obrađivati. Uspješan odgovor trebao bi biti JSON koji sadrži normalizirani javni objekt identiteta. Namjerno ćemo izbjeći pretpostavljanje nedokumentiranih naziva polja.
Iako nema vjerodajnice, operativnu konfiguraciju držite izvan kontrole izvornog koda. Izradite lokalnu datoteku .env i isključite je iz Gita:
IDENTITY_RESOLVER_URL=https://ai.mihajlo.mk/api/identity-resolver/v1/resolve
IDENTITY_CONNECT_TIMEOUT_MS=1500
IDENTITY_RESPONSE_TIMEOUT_MS=5000
set -a
. ./.env
set +a
U produkciji ubrizgajte ove varijable putem upravitelja procesa ili hosting platforme. Ako se autentikacija uvede kasnije, dodajte dokumentiranu vjerodajnicu kao tajnu varijablu okruženja tek nakon potvrde novog ugovora.
Arhitektura i kompromisi
Aplikacija ima četiri granice: HTTP prijenos, klijent za Identity Resolver, domenski mapper i mali HTTP kontroler. Kontroler nikada ne tumači odgovore specifične za pružatelja. Prijenos poznaje cURL, ali ništa o kreatorima. Resolver upravlja ponavljanjima i validacijom odgovora. Mapper prihvaćeni objekt identiteta pretvara u verzionirani oblik kartice.
Razrješavanje ostaje sinkrono jer stvaranje jednog kontakta zahtijeva jedno malo traženje, a korisniku je potrebna trenutačna povratna informacija. Red bi dodao operativnu infrastrukturu bez poboljšanja ovog uobičajenog tijeka rada upravitelja kontakata. Ako se kasnije dodaju skupni uvozi, isti resolver može raditi iza workera.
Koristite PHP 8.3 s proširenjima cURL i JSON, Composer i PHPUnit 11 za testove:
mkdir creator-cards
cd creator-cards
composer init --name=app/creator-cards --require=php:^8.3 --no-interaction
composer require --dev phpunit/phpunit:^11.0
mkdir -p public src/Http src/Identity tests
composer config autoload.psr-4 'App\\' src/
composer config autoload-dev.psr-4 'Tests\\' tests/
composer dump-autoload
Rezultirajući projekt namjerno je malen:
creator-cards/
public/index.php
src/Http/Transport.php
src/Http/CurlTransport.php
src/Identity/IdentityResolver.php
src/Identity/ProfileCardMapper.php
tests/IdentityResolverTest.php
composer.json
.env
Izradite izolirani izvorni cURL prijenos
Sučelje čini vanjsku granicu zamjenjivom u testovima. Produkcijska implementacija bilježi zaglavlja odgovora jer Retry-After može utjecati na upravljanje ograničenjem broja zahtjeva.
<?php
// src/Http/Transport.php
namespace App\Http;
final readonly class HttpResponse
{
public function __construct(
public int $status,
public string $body,
public array $headers
) {}
}
interface Transport
{
public function get(
string $url,
array $query,
int $connectTimeoutMs,
int $responseTimeoutMs
): HttpResponse;
}
final class TransportException extends \RuntimeException {}
<?php
// src/Http/CurlTransport.php
namespace App\Http;
final class CurlTransport implements Transport
{
public function get(
string $url,
array $query,
int $connectTimeoutMs,
int $responseTimeoutMs
): HttpResponse {
$headers = [];
$handle = curl_init($url . '?' . http_build_query(
$query,
'',
'&',
PHP_QUERY_RFC3986
));
if ($handle === false) {
throw new TransportException('Unable to initialize cURL');
}
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => $connectTimeoutMs,
CURLOPT_TIMEOUT_MS => $responseTimeoutMs,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'User-Agent: CreatorCards/1.0',
],
CURLOPT_HEADERFUNCTION => static function (
\CurlHandle $handle,
string $line
) use (&$headers): int {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return strlen($line);
},
]);
$body = curl_exec($handle);
if ($body === false) {
$message = curl_error($handle);
$number = curl_errno($handle);
throw new TransportException(
"cURL failure {$number}: {$message}"
);
}
return new HttpResponse(
curl_getinfo($handle, CURLINFO_RESPONSE_CODE),
$body,
$headers
);
}
}
Vremensko ograničenje povezivanja ograničava DNS i uspostavljanje veze. Ukupno vremensko ograničenje ograničava cjelokupni zahtjev. Nijedno ne bi trebalo ostaviti na neograničenoj zadanoj vrijednosti u PHP procesu okrenutom korisnicima.
Razriješite identitete uz kontrolirana ponavljanja
Resolver prihvaća samo dokumentirane platforme i nazive parametara reference. Ponovno pokušava nakon neuspjeha prijenosa, HTTP 429 i odgovora poslužitelja 5xx. Neuspjesi validacije i ostali odgovori 4xx odmah ne uspijevaju jer ponavljanje istog zahtjeva neće ih popraviti.
<?php
// src/Identity/IdentityResolver.php
namespace App\Identity;
use App\Http\Transport;
use App\Http\TransportException;
final class ResolverFailure extends \RuntimeException
{
public function __construct(
public readonly string $kind,
string $message,
public readonly ?int $status = null
) {
parent::__construct($message);
}
}
final class IdentityResolver
{
private const PLATFORMS = ['facebook', 'instagram', 'linkedin'];
private const REFERENCES = [
'username', 'id', 'identifier', 'profile', 'url'
];
public function __construct(
private Transport $transport,
private string $endpoint,
private int $connectTimeoutMs,
private int $responseTimeoutMs,
private \Closure $sleep,
private \Closure $log
) {}
public function resolve(
string $platform,
string $referenceType,
string $reference
): array {
$platform = strtolower(trim($platform));
$referenceType = strtolower(trim($referenceType));
$reference = trim($reference);
if (!in_array($platform, self::PLATFORMS, true) ||
!in_array($referenceType, self::REFERENCES, true) ||
$reference === '') {
throw new ResolverFailure(
'invalid_input',
'Unsupported platform or reference'
);
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->transport->get(
$this->endpoint,
['platform' => $platform, $referenceType => $reference],
$this->connectTimeoutMs,
$this->responseTimeoutMs
);
} catch (TransportException $exception) {
($this->log)([
'event' => 'identity_resolver_transport_failure',
'platform' => $platform,
'attempt' => $attempt,
]);
if ($attempt === 3) {
throw new ResolverFailure(
'unavailable',
'Identity service is unavailable'
);
}
($this->sleep)(200 * (2 ** ($attempt - 1)));
continue;
}
if ($response->status >= 200 && $response->status < 300) {
try {
$identity = json_decode(
$response->body,
true,
512,
JSON_THROW_ON_ERROR
);
} catch (\JsonException) {
throw new ResolverFailure(
'invalid_response',
'Identity service returned invalid JSON',
$response->status
);
}
if (!is_array($identity) || array_is_list($identity)) {
throw new ResolverFailure(
'invalid_response',
'Expected a normalized identity object',
$response->status
);
}
return $identity;
}
$retryable = $response->status === 429 ||
$response->status >= 500;
if (!$retryable || $attempt === 3) {
$kind = $response->status === 429
? 'rate_limited'
: ($response->status >= 500
? 'unavailable'
: 'rejected');
throw new ResolverFailure(
$kind,
'Identity resolution failed',
$response->status
);
}
$delay = 200 * (2 ** ($attempt - 1));
$retryAfter = $response->headers['retry-after'] ?? null;
if (is_string($retryAfter) && ctype_digit($retryAfter)) {
$delay = min(2000, ((int) $retryAfter) * 1000);
}
($this->log)([
'event' => 'identity_resolver_retry',
'platform' => $platform,
'status' => $response->status,
'attempt' => $attempt,
'delay_ms' => $delay,
]);
($this->sleep)($delay);
}
throw new ResolverFailure('unavailable', 'Resolution failed');
}
}
Ograničenje ponavljanja štiti PHP worker i nadređenu uslugu. Poštuju se samo cjelobrojne vrijednosti Retry-After, a odgoda je ograničena na dvije sekunde. Sama referenca namjerno je izostavljena iz logova.
Preslikajte odgovor u stabilnu karticu profila
Navedeni ugovor obećava normalizirani javni objekt identiteta, ali ne opravdava ovdje čvrsto kodiranje pojedinih polja odgovora. Sačuvajte taj objekt, pridružite lokalno poznate metapodatke i izvedite stabilan otisak iz kanonskog JSON-a. To održava pohranu kartica dosljednom bez pretvaranja da nedokumentirano polje name ili avatar mora postojati.
<?php
// src/Identity/ProfileCardMapper.php
namespace App\Identity;
final class ProfileCardMapper
{
public function map(
string $platform,
string $referenceType,
string $reference,
array $identity,
\DateTimeImmutable $resolvedAt
): array {
$canonical = $this->sortRecursively($identity);
return [
'schema' => 'creator-profile-card/v1',
'id' => hash(
'sha256',
$platform . "\n" .
json_encode($canonical, JSON_THROW_ON_ERROR)
),
'platform' => $platform,
'source' => [
'type' => $referenceType,
'value' => $reference,
],
'identity' => $canonical,
'resolved_at' => $resolvedAt->format(DATE_ATOM),
];
}
private function sortRecursively(array $value): array
{
if (!array_is_list($value)) {
ksort($value);
}
foreach ($value as $key => $item) {
if (is_array($item)) {
$value[$key] = $this->sortRecursively($item);
}
}
return $value;
}
}
Hash je otisak aplikacije, a ne tvrdnja da usluga izlaže određeni identifikator. Pohranite cijelu karticu kao JSON ili je preslikajte u svoju bazu kontakata. Pri prikazivanju vrijednosti identiteta u HTML-u uvijek ih escapajte pomoću htmlspecialchars; javni podaci i dalje su nepouzdan ulaz.
Izložite krajnji punkt upravitelja kontakata
Kontroler prihvaća neutralni oblik ulaza i prevodi ga u točan upit koji resolver očekuje.
<?php
// public/index.php
require dirname(__DIR__) . '/vendor/autoload.php';
use App\Http\CurlTransport;
use App\Identity\IdentityResolver;
use App\Identity\ProfileCardMapper;
use App\Identity\ResolverFailure;
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST' ||
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) !== '/api/profile-cards') {
http_response_code(404);
echo json_encode(['error' => 'not_found']);
exit;
}
try {
$input = json_decode(
file_get_contents('php://input'),
true,
32,
JSON_THROW_ON_ERROR
);
if (!is_array($input)) {
throw new \InvalidArgumentException();
}
$resolver = new IdentityResolver(
new CurlTransport(),
getenv('IDENTITY_RESOLVER_URL') ?: '',
(int) (getenv('IDENTITY_CONNECT_TIMEOUT_MS') ?: 1500),
(int) (getenv('IDENTITY_RESPONSE_TIMEOUT_MS') ?: 5000),
static fn (int $milliseconds) =>
usleep($milliseconds * 1000),
static fn (array $event) =>
error_log(json_encode($event, JSON_THROW_ON_ERROR))
);
$platform = (string) ($input['platform'] ?? '');
$type = (string) ($input['reference_type'] ?? '');
$reference = (string) ($input['reference'] ?? '');
$identity = $resolver->resolve($platform, $type, $reference);
$card = (new ProfileCardMapper())->map(
strtolower(trim($platform)),
strtolower(trim($type)),
trim($reference),
$identity,
new \DateTimeImmutable('now', new \DateTimeZone('UTC'))
);
http_response_code(201);
echo json_encode(['card' => $card], JSON_THROW_ON_ERROR);
} catch (ResolverFailure $failure) {
$status = match ($failure->kind) {
'invalid_input' => 422,
'rate_limited' => 429,
'rejected' => 422,
default => 503,
};
http_response_code($status);
echo json_encode([
'error' => $failure->kind,
'message' => $failure->getMessage(),
], JSON_THROW_ON_ERROR);
} catch (\JsonException | \InvalidArgumentException) {
http_response_code(400);
echo json_encode(['error' => 'invalid_json']);
}
Produkcijske aplikacije također bi trebale ograničiti veličinu tijela zahtjeva na web-poslužitelju i nametnuti razumne duljine referenci prije upućivanja odlaznog poziva.
Testirajte ponavljanja bez pozivanja usluge
Lažni prijenos čini putanje neuspjeha brzima i determinističkima. Nisu potrebni mreža, račun ni vjerodajnica fixturea.
<?php
// tests/IdentityResolverTest.php
namespace Tests;
use App\Http\HttpResponse;
use App\Http\Transport;
use App\Identity\IdentityResolver;
use App\Identity\ResolverFailure;
use PHPUnit\Framework\TestCase;
final class FakeTransport implements Transport
{
public array $calls = [];
public function __construct(private array $responses) {}
public function get(
string $url,
array $query,
int $connectTimeoutMs,
int $responseTimeoutMs
): HttpResponse {
$this->calls[] = compact('url', 'query');
return array_shift($this->responses);
}
}
final class IdentityResolverTest extends TestCase
{
public function testReturnsNormalizedObject(): void
{
$fake = new FakeTransport([
new HttpResponse(200, '{"public":"identity"}', []),
]);
$resolver = $this->resolver($fake);
$result = $resolver->resolve(
'instagram',
'username',
'example'
);
self::assertSame(['public' => 'identity'], $result);
self::assertSame(
['platform' => 'instagram', 'username' => 'example'],
$fake->calls[0]['query']
);
}
public function testRetriesRateLimitThenSucceeds(): void
{
$fake = new FakeTransport([
new HttpResponse(429, '{}', ['retry-after' => '1']),
new HttpResponse(200, '{"resolved":true}', []),
]);
$delays = [];
$resolver = $this->resolver($fake, $delays);
self::assertSame(
['resolved' => true],
$resolver->resolve('linkedin', 'id', '123')
);
self::assertCount(2, $fake->calls);
self::assertSame([1000], $delays);
}
public function testDoesNotRetryRejectedInput(): void
{
$fake = new FakeTransport([
new HttpResponse(400, '{}', []),
]);
$this->expectException(ResolverFailure::class);
try {
$this->resolver($fake)->resolve(
'facebook',
'profile',
'bad-reference'
);
} finally {
self::assertCount(1, $fake->calls);
}
}
private function resolver(
FakeTransport $fake,
array &$delays = []
): IdentityResolver {
return new IdentityResolver(
$fake,
'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve',
1500,
5000,
static function (int $ms) use (&$delays): void {
$delays[] = $ms;
},
static function (array $event): void {}
);
}
}
vendor/bin/phpunit --testdox tests
php -S 127.0.0.1:8080 -t public
curl --request POST 'http://127.0.0.1:8080/api/profile-cards' \
--header 'Content-Type: application/json' \
--data '{"platform":"instagram","reference_type":"url","reference":"https://www.instagram.com/example/"}'
Sigurnost, vidljivost i implementacija
Prihvaćajte samo dokumentirane popise dopuštenih platformi i parametara. Budući da je odredišni URL fiksan u konfiguraciji okruženja, pozivatelji ne mogu aplikaciju pretvoriti u proizvoljni HTTP proxy. Nemojte dopustiti da podaci zahtjeva nadjačaju naziv hosta resolvera.
Zapisujte nazive događaja, platformu, status, pokušaj, odgodu, trajanje i ID korelacije zahtjeva ako ga vaša aplikacija već ima. Izbjegavajte zapisivanje poslanih korisničkih imena, URL-ova, sirovih tijela ili potpunih normaliziranih identiteta. Pratite broj uspjeha, odbijenih ulaza, ograničenja broja zahtjeva, neuspjeha nadređene usluge, neispravnih odgovora i latenciju.
Za implementaciju usmjerite korijen dokumenta web-poslužitelja na public, pokrenite PHP-FPM kao neprivilegirani korisnik, zahtijevajte HTTPS na javnom rubu i ubrizgajte tri varijable okruženja u PHP-FPM proces. Instalirajte optimizirane produkcijske ovisnosti pomoću composer install --no-dev --classmap-authoritative. Provjerite vjeruje li cURL paketu CA certifikata operacijskog sustava; nikada ne rješavajte neuspjehe certifikata isključivanjem TLS provjere.
Uobičajeni neuspjesi
- HTTP 400 ili 422: Provjerite platformu i osigurajte da je poslan točno jedan podržani parametar reference. Nemojte ponovno pokušavati s neizmijenjenim ulazom.
- HTTP 429: Poštujte ograničeni backoff, vratite strukturirano stanje ograničenja broja zahtjeva nakon proračuna ponavljanja i dopustite korisniku da pokuša kasnije.
- HTTP 401 ili 403: Nemojte dodavati nagađani token. Ponovno provjerite službenu dokumentaciju jer trenutačni javni ugovor ne zahtijeva nijedan.
- HTTP 5xx ili cURL timeout: Ponavljajte samo unutar malog proračuna, a zatim vratite
503bez izlaganja internih detalja prijenosa. - Valjan JSON s neočekivanim oblikom: Tretirajte ga kao neuspjeh ugovora nadređene usluge. Nemojte tiho stvoriti djelomičnu karticu.
- Kartice koje izgledaju duplicirano: Usporedite kanonski otisak i normalizirani identitet, a ne izvorno formatiranje poslane društvene poveznice.
Završni kontrolni popis za provjeru
- Aplikacija poziva samo
GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. - Svaki zahtjev sadrži
platformi točno jedan podržani parametar reference. - Nijedan token, API ključ ni autorizacijsko zaglavlje nisu izmišljeni.
- Vremenska ograničenja povezivanja i ukupnog odgovora su ograničena.
- Ponavljaju se samo neuspjesi prijenosa, odgovori 429 i odgovori 5xx.
- Neispravan JSON i odgovori koji nisu objekti proizvode strukturirane neuspjehe.
- Testovi koriste deterministički lažni prijenos i nikada ne pozivaju aktivnu uslugu.
- Logovi isključuju društvene reference i tijela normaliziranih identiteta.
- Kartice profila koriste istu verzioniranu shemu bez obzira na izvornu platformu.
- Produkcijski web-korijen izlaže samo direktorij
public.
Vrijedan rezultat nije samo još jedan API poziv. To je disciplinirana granica između nepredvidivih društvenih poveznica koje unose korisnici i stabilnog modela koji vaš upravitelj kontakata treba. Kada svaka Facebook, Instagram i LinkedIn referenca prođe kroz tu granicu, kartice profila postaju dosljedne, neuspjesi postaju objašnjivi, a buduće promjene ostaju izolirane ondje gdje i pripadaju.