Symfony: Разрешете ги социјалните врски во конзистентни картички за профили на креатори со ВИ
Управувачот со контакти на креатори често започнува со измамливо едноставно поле: „социјална врска“. Потоа пристигнуваат податоците. Едно лице лепи URL на Instagram, друго внесува корисничко име, а трето доставува референца до профил на LinkedIn. Споредувањето, отстранувањето дупликати и прикажувањето на тие вредности брзо станува ненадежно.
Identity Resolver го решава проблемот на границата. Тој прифаќа јавни референци од Facebook, Instagram и LinkedIn и враќа нормализиран објект за јавен идентитет. Во ова упатство ќе изградиме сервис на Symfony апликација што ги претвора тие резултати во конзистентни картички за профили на креатори, со дефанзивно мапирање, ограничени повторни обиди, структурирани неуспеси, тестови и логирање безбедно за продукција.
Добијте пристап пред да напишете код за интеграција
Започнете со страницата на услугата Identity Resolver, а потоа прочитајте ја официјалната документација. Документацијата е и авторитетен извор за информации за регистрација и информации за најава.
Тековната јавна крајна точка не бара токен за сметка ниту API клуч. Следствено, нема регистрација, најава, избор на план, екран за копирање токен или акредитив што треба да се заврши пред првото барање. Не измислувајте заглавие за авторизација и не ставајте токен-местодржач во апликацијата. Проверете ја повторно документацијата пред распоредување, во случај договорот за пристап подоцна да се промени.
Точната операција е:
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 'username=example_creator'
Бидејќи не постои акредитив, по овој тест нема тајна што треба да се зачува. Сепак, ќе ја задржиме крајната точка во конфигурација поддржана од околината, за staging, тестирањето и идните API миграции да не бараат промени во изворниот код.
Креирајте го Symfony проектот
Оваа имплементација е наменета за PHP 8.3 или понов и тековна Symfony апликација со FrameworkBundle, HttpClient, Monolog и PHPUnit алатки. Креирајте мал проект ориентиран кон API на следниов начин:
composer create-project symfony/skeleton creator-contacts
cd creator-contacts
composer require symfony/http-client symfony/monolog-bundle
composer require --dev symfony/test-pack
Додадете го URI-то на услугата во .env, при што ќе ја зачувате само оваа не-тајна стандардна вредност:
IDENTITY_RESOLVER_URI=https://ai.mihajlo.mk/api/identity-resolver/v1/resolve
Распоредувањето може да го надмине во .env.local или, по можност, преку конфигурацијата на околината на платформата за хостирање. Намерно нема IDENTITY_RESOLVER_TOKEN.
Проектот има три важни граници:
- Контролерот го валидира влезот од управувачот со контакти и го обликува одговорот на апликацијата.
- Решавачот го поседува HTTP однесувањето, повторните обиди и декодирањето на одговорот.
- Маперот на доменот прифаќа неизвесен надворешен објект и создава детерминистичка локална картичка.
Ќе ја задржиме резолуцијата синхрона. Корисник што додава една социјална референца има корист од непосреден резултат, а воведувањето Messenger би создало повеќе оперативна механика отколку што му е потребно на оваа интеракција. За масовни увози, истиот решавач подоцна може да се повика од Messenger обработувач.
Моделирајте стабилен одговор на апликацијата
Доставениот договор ветува нормализиран одговор за јавен идентитет, но не пропишува полиња што можеме безбедно да ги хардкодираме тука. Погодувањето клучеви како прикажувано име или URL на аватар би ја врзало апликацијата за претпоставки наместо за документација.
Наместо тоа, картичката комбинира локални податоци за контакт со валидираниот јавен објект. Таа исто така пресметува детерминистички отпечаток од рекурзивно сортиран JSON. Тој отпечаток ѝ припаѓа на нашата апликација; не е претставен како идентификатор издаден од услугата.
<?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,
];
}
}
Управувач со контакти поддржан од база на податоци може да ги зачува тие полиња во сопствениот ентитет. Одржувањето на перзистентноста надвор од API клиентот спречува транспортните неуспеси да протечат во кодот на доменот и ѝ овозможува на апликацијата да одлучи дали освежените јавни податоци треба да заменат претходна снимка.
Изградете отпорен решавач
Креирајте типизиран исклучок за повикувачите да можат да разликуваат невалиден влез, одбивање од надворешниот систем, привремена недостапност и погрешно форматирани податоци.
<?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 услугата дозволува само документирани имиња на параметри. Таа користи кратко временско ограничување за поврзување, ограничено вкупно времетраење и најмногу три вкупни обиди. Се повторуваат само транспортни неуспеси, ограничување на стапката и неуспеси на серверот. Другите 4xx одговори обично укажуваат на барање што нема да се подобри со повторување.
<?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));
}
}
Забележете што изоставуваат логовите: доставениот URL, корисничкото име, телото на одговорот и целата низа на барањето. Јавните информации сè уште можат да бидат чувствителни во збир, па набљудливоста треба да бележи оперативен контекст без да ги претвора логовите во сенковна база на податоци за контакти.
Поврзете вбризгување на зависности
Експлицитно конфигурирајте ја скаларната крајна точка во config/services.yaml:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
App\Identity\IdentityResolver:
arguments:
$endpoint: '%env(string:IDENTITY_RESOLVER_URI)%'
Symfony автоматски ги вбризгува HttpClientInterface и логерот. Опционалниот заспивач останува корисен за детерминистички тестови и не бара конфигурација за продукција.
Изложете ја крајната точка за картички на профили
Контролерот прифаќа JSON од управувачот со контакти на креатори. Неговиот избор на параметар е експлицитен, што поддржува URL-адреси, како и кориснички имиња и други документирани облици на референца.
<?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);
}
}
}
Во распоредување насочено кон прелистувач, заштитете ја оваа рута со вообичаената автентикација и авторизација на управувачот со контакти. Ако се користи автентикација со колачиња, задржете ги CSRF заштитите на Symfony. Применете ограничувања на големината на барањата и ограничување на стапката на ниво на апликација, за напаѓачот да не може да го користи вашиот сервер како неограничен прокси.
Тестирајте без да ја повикувате живата услуга
MockHttpClient го прави транспортното однесување детерминистичко. Овој тест ги проверува точниот HTTP метод и барањето, притоа избегнувајќи зависност од жив јавен профил.
<?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());
}
}
Извршете го пакетот со php bin/phpunit. Додадете тестови за контролерот за погрешно форматиран JSON, предолги референци и секој структуриран одговор за неуспех. Одделен тест за маперот треба да докаже дека различно подредените клучеви на објект генерираат ист отпечаток.
Распоредете и управувајте намерно
Конфигурацијата за продукција треба да достави IDENTITY_RESOLVER_URI преку околината при извршување, да го загрее кешот на Symfony и да изврши тестови пред пренасочување на сообраќајот. Во моментов не е потребен API акредитив. Ако подоцна се воведе автентикација, додајте ја само според официјалната документација и вбризгајте ја преку Symfony тајни или складиштето за тајни на платформата за распоредување.
Следете ги броевите и латентноста според типот на неуспех, а не според референцата на креаторот. Нагло зголемување на invalid_response може да открие промена на договорот; повторените резултати rate_limited укажуваат дека истовременоста или зачестеноста на барањата бара внимание. Кеширајте успешни резолуции кога барањата на производот го дозволуваат тоа, но дефинирајте истек бидејќи јавните профили можат да се променат.
Вообичаени обрасци на неуспех
- Непосреден 4xx одговор: проверете ја платформата и избраниот параметар за референца. Не повторувајте непроменети неуспеси на валидација.
- Повторени 429 одговори: почитувајте го ограниченото постепено одложување, вратете апликациска грешка што може да се повтори и намалете го притисокот од барања.
- Истекувања на време или 5xx одговори: повторувајте само во рамките на фиксниот буџет за обиди и времетраење; никогаш не го оставајте корисникот да чека неограничено.
- Валиден JSON со неочекувана структура: отфрлете празни или неупотребливи податоци на границата, наместо подоцна да дозволите неуспех на кодот за шаблони.
- Различни отпечатоци со текот на времето: третирајте го отпечатокот како отпечаток на снимка, а не како непроменлив идентификатор издаден од услугата.
Контролна листа за конечна проверка
- Потврдете го тековниот договор за пристап без токен во официјалната документација.
- Извршете го минималното curl барање со поддржана јавна референца.
- Проверете дека URI-то на крајната точка е поддржано од околината и дека не постои лажен токен.
- Извршете PHPUnit и потврдете дека тестовите за повторни обиди не контактираат со мрежата.
- Испратете име на креатор, платформа, параметар и референца до Symfony рутата.
- Потврдете дека одговорот содржи полиња на локалната картичка, отпечаток и нормализиран објект за јавен идентитет.
- Проверете ги патеките за невалиден влез, ограничување на стапката, погрешно форматиран JSON, истекување на време и грешка на серверот.
- Проверете дека логовите содржат оперативни метаподатоци, но не и социјална референца или товар на одговорот.
Трајната лекција за дизајн е поголема од оваа една интеграција: надворешен одговор за идентитет треба да премине тесна, дефанзивна граница пред да влезе во вашиот производ. Штом се раздвојат транспортната политика, валидацијата и мапирањето на доменот, неконзистентните социјални врски престануваат да го заразуваат остатокот од управувачот со контакти. Тие стануваат она што требало да бидат уште од почетокот: заменливи влезови за една доверлива картичка на креатор.