Izradite profile autora: izvorni PHP integrira društvene poveznice s AI razrješivačem identiteta
Upravitelj kontakata za kreatore obično počinje bezazlenim poljem pod nazivom „društvena poveznica”. Ubrzo to polje sadrži pune URL-ove, korisnička imena, kopirane putanje profila, numeričke identifikatore i nekoliko načina zapisa iste platforme. Sučelje zatim tu nedosljednost prenosi u rezultate pretraživanja, izvoze i kartice profila.
Ovaj vodič izrađuje integraciju u izvornom PHP-u 8.3 koja te reference šalje servisu Identity Resolver i normalizirani odgovor pretvara u jedan predvidljiv model kartice profila. Granica ostaje namjerno stroga: resolver upravlja normalizacijom javnog identiteta, dok naša aplikacija upravlja ID-ovima kontakata, prikazom, pohranom i pravilima za neuspjehe.
Dobijte pristup prije pisanja integracijskog koda
Započnite sa stranicom usluge Identity Resolver, a zatim pročitajte službenu dokumentaciju. Trenutačni javni krajnji endpoint ne zahtijeva token računa ni API ključ.
Slijedom toga, registracija i prijava nisu koraci u ovom tijeku uvođenja. Upotrijebite službenu dokumentaciju kako biste provjerili trenutačni status registracije i status prijave, umjesto da nagađate nedokumentirane URL-ove računa. Nema zaslona za vjerodajnice niti ičega za kopiranje. Ako se autentifikacija uvede kasnije, prije promjene produkcijske konfiguracije dokumentaciju tretirajte kao izvor istine.
Točan zahtjev je GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. Pošaljite platform uz točno jedan podržani parametar username, id, identifier, profile ili url. Podržane platforme su Facebook, Instagram i LinkedIn.
Prvi zahtjev uputite s jednokratnom testnom referencom:
curl --fail-with-body --get \
--connect-timeout 3 \
--max-time 10 \
--data-urlencode "platform=instagram" \
--data-urlencode "username=YOUR_USERNAME" \
"https://ai.mihajlo.mk/api/identity-resolver/v1/resolve"
Nijedno zaglavlje Authorization ne pripada tom zahtjevu. Međutim, „javno” ne tumačite kao „neograničeno”: klijenti i dalje trebaju ograničena vremenska ograničenja, konzervativne ponovne pokušaje i izričito rukovanje ograničenjem stope.
Izvorni PHP ne učitava automatski datoteke .env. Za lokalni razvoj učitat ćemo je putem ljuske; produkcija bi trebala unijeti iste varijable putem svojeg upravitelja procesa ili sustava za upravljanje tajnama. Prazan unos tokena bilježi trenutačni ugovor o autentifikaciji i nikada se ne prenosi:
# .env
IDENTITY_RESOLVER_URL=https://ai.mihajlo.mk/api/identity-resolver/v1/resolve
IDENTITY_RESOLVER_TOKEN=
APP_ENV=development
# Install the only third-party development dependency.
composer require --dev phpunit/phpunit:^11.0
# Load local variables, then start the application.
set -a
. ./.env
set +a
php -S 127.0.0.1:8080 -t public
Arhitektura: nesigurne podatke zadržite na granici
Usluga obećava normalizirani objekt javnog identiteta, ali ovaj vodič ne pretpostavlja nedokumentirana imena polja kao što su ime za prikaz, avatar ili kanonski URL. Umjesto toga, granica provjerava je li odgovor JSON objekt i čuva ga pod identity. Kasniji prezentacijski sloj može mapirati dokumentirana polja bez povezivanja transportnog koda s pretpostavkama.
Ugovor kartice upravitelja kontakata stabilan je bez obzira na platformu:
contact_idje ključ zapisa u vlasništvu aplikacije.platformisourceopisuju poslanu referencu.identitysadrži resolverov normalizirani javni objekt.resolved_atbilježi svježinu bez pretvaranja da je riječ o podacima identiteta.
Mali sinkroni zahtjev prikladan je kada korisnik izričito doda ili osvježi jedan kontakt. Skupni uvozi trebali bi pozivati istu servisnu klasu iz radnog procesa kako spor pružatelj ne bi zauzeo svaki web-proces.
Upotrijebite ovu strukturu:
creator-contacts/
├── composer.json
├── public/
│ └── index.php
├── src/
│ ├── HttpTransport.php
│ ├── CurlTransport.php
│ ├── IdentityResolverClient.php
│ └── ProfileCard.php
└── tests/
└── IdentityResolverClientTest.php
Konfigurirajte Composerovo automatsko učitavanje s "CreatorContacts\\": "src/" i "CreatorContacts\\Tests\\": "tests/", zatim pokrenite composer dump-autoload.
Izradite ograničeni cURL transport i klijent resolvera
Transport izlaže odgovore umjesto da baca iznimke za HTTP statusne kodove. To omogućuje klijentu da razlikuje neuspjeh pružatelja koji se može ponoviti od trajnog neispravnog zahtjeva.
<?php
// src/HttpTransport.php
namespace CreatorContacts;
interface HttpTransport
{
public function get(string $url): TransportResponse;
}
final readonly class TransportResponse
{
public function __construct(
public int $status,
public array $headers,
public string $body,
) {}
}
// src/CurlTransport.php
namespace CreatorContacts;
use RuntimeException;
final class CurlTransport implements HttpTransport
{
public function get(string $url): TransportResponse
{
$headers = [];
$handle = curl_init($url);
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => 3000,
CURLOPT_TIMEOUT_MS => 10000,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_HEADERFUNCTION => static function ($curl, string $line) use (&$headers): int {
$length = strlen($line);
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return $length;
},
]);
$body = curl_exec($handle);
if ($body === false) {
$message = curl_error($handle);
curl_close($handle);
throw new RuntimeException('Identity Resolver transport error: ' . $message);
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
return new TransportResponse($status, $headers, $body);
}
}
Klijent lokalno provjerava valjanost, URL-kodira upit, ponavlja samo prolazne ishode i emitira metapodatke umjesto osobnih referenci ili tijela odgovora. Njegov se uspavljivač može ubrizgati, čime su testovi ponovnih pokušaja trenutačni i deterministički.
<?php
// src/IdentityResolverClient.php
namespace CreatorContacts;
use JsonException;
use RuntimeException;
final class ResolverException extends RuntimeException
{
public function __construct(public readonly string $category, string $message)
{
parent::__construct($message);
}
}
final class IdentityResolverClient
{
private const PLATFORMS = ['facebook', 'instagram', 'linkedin'];
private const REFERENCES = ['username', 'id', 'identifier', 'profile', 'url'];
public function __construct(
private readonly HttpTransport $transport,
private readonly string $endpoint,
private readonly mixed $sleeper = null,
private readonly mixed $logger = null,
) {}
public function resolve(string $platform, string $kind, string $value): array
{
$platform = strtolower(trim($platform));
$value = trim($value);
if (!in_array($platform, self::PLATFORMS, true)
|| !in_array($kind, self::REFERENCES, true)
|| $value === '') {
throw new ResolverException('validation', 'Unsupported or empty identity reference.');
}
if ($kind === 'url') {
$scheme = strtolower((string) parse_url($value, PHP_URL_SCHEME));
if (!in_array($scheme, ['http', 'https'], true)) {
throw new ResolverException('validation', 'Profile URL must use HTTP or HTTPS.');
}
}
$url = $this->endpoint . '?' . http_build_query(
['platform' => $platform, $kind => $value],
'',
'&',
PHP_QUERY_RFC3986,
);
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->transport->get($url);
} catch (RuntimeException $error) {
$this->log($platform, $attempt, null, 'transport_failure');
if ($attempt === 3) {
throw new ResolverException('unavailable', 'Resolver transport failed.');
}
$this->pause($attempt, null);
continue;
}
$this->log($platform, $attempt, $response->status, 'response');
if ($response->status === 200) {
try {
$object = json_decode($response->body, false, 512, JSON_THROW_ON_ERROR);
$identity = json_decode($response->body, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
throw new ResolverException('malformed_response', 'Resolver returned invalid JSON.');
}
if (!is_object($object) || !is_array($identity)) {
throw new ResolverException(
'malformed_response',
'Resolver response must be a JSON object.',
);
}
return $identity;
}
$retryable = $response->status === 429
|| in_array($response->status, [502, 503, 504], true);
if ($retryable && $attempt < 3) {
$this->pause($attempt, $response->headers['retry-after'] ?? null);
continue;
}
$category = match (true) {
$response->status === 429 => 'rate_limited',
$response->status >= 500 => 'unavailable',
default => 'rejected',
};
throw new ResolverException($category, 'Resolver request was not accepted.');
}
throw new ResolverException('unavailable', 'Resolver attempts exhausted.');
}
private function pause(int $attempt, ?string $retryAfter): void
{
$microseconds = min(2_000_000, 200_000 * (2 ** ($attempt - 1)));
if ($retryAfter !== null && ctype_digit($retryAfter)) {
$microseconds = min(2_000_000, (int) $retryAfter * 1_000_000);
}
($this->sleeper ?? usleep(...))($microseconds);
}
private function log(string $platform, int $attempt, ?int $status, string $event): void
{
($this->logger ?? error_log(...))(json_encode([
'event' => 'identity_resolver.' . $event,
'platform' => $platform,
'attempt' => $attempt,
'status' => $status,
], JSON_THROW_ON_ERROR));
}
}
Ograničenje od dvije sekunde za Retry-After namjerno je za interaktivni zahtjev. Dulji prozori kvote trebali bi brzo završiti neuspjehom kao rate_limited; aplikacija može zakazati kasnije osvježavanje umjesto da PHP radni proces ostane otvoren. Neuspjesi validacije, ostali odgovori 4xx i neispravni uspješni odgovori nikada se ne ponavljaju.
Mapirajte identitet u karticu profila
Domenski objekt odvaja podatke resolvera od metapodataka aplikacije. Pohrana može serijalizirati ovaj model prikaza kao JSON ili ga podijeliti u stupce baze podataka prema potrebama upravitelja kontakata.
<?php
// src/ProfileCard.php
namespace CreatorContacts;
use DateTimeImmutable;
final readonly class ProfileCard
{
public function __construct(
public string $contactId,
public string $platform,
public array $source,
public array $identity,
public string $resolvedAt,
) {}
public static function create(
string $contactId,
string $platform,
string $kind,
string $value,
array $identity,
): self {
return new self(
$contactId,
$platform,
[$kind => $value],
$identity,
(new DateTimeImmutable())->format(DATE_ATOM),
);
}
public function toArray(): array
{
return [
'contact_id' => $this->contactId,
'platform' => $this->platform,
'source' => $this->source,
'identity' => $this->identity,
'resolved_at' => $this->resolvedAt,
];
}
}
Prednji kontroler prihvaća ID kontakta, platformu i točno jednu podržanu referencu. Očekivane neuspjehe pretvara u strukturirana stanja bez izlaganja uzvodnih tijela ili tragova iznimki.
<?php
// public/index.php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use CreatorContacts\CurlTransport;
use CreatorContacts\IdentityResolverClient;
use CreatorContacts\ProfileCard;
use CreatorContacts\ResolverException;
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|| parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) !== '/profile-cards/resolve') {
http_response_code(404);
echo json_encode(['error' => ['code' => 'not_found']]);
exit;
}
try {
$input = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
$contactId = $input['contact_id'] ?? '';
$platform = $input['platform'] ?? '';
$references = array_intersect_key(
$input,
array_fill_keys(['username', 'id', 'identifier', 'profile', 'url'], true),
);
if (!is_string($contactId)
|| preg_match('/^[A-Za-z0-9_-]{1,64}$/', $contactId) !== 1
|| count($references) !== 1) {
throw new ResolverException('validation', 'Invalid contact or reference.');
}
$kind = (string) array_key_first($references);
$value = $references[$kind];
if (!is_string($platform) || !is_string($value)) {
throw new ResolverException('validation', 'Platform and reference must be strings.');
}
$endpoint = getenv('IDENTITY_RESOLVER_URL');
if ($endpoint === false || $endpoint === '') {
throw new RuntimeException('IDENTITY_RESOLVER_URL is not configured.');
}
$client = new IdentityResolverClient(new CurlTransport(), $endpoint);
$identity = $client->resolve($platform, $kind, $value);
$card = ProfileCard::create($contactId, strtolower($platform), $kind, $value, $identity);
echo json_encode(['data' => $card->toArray()], JSON_THROW_ON_ERROR);
} catch (ResolverException $error) {
$status = match ($error->category) {
'validation' => 422,
'rate_limited' => 429,
'rejected' => 400,
default => 503,
};
http_response_code($status);
echo json_encode(['error' => [
'code' => $error->category,
'message' => $error->getMessage(),
]], JSON_THROW_ON_ERROR);
} catch (Throwable) {
http_response_code(500);
echo json_encode(['error' => ['code' => 'internal_error']]);
}
Testirajte ponovne pokušaje i validaciju granice
Lažni transport poželjniji je od testova na živoj mreži: dokazuje konstrukciju upita i politiku ponovnih pokušaja bez trošenja kapaciteta pružatelja ili ovisnosti o promjenjivim javnim profilima.
<?php
// tests/IdentityResolverClientTest.php
namespace CreatorContacts\Tests;
use CreatorContacts\HttpTransport;
use CreatorContacts\IdentityResolverClient;
use CreatorContacts\ResolverException;
use CreatorContacts\TransportResponse;
use PHPUnit\Framework\TestCase;
final class FakeTransport implements HttpTransport
{
public array $urls = [];
public function __construct(private array $responses) {}
public function get(string $url): TransportResponse
{
$this->urls[] = $url;
return array_shift($this->responses);
}
}
final class IdentityResolverClientTest extends TestCase
{
public function testItBuildsTheRequestAndPreservesTheIdentityObject(): void
{
$transport = new FakeTransport([
new TransportResponse(200, [], '{"public":{"label":"Creator"}}'),
]);
$client = new IdentityResolverClient(
$transport,
'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve',
static fn (int $microseconds) => null,
static fn (string $message) => null,
);
$identity = $client->resolve('instagram', 'username', 'sample_creator');
self::assertSame(['public' => ['label' => 'Creator']], $identity);
self::assertStringContainsString('platform=instagram', $transport->urls[0]);
self::assertStringContainsString('username=sample_creator', $transport->urls[0]);
}
public function testItRetriesRateLimitingThenSucceeds(): void
{
$transport = new FakeTransport([
new TransportResponse(429, ['retry-after' => '1'], '{}'),
new TransportResponse(200, [], '{"normalized":true}'),
]);
$client = new IdentityResolverClient(
$transport,
'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve',
static fn (int $microseconds) => null,
static fn (string $message) => null,
);
self::assertSame(['normalized' => true], $client->resolve(
'linkedin',
'url',
'https://www.linkedin.com/in/sample',
));
self::assertCount(2, $transport->urls);
}
public function testItRejectsUnsupportedInputBeforeTransport(): void
{
$transport = new FakeTransport([]);
$client = new IdentityResolverClient($transport, 'https://example.invalid');
$this->expectException(ResolverException::class);
$client->resolve('unknown', 'username', 'sample');
}
}
Pokrenite vendor/bin/phpunit tests. Objekti odgovora u ovim fiksnim podacima namjerno su sintetički granični podaci, a ne tvrdnje o nedokumentiranim produkcijskim poljima.
Sigurnost, vidljivost i implementacija
Postavite kontroler iza postojeće autentifikacije i CSRF pravila upravitelja kontakata; primjer se usredotočuje na granicu resolvera umjesto na izmišljanje sustava prijave u aplikaciju. Dopustite samo pet ključeva referenci, ograničite veličinu tijela zahtjeva na web-poslužitelju i izbjegnite svaku vrijednost kada preglednik prikazuje vraćenu karticu. Javni društveni profil i dalje može sadržavati zlonamjeran tekst.
Nemojte bilježiti poslana korisnička imena, URL-ove, normalizirane podatke ili tijela pružatelja. Strukturirani događaji već izlažu korisne operativne dimenzije: događaj, platformu, pokušaj i status. Upozorite na trajne stope identity_resolver.unavailable i zasebno pratite rate_limited, jer ti uvjeti zahtijevaju različite odgovore.
Implementirajte s uključenim PHP proširenjima cURL i JSON. Pokrenite composer install --no-dev --classmap-authoritative, ubrizgajte IDENTITY_RESOLVER_URL u okruženje PHP-FPM-a ili spremnika te ponovno pokrenite radne procese kako bi ga naslijedili. Zadržite omogućenu provjeru odlaznog HTTPS-a; ova implementacija nikada ne onemogućuje provjere certifikata. Provjere zdravlja trebale bi provjeravati samu aplikaciju, a ne opetovano pozivati vanjsku uslugu.
Uobičajeni neuspjesi
- HTTP 422 lokalno: platforma, ID kontakta, URL shema ili broj referenci nisu prošli validaciju.
- Odbijeni zahtjev: potvrdite da je odabrani parametar podržan i konzultirajte službenu dokumentaciju prije promjene ugovora.
- HTTP 429: sačuvajte postojeću karticu, označite osvježavanje kao odgođeno i pokušajte ponovno kasnije umjesto stvaranja oluje ponovnih pokušaja.
- HTTP 503: pružatelj je prekoračio vrijeme, vratio prolazni status poslužitelja ili proizveo neupotrebljiv teret nakon ograničenih pokušaja.
- Prazna vrijednost okruženja: izvezite
IDENTITY_RESOLVER_URLu stvarno okruženje PHP radnog procesa, ne samo u interaktivnu ljusku.
Završni kontrolni popis provjere
- Potvrdite u službenoj dokumentaciji da endpoint ostaje javan i bez tokena.
- Pokrenite minimalni cURL zahtjev bez autorizacijskog zaglavlja.
- Pokrenite PHPUnit i potvrdite da lažni transport izvršava točno dva poziva u testu ograničenja stope.
- Pošaljite jednu po jednu referencu na
POST /profile-cards/resolve. - Potvrdite da rezultati za Facebook, Instagram i LinkedIn dijele isti oblik kartice na razini aplikacije.
- Potvrdite da zapisnici sadrže metapodatke statusa, ali ne i javni teret identiteta ni poslanu referencu.
- Prije implementacije provjerite putanje 429, 5xx, neispravnog JSON-a, vremenskog ograničenja i validacije.
Najtrajniji dio ove integracije nije cURL poziv. To je granica: promjenjiva javna referenca ulazi s jedne strane, validirani objekt identiteta izlazi s druge, a svaki neuspjeh postaje izričito stanje. Ta disciplina danas održava kartice kreatora dosljednima i upravitelju kontakata daje prostor za razvoj bez pretvaranja čišćenja društvenih poveznica u trajnu složenost aplikacije.