Symfony: Pretvorite društvene poveznice u dosljedne kartice profila autora uz AI
Upravitelj kontakata kreatora često započinje varljivo jednostavnim poljem: „društvena poveznica”. Zatim pristižu podaci. Jedna osoba zalijepi Instagram URL, druga unese korisničko ime, a treća navede referencu LinkedIn profila. Usporedba, uklanjanje duplikata i prikaz tih vrijednosti brzo postaju nepouzdani.
Identity Resolver rješava problem granice. Prihvaća javne reference za Facebook, Instagram i LinkedIn te vraća normalizirani objekt javnog identiteta. U ovom ćemo vodiču izraditi Symfony aplikacijsku uslugu koja te rezultate pretvara u dosljedne kartice profila kreatora, uz defenzivno mapiranje, ograničene ponovne pokušaje, strukturirane neuspjehe, testove i zapisivanje sigurno za produkciju.
Pribavite pristup prije pisanja integracijskog koda
Započnite sa stranicom usluge Identity Resolver, a zatim pročitajte službenu dokumentaciju. Dokumentacija je također mjerodavan izvor za informacije o registraciji i informacije o prijavi.
Trenutačni javni krajnji punkt ne zahtijeva token računa ni API ključ. Stoga prije prvog zahtjeva nema registracije, prijave, odabira paketa, zaslona za kopiranje tokena ni vjerodajnice koju treba dovršiti. Nemojte izmišljati zaglavlje za autorizaciju niti u aplikaciju unositi zamjenski token. Prije implementacije ponovno provjerite dokumentaciju, u slučaju da se ugovor o pristupu kasnije promijeni.
Točna operacija je:
GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve
Pošaljite platform uz jedan podržani referentni parametar: username, id, identifier, profile ili url. Evo minimalnog zahtjeva s namjerno generičkom primjerom vrijednosti:
curl --get \
'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve' \
--data-urlencode 'platform=instagram' \
--data-urlencode 'username=example_creator'
Budući da vjerodajnica ne postoji, nakon ovog testa nema tajne koju treba pohraniti. Ipak ćemo krajnji punkt zadržati u konfiguraciji podržanoj varijablama okruženja, kako priprema, testiranje i buduće migracije API-ja ne bi zahtijevali promjene izvornog koda.
Izradite Symfony projekt
Ova implementacija cilja PHP 8.3 ili noviji te aktualnu Symfony aplikaciju s alatima FrameworkBundle, HttpClient, Monolog i PHPUnit. Izradite mali projekt usmjeren na API na sljedeći način:
composer create-project symfony/skeleton creator-contacts
cd creator-contacts
composer require symfony/http-client symfony/monolog-bundle
composer require --dev symfony/test-pack
Dodajte URI usluge u .env, pri čemu se predaje samo ova zadana vrijednost koja nije tajna:
IDENTITY_RESOLVER_URI=https://ai.mihajlo.mk/api/identity-resolver/v1/resolve
Implementacija ga može nadjačati u .env.local ili, po mogućnosti, putem konfiguracije okruženja platforme za hosting. Namjerno ne postoji IDENTITY_RESOLVER_TOKEN.
Projekt ima tri važne granice:
- Kontroler validira unos upravitelja kontakata i oblikuje odgovor aplikacije.
- Razrješivač upravlja HTTP ponašanjem, ponovnim pokušajima i dekodiranjem odgovora.
- Mapiranje domene prihvaća nesiguran vanjski objekt i proizvodi determinističku lokalnu karticu.
Razrješavanje ćemo zadržati sinkronim. Korisnik koji dodaje jednu društvenu referencu ima koristi od trenutačnog rezultata, a uvođenje Messengera stvorilo bi više operativne infrastrukture nego što je ovoj interakciji potrebno. Za skupne uvoze isti se razrješivač kasnije može pozvati iz Messenger obrađivača.
Modelirajte stabilan odgovor aplikacije
Dani ugovor obećava normalizirani odgovor javnog identiteta, ali ne propisuje polja koja ovdje možemo sigurno tvrdo kodirati. Nagađanje ključeva kao što su prikazano ime ili URL avatara povezalo bi aplikaciju s pretpostavkama umjesto s dokumentacijom.
Umjesto toga, kartica kombinira lokalne podatke o kontaktu s validiranim javnim objektom. Također izračunava deterministički otisak iz rekurzivno sortiranog JSON-a. Taj otisak pripada našoj aplikaciji; nije predstavljen kao identifikator koji izdaje usluga.
<?php
// src/Identity/ProfileCard.php
namespace App\Identity;
final readonly class ProfileCard
{
public function __construct(
public string $creatorName,
public string $platform,
public string $submittedReference,
public string $identityFingerprint,
public array $publicIdentity,
) {
}
public static function fromResolved(
string $creatorName,
string $platform,
string $reference,
array $identity,
): self {
$canonical = self::canonicalize($identity);
$json = json_encode(
$canonical,
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
return new self(
$creatorName,
strtolower($platform),
$reference,
hash('sha256', $json),
$canonical,
);
}
private static function canonicalize(array $value): array
{
if (!array_is_list($value)) {
ksort($value);
}
foreach ($value as $key => $item) {
if (is_array($item)) {
$value[$key] = self::canonicalize($item);
}
}
return $value;
}
public function toArray(): array
{
return [
'creatorName' => $this->creatorName,
'platform' => $this->platform,
'submittedReference' => $this->submittedReference,
'identityFingerprint' => $this->identityFingerprint,
'publicIdentity' => $this->publicIdentity,
];
}
}
Upravitelj kontakata s bazom podataka može ta polja zadržati u vlastitom entitetu. Zadržavanje perzistencije izvan API klijenta sprječava prelijevanje transportnih neuspjeha u kod domene i omogućuje aplikaciji da odluči trebaju li osvježeni javni podaci zamijeniti raniju snimku.
Izgradite otporni razrješivač
Izradite tipiziranu iznimku kako bi pozivatelji mogli razlikovati neispravan unos, odbijanje od strane uzvodne usluge, privremenu nedostupnost i neispravne podatke.
<?php
// src/Identity/IdentityResolverException.php
namespace App\Identity;
final class IdentityResolverException extends \RuntimeException
{
public function __construct(
public readonly string $failureType,
string $message,
public readonly bool $retryable = false,
public readonly ?int $upstreamStatus = null,
?\Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
}
HTTP usluga dopušta samo dokumentirana imena parametara. Upotrebljava kratko vremensko ograničenje povezivanja, ograničeno ukupno trajanje i najviše tri ukupna pokušaja. Ponovno se pokušavaju samo transportni neuspjesi, ograničavanje brzine i pogreške poslužitelja. Ostali odgovori 4xx obično upućuju na zahtjev koji se ponavljanjem neće poboljšati.
<?php
// src/Identity/IdentityResolver.php
namespace App\Identity;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final readonly class IdentityResolver
{
private const PARAMETERS = [
'username', 'id', 'identifier', 'profile', 'url',
];
public function __construct(
private HttpClientInterface $httpClient,
private LoggerInterface $logger,
private string $endpoint,
private ?\Closure $sleep = null,
) {
}
public function resolve(
string $platform,
string $parameter,
string $reference,
): array {
$platform = strtolower(trim($platform));
$reference = trim($reference);
if ($platform === '' || $reference === ''
|| !in_array($parameter, self::PARAMETERS, true)) {
throw new IdentityResolverException(
'validation',
'A platform, supported parameter, and reference are required.'
);
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->httpClient->request('GET', $this->endpoint, [
'query' => [
'platform' => $platform,
$parameter => $reference,
],
'timeout' => 3.0,
'max_duration' => 8.0,
'headers' => ['Accept' => 'application/json'],
]);
$status = $response->getStatusCode();
$headers = $response->getHeaders(false);
if (($status === 429 || $status >= 500) && $attempt < 3) {
$this->pause($attempt, $headers['retry-after'][0] ?? null);
continue;
}
if ($status < 200 || $status >= 300) {
throw new IdentityResolverException(
$status === 429 ? 'rate_limited' : 'upstream_rejected',
'The identity service rejected the request.',
$status === 429 || $status >= 500,
$status,
);
}
$decoded = json_decode(
$response->getContent(false),
true,
512,
JSON_THROW_ON_ERROR
);
if (!is_array($decoded) || $decoded === []) {
throw new IdentityResolverException(
'invalid_response',
'The identity service returned no usable identity object.'
);
}
return $decoded;
} catch (TransportExceptionInterface $exception) {
if ($attempt === 3) {
throw new IdentityResolverException(
'transport',
'The identity service is temporarily unreachable.',
true,
null,
$exception,
);
}
$this->logger->warning('Identity resolution transport failure', [
'attempt' => $attempt,
'platform' => $platform,
]);
$this->pause($attempt, null);
} catch (\JsonException $exception) {
throw new IdentityResolverException(
'invalid_response',
'The identity service returned invalid JSON.',
false,
null,
$exception,
);
}
}
throw new \LogicException('Unreachable retry state.');
}
private function pause(int $attempt, ?string $retryAfter): void
{
$seconds = ctype_digit((string) $retryAfter)
? min(2.0, (float) $retryAfter)
: 0.15 * (2 ** ($attempt - 1));
$sleeper = $this->sleep ?? static fn (int $microseconds) =>
usleep($microseconds);
$sleeper((int) ($seconds * 1_000_000));
}
}
Primijetite što zapisi izostavljaju: poslani URL, korisničko ime, tijelo odgovora i cijeli niz upita. Javne informacije i dalje mogu biti osjetljive u agregatu, stoga nadzornici trebaju bilježiti operativni kontekst bez pretvaranja zapisa u paralelnu bazu kontakata.
Povežite ubrizgavanje ovisnosti
Izričito konfigurirajte skalarni krajnji punkt u config/services.yaml:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
App\Identity\IdentityResolver:
arguments:
$endpoint: '%env(string:IDENTITY_RESOLVER_URI)%'
Symfony automatski ubrizgava HttpClientInterface i zapisivač. Neobavezni uspavljivač i dalje je koristan za determinističke testove i ne zahtijeva produkcijsku konfiguraciju.
Izložite krajnji punkt kartice profila
Kontroler prihvaća JSON iz upravitelja kontakata kreatora. Njegov odabir parametra je izričit, što podržava URL-ove, kao i korisnička imena i druge dokumentirane oblike referenci.
<?php
// src/Controller/ProfileCardController.php
namespace App\Controller;
use App\Identity\IdentityResolver;
use App\Identity\IdentityResolverException;
use App\Identity\ProfileCard;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
final class ProfileCardController extends AbstractController
{
#[Route('/api/creator-profile-cards', methods: ['POST'])]
public function create(
Request $request,
IdentityResolver $resolver,
): JsonResponse {
try {
$input = $request->toArray();
} catch (\JsonException) {
return $this->json(['error' => 'invalid_json'], 400);
}
$name = trim((string) ($input['creatorName'] ?? ''));
$platform = trim((string) ($input['platform'] ?? ''));
$parameter = trim((string) ($input['parameter'] ?? ''));
$reference = trim((string) ($input['reference'] ?? ''));
if ($name === '' || mb_strlen($name) > 120
|| mb_strlen($reference) > 2048) {
return $this->json(['error' => 'invalid_input'], 422);
}
try {
$identity = $resolver->resolve(
$platform,
$parameter,
$reference
);
return $this->json(
ProfileCard::fromResolved(
$name,
$platform,
$reference,
$identity
)->toArray(),
201
);
} catch (IdentityResolverException $exception) {
$status = match ($exception->failureType) {
'validation' => 422,
'rate_limited' => 503,
'transport' => 503,
default => 502,
};
return $this->json([
'error' => $exception->failureType,
'retryable' => $exception->retryable,
], $status);
}
}
}
U implementaciji okrenutoj pregledniku zaštitite ovu rutu uobičajenom autentikacijom i autorizacijom upravitelja kontakata. Ako se upotrebljava autentikacija kolačićima, zadržite Symfonyjevu CSRF zaštitu. Primijenite ograničenja veličine zahtjeva i ograničavanje brzine na razini aplikacije kako napadač ne bi mogao upotrebljavati vaš poslužitelj kao neograničeni proxy.
Testirajte bez pozivanja aktivne usluge
MockHttpClient čini transportno ponašanje determinističkim. Ovaj test provjerava točnu HTTP metodu i upit, izbjegavajući pritom ovisnost o aktivnom javnom profilu.
<?php
// tests/Identity/IdentityResolverTest.php
namespace App\Tests\Identity;
use App\Identity\IdentityResolver;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class IdentityResolverTest extends TestCase
{
public function testItResolvesAUrlReference(): void
{
$client = new MockHttpClient(
function (string $method, string $url): MockResponse {
self::assertSame('GET', $method);
parse_str((string) parse_url($url, PHP_URL_QUERY), $query);
self::assertSame('linkedin', $query['platform']);
self::assertSame(
'https://www.linkedin.com/in/example',
$query['url']
);
return new MockResponse(
'{"identity":{"kind":"public-profile"}}',
['http_code' => 200]
);
}
);
$resolver = new IdentityResolver(
$client,
new NullLogger(),
'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve',
static fn (int $microseconds) => null,
);
self::assertSame(
['identity' => ['kind' => 'public-profile']],
$resolver->resolve(
'linkedin',
'url',
'https://www.linkedin.com/in/example'
)
);
}
public function testItRetriesRateLimitingThenSucceeds(): void
{
$client = new MockHttpClient([
new MockResponse('', ['http_code' => 429]),
new MockResponse('{"identity":{"resolved":true}}', [
'http_code' => 200,
]),
]);
$resolver = new IdentityResolver(
$client,
new NullLogger(),
'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve',
static fn (int $microseconds) => null,
);
$result = $resolver->resolve('facebook', 'username', 'example');
self::assertTrue($result['identity']['resolved']);
self::assertSame(2, $client->getRequestsCount());
}
}
Pokrenite skup testova naredbom php bin/phpunit. Dodajte testove kontrolera za neispravan JSON, prevelike reference i svaki strukturirani odgovor o neuspjehu. Zaseban test mapiranja treba dokazati da različito poredani ključevi objekta stvaraju isti otisak.
Namjerno implementirajte i upravljajte sustavom
Produkcijska konfiguracija treba pružati IDENTITY_RESOLVER_URI putem izvođenog okruženja, zagrijati Symfonyjev predmemoriju i pokrenuti testove prije preusmjeravanja prometa. Trenutačno nije potrebna API vjerodajnica. Ako se autentikacija uvede kasnije, dodajte je samo prema službenoj dokumentaciji i ubrizgajte je putem Symfony tajni ili spremišta tajni platforme za implementaciju.
Pratite brojanja i latenciju prema vrsti neuspjeha, a ne prema referenci kreatora. Nagli porast invalid_response može otkriti promjenu ugovora; ponovljeni rezultati rate_limited upućuju na to da je potrebna pažnja u vezi s istodobnošću ili učestalošću zahtjeva. Predmemorirajte uspješna razrješavanja kada to zahtjevi proizvoda dopuštaju, ali definirajte rok isteka jer se javni profili mogu promijeniti.
Uobičajeni obrasci neuspjeha
- Neposredan odgovor 4xx: provjerite platformu i odabrani parametar reference. Nemojte ponovno pokušavati nepromijenjene neuspjehe validacije.
- Ponovljeni odgovori 429: poštujte ograničeno eksponencijalno čekanje, vratite aplikacijsku pogrešku koja se može ponovno pokušati i smanjite pritisak zahtjeva.
- Istek vremena ili odgovori 5xx: ponavljajte samo unutar fiksnog proračuna pokušaja i trajanja; nikada ne ostavljajte korisnika da čeka neograničeno.
- Valjan JSON neočekivanog oblika: odbacite prazne ili neupotrebljive podatke na granici, umjesto da dopustite kasniji neuspjeh koda predloška.
- Različiti otisci tijekom vremena: tretirajte otisak kao otisak snimke, a ne kao nepromjenjivi identifikator koji izdaje usluga.
Završni popis za provjeru
- Potvrdite trenutačni ugovor pristupa bez tokena u službenoj dokumentaciji.
- Pokrenite minimalni curl zahtjev s podržanom javnom referencom.
- Provjerite je li URI krajnjeg punkta podržan varijablama okruženja i da ne postoji lažni token.
- Pokrenite PHPUnit i potvrdite da testovi ponovnih pokušaja ne kontaktiraju mrežu.
- Pošaljite ime kreatora, platformu, parametar i referencu na Symfony rutu.
- Potvrdite da odgovor sadrži polja lokalne kartice, otisak i normalizirani objekt javnog identiteta.
- Isprobajte putanje neispravnog unosa, ograničavanja brzine, neispravnog JSON-a, isteka vremena i pogreške poslužitelja.
- Provjerite sadrže li zapisi operativne metapodatke, ali ne i društvenu referencu ili sadržaj odgovora.
Trajna pouka dizajna veća je od ove jedne integracije: vanjski odgovor identiteta treba prije ulaska u vaš proizvod prijeći usku, defenzivnu granicu. Kada su transportna pravila, validacija i mapiranje domene odvojeni, nedosljedne društvene poveznice prestaju širiti probleme na ostatak upravitelja kontakata. Postaju ono što su cijelo vrijeme trebale biti: zamjenjivi ulazi za jednu pouzdanu karticu kreatora.