Native PHP 8.3: Генерирајте унифицирани картички за профили на креатори од врски на социјалните мрежи
Менаџер за контакти на креатори брзо станува неуреден кога едно лице пристигнува како URL-адреса на Instagram, друго како идентификатор на LinkedIn, а трето како корисничко име на Facebook. Директното споредување на тие низи создава дупликати на контакти, неконзистентни прикази и кршлив код специфичен за платформа.
Подобрата граница е нормализиран објект за идентитет. Во ова упатство, ќе изградиме Native PHP 8.3 апликација што испраќа јавни социјални референци до Identity Resolver, дефанзивно го валидира неговиот одговор и го мапира резултатот во стабилна шема за картичка на профил. Имплементацијата вклучува ограничени тајм-аути, селективни повторни обиди, детерминистички тестови, структурирани неуспеси, безбедно логирање и практични насоки за распоредување.
Добијте пристап пред да пишувате интеграциски код
Почнете со официјалната документација за Identity Resolver. Тековната јавна крајна точка не бара токен за сметка ниту API-клуч, така што нема задолжителен чекор за регистрација, најава, активирање план или копирање ингеренции пред првото барање.
- Прегледајте ги поддржаните влезови во официјалната документација.
- Проверете ја страницата за услугата и плановите за тековниот статус на услугата и информации за плановите.
- Во моментов не е потребен чекор за регистрација ниту чекор за најава за оваа јавна крајна точка. Ако моделот за воведување се промени, следете ги врските изложени во официјалната документација наместо да погодувате URL-адреси на порталот.
- Нема токен за копирање. Не измислувајте API-клуч, не додавајте празно bearer-заглавие и не складирајте ингеренција-заменка што услугата не ја бара.
Точното барање е GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. Испратете platform заедно со еден поддржан параметар username, id, identifier, profile или url.
Направете минимално тест-барање пред да ја изградите апликацијата:
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'
Заменете ја примерната URL-адреса со поддржана јавна референца што имате дозвола да ја обработувате. Успешниот одговор треба да биде JSON што го содржи нормализираниот објект за јавен идентитет. Намерно ќе избегнеме да претпоставуваме недокументирани имиња на полиња.
Иако нема ингеренција, чувајте ја оперативната конфигурација надвор од контролата на изворниот код. Создадете локална .env датотека и исклучете ја од Git:
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
Во продукција, внесете ги овие променливи преку менаџерот на процеси или хостинг-платформата. Ако подоцна се воведе автентикација, додајте ја документираната ингеренција како тајна во околината само откако ќе го потврдите новиот договор.
Архитектура и компромиси
Апликацијата има четири граници: HTTP-транспорт, клиент на Identity Resolver, доменски мапер и мал HTTP-контролер. Контролерот никогаш не толкува одговори специфични за провајдер. Транспортот знае за cURL, но ништо за креаторите. Разрешувачот ги поседува повторните обиди и валидацијата на одговорите. Маперот го претвора прифатениот објект за идентитет во верзионирана форма на картичка.
Разрешувањето останува синхроно бидејќи создавањето на еден контакт бара едно мало пребарување и на корисникот му треба непосредна повратна информација. Редицата би додала оперативна механика без да го подобри овој вообичаен работен тек на менаџерот за контакти. Ако подоцна се додадат масовни увози, истиот разрешувач може да стои зад worker.
Користете PHP 8.3 со екстензиите cURL и JSON, Composer и PHPUnit 11 за тестови:
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
Добиениот проект е намерно мал:
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
Изградете изолиран native cURL-транспорт
Интерфејсот ја прави надворешната граница заменлива во тестовите. Продукциската имплементација ги запишува заглавијата на одговорот бидејќи Retry-After може да влијае врз справувањето со ограничувањата на стапката.
<?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
);
}
}
Тајм-аутот за поврзување ги ограничува DNS и воспоставувањето на врска. Вкупниот тајм-аут го ограничува целото барање. Ниту еден не треба да остане со неограничена стандардна вредност во PHP-процес насочен кон корисници.
Разрешувајте идентитети со контролирани повторни обиди
Разрешувачот ги прифаќа само документираните платформи и имиња на параметри за референца. Тој повторува при неуспеси на транспортот, HTTP 429 и одговори 5xx од серверот. Неуспесите при валидација и другите одговори 4xx неуспеваат веднаш бидејќи повторувањето на истото барање нема да ги поправи.
<?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');
}
}
Ограничувањето на повторните обиди ги заштитува PHP worker-от и upstream-услугата. Се почитуваат само целобројни вредности на Retry-After, а доцнењето е ограничено на две секунди. Самата референца намерно не се внесува во логовите.
Мапирајте го одговорот во стабилна профилна картичка
Дадениот договор ветува нормализиран објект за јавен идентитет, но не оправдува тука тврдо кодирање на конкретни полиња во одговорот. Зачувајте го тој објект, прикачете локално познати метаподатоци и изведете стабилен отпечаток од канонски JSON. Ова го одржува складирањето на картичките конзистентно без да се преправа дека мора да постои недокументирано поле name или avatar.
<?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;
}
}
Хешот е отпечаток на апликацијата, а не тврдење дека услугата изложува конкретен идентификатор. Чувајте ја целосната картичка како JSON или мапирајте ја во вашата база на податоци за контакти. Кога ги прикажувате вредностите на идентитетот во HTML, секогаш избегнувајте ги со htmlspecialchars; јавните податоци сè уште се недоверлив влез.
Изложете ја крајната точка на менаџерот за контакти
Контролерот прифаќа неутрална влезна форма и ја преведува во точното барање што го очекува разрешувачот.
<?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']);
}
Продукциските апликации треба дополнително да ја ограничат големината на телото на барањето на веб-серверот и да спроведат разумни должини на референците пред да го направат појдовниот повик.
Тестирајте повторни обиди без да ја повикувате услугата
Лажен транспорт ги прави патеките на неуспех брзи и детерминистички. Не се потребни мрежа, сметка или ингеренција за fixture.
<?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/"}'
Безбедност, набљудливост и распоредување
Прифаќајте само документирани allowlist-и за платформа и параметри. Бидејќи одредишната URL-адреса е фиксна во конфигурацијата на околината, повикувачите не можат да ја претворат апликацијата во произволен HTTP-прокси. Не дозволувајте податоците од барањето да го заменат името на хостот на разрешувачот.
Логирајте имиња на настани, платформа, статус, обид, доцнење, времетраење и ID за корелација на барањето ако вашата апликација веќе има такво. Избегнувајте логирање на доставени кориснички имиња, URL-адреси, сурови тела или целосни нормализирани идентитети. Следете броеви на успеси, отфрлени влезови, ограничувања на стапката, upstream-неуспеси, неправилно форматирани одговори и латентност.
За распоредување, насочете го document root на веб-серверот кон public, извршувајте PHP-FPM како непривилегиран корисник, барајте HTTPS на јавниот раб и внесете ги трите променливи на околината во процесот PHP-FPM. Инсталирајте оптимизирани продукциски зависности со composer install --no-dev --classmap-authoritative. Проверете дали cURL му верува на CA-пакетот на оперативниот систем; никогаш не решавајте проблеми со сертификати со оневозможување на TLS-верификацијата.
Вообичаени неуспеси
- HTTP 400 или 422: Проверете ја платформата и осигурете се дека е испратен точно еден поддржан параметар за референца. Не повторувајте непроменет влез.
- HTTP 429: Почитувајте ограничен backoff, вратете структурирана состојба за ограничување на стапката по буџетот за повторни обиди и дозволете му на корисникот да се обиде подоцна.
- HTTP 401 или 403: Не додавајте погоден токен. Повторно проверете ја официјалната документација бидејќи тековниот јавен договор не бара таков.
- HTTP 5xx или cURL тајм-аут: Повторувајте само во рамките на малиот буџет, потоа вратете
503без да ги изложувате внатрешните детали на транспортот. - Валиден JSON со неочекувана форма: Третирајте го како неуспех на upstream-договорот. Не создавајте тивко делумна картичка.
- Картички што изгледаат како дупликати: Споредувајте ги канонскиот отпечаток и нормализираниот идентитет, а не оригиналното форматирање на доставената социјална врска.
Контролна листа за конечна верификација
- Апликацијата повикува само
GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. - Секое барање содржи
platformи точно еден поддржан параметар за референца. - Не се измислуваат токен, API-клуч или заглавие за авторизација.
- Тајм-аутите за поврзување и за вкупниот одговор се ограничени.
- Се повторуваат само неуспеси на транспортот, одговори 429 и одговори 5xx.
- Неправилно форматираниот JSON и одговорите што не се објекти создаваат структурирани неуспеси.
- Тестовите користат детерминистички лажен транспорт и никогаш не ја повикуваат живата услуга.
- Логовите ги исклучуваат социјалните референци и телата на нормализираните идентитети.
- Профилните картички ја користат истата верзионирана шема без оглед на изворната платформа.
- Продукцискиот web root го изложува само директориумот
public.
Вредниот резултат не е само уште еден API-повик. Тој е дисциплинирана граница меѓу непредвидливите социјални врски внесени од корисници и стабилниот модел што му е потребен на вашиот менаџер за контакти. Откако секоја референца за Facebook, Instagram и LinkedIn ќе помине низ таа граница, профилните картички стануваат конзистентни, неуспесите стануваат објасниви, а идните промени остануваат изолирани таму каде што припаѓаат.