Symfony: Стандардизирајте ги врските до социјалните мрежи во вашиот директориум со AI Identity Resolver
Директориумот на заедница ретко добива профили на социјални мрежи во еден уреден формат. Еден член внесува целосен Instagram URL, друг испраќа Facebook врска со параметри за следење, а некој друг внесува LinkedIn профил копиран од мобилен прелистувач. Ако тие вредности се зачуваат непроменети, пребарувањето, отстранувањето дупликати, валидацијата и прикажувањето брзо стануваат ненадежни.
Ова упатство создава Symfony функционалност подготвена за продукција што ги испраќа доставените Facebook, Instagram и LinkedIn врски до услугата Identity Resolver и враќа стабилен објект за јавен идентитет. Интеграцијата користи Symfony HTTP клиент, експлицитно мапирање на доменот, ограничени повторни обиди, структурирани неуспеси, детерминистички тестови и конфигурација безбедна за распоредување.
Обезбедете пристап пред да пишувате код за интеграција
Почнете со страницата на услугата Identity Resolver, која ги опишува услугата и достапните информации за плановите. Потоа прочитајте ја официјалната документација за Identity Resolver пред да направите барање.
Вообичаените прашања за регистрација и барања за најава тука имаат особено едноставен одговор: тековната јавна крајна точка не бара токен за сметка или API клуч. Затоа нема екран за акредитиви од кој треба да копирате токен и не треба да се додаде заглавие за авторизација. Не измислувајте и не обезбедувајте тајна со место за пополнување за оваа интеграција.
Точното барање е HTTP GET до:
https://ai.mihajlo.mk/api/identity-resolver/v1/resolve
Крајната точка прифаќа platform плус еден поддржан параметар username, id, identifier, profile или url. Бидејќи овој проект ги нормализира врските доставени во директориум, намерно испраќа само platform и url.
Направете минимално барање пред да ја изградите функционалноста:
curl --fail-with-body --get \
'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve' \
--data-urlencode 'platform=linkedin' \
--data-urlencode 'url=https://www.linkedin.com/in/example-profile/'
Заменете го примерот со URL на јавен профил што имате дозвола да го обработите. Не е потребно заглавие Authorization. Вратениот JSON третирајте го како нормализиран објект за јавен идентитет; границата на апликацијата подолу намерно избегнува да претпоставува недокументирани имиња на полиња.
Предуслови и структура на проектот
Потребни ви се PHP 8.3 или понов, Composer и Symfony апликација способна да прима JSON барања. Фокусирана инсталација може да се создаде со:
composer create-project symfony/skeleton community-directory
cd community-directory
composer require symfony/http-client symfony/monolog-bundle
composer require --dev symfony/test-pack
Функционалноста има три слоја:
- Контролерот ја валидира доставата во директориумот и е сопственик на HTTP одговорот на апликацијата.
IdentityResolverе сопственик на далечинскиот протокол, истекувањата, повторните обиди и класификацијата на upstream грешките.NormalizedSocialIdentityги пренесува платформата и нетранспарентниот нормализиран објект за идентитет во доменот на директориумот.
Нормализацијата е синхрона бидејќи лице што доставува профил во директориум има корист од непосредна повратна информација. Messenger би додал оперативна сложеност без да ја подобри оваа кратка патека на барањето. Ако увозите подоцна обработуваат илјадници постојни членови, истата услуга може да се повика од Messenger обработувач без да се промени нејзината API граница.
Релевантната структура е намерно мала:
config/services.yaml
src/Controller/NormalizeSocialLinkController.php
src/Identity/IdentityResolver.php
src/Identity/NormalizedSocialIdentity.php
src/Identity/ResolverFailure.php
tests/Identity/IdentityResolverTest.php
Конфигурирајте ја крајната точка без да измислувате акредитив
Чувајте ги вредностите специфични за распоредувањето во конфигурација поддржана од околински променливи. Бидејќи оваа јавна крајна точка во моментов нема токен, чувајте само нејзиниот URL во .env.local за време на развојот:
IDENTITY_RESOLVER_ENDPOINT=https://ai.mihajlo.mk/api/identity-resolver/v1/resolve
Не додавајте IDENTITY_RESOLVER_TOKEN или YOUR_SERVICE_TOKEN; ниту едно не припаѓа на доставениот договор за автентикација. Ако автентикацијата се воведе во иднина, следете ја официјалната документација и додајте го акредитивот преку складиштето за тајни на платформата за распоредување, наместо да го предадете во комит.
Поврзете ја крајната точка и HTTP клиентот во config/services.yaml:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
App\Identity\IdentityResolver:
arguments:
$endpoint: '%env(string:IDENTITY_RESOLVER_ENDPOINT)%'
Мапирајте го одговорот на границата на доменот
Објавениот договор ветува нормализиран одговор за јавен идентитет, но не оправдува претпоставување поединечни JSON својства. Зачувајте го тој објект, а на апликацијата дајте ѝ сопствена стабилна обвивка:
<?php
namespace App\Identity;
final readonly class NormalizedSocialIdentity
{
public function __construct(
public string $platform,
public array $publicIdentity,
) {
}
public function toArray(): array
{
return [
'platform' => $this->platform,
'identity' => $this->publicIdentity,
];
}
}
Ова раздвојување е важно. Договорот на вашиот контролер останува platform плус identity, додека документираниот објект за јавен идентитет на resolver-от останува недопрен. Ако директориумот користи Doctrine, зачувајте го објектот во JSON колона заедно со членот и платформата, или мапирајте ги документираните полиња само откако ќе ги потврдите во официјалната документација.
Претставете ги оперативните неуспеси експлицитно:
<?php
namespace App\Identity;
final class ResolverFailure extends \RuntimeException
{
public function __construct(
public readonly string $reason,
public readonly ?int $upstreamStatus = null,
?\Throwable $previous = null,
) {
parent::__construct($reason, 0, $previous);
}
}
Изградете ограничен HTTP клиент свесен за повторни обиди
Класата за услугата прифаќа само поддржани платформи и валидни HTTP или HTTPS URL-адреси. Таа повторува при транспортни грешки, ограничувања на стапката и тесен сет на минливи одговори од серверот. Грешките на клиентот не се повторуваат слепо. Одложувањето е ограничено за доставувањето профил да не може неограничено да зафаќа PHP worker.
<?php
namespace App\Identity;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class IdentityResolver
{
private const PLATFORMS = ['facebook', 'instagram', 'linkedin'];
private readonly \Closure $sleeper;
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly string $endpoint,
?\Closure $sleeper = null,
) {
$this->sleeper = $sleeper ?? static fn (int $microseconds) => usleep($microseconds);
}
public function resolveUrl(string $platform, string $url): NormalizedSocialIdentity
{
$platform = strtolower(trim($platform));
$url = trim($url);
if (!in_array($platform, self::PLATFORMS, true)) {
throw new \InvalidArgumentException('Unsupported social platform.');
}
if (
filter_var($url, FILTER_VALIDATE_URL) === false
|| !in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)
) {
throw new \InvalidArgumentException('A valid HTTP or HTTPS profile URL is required.');
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->httpClient->request('GET', $this->endpoint, [
'query' => [
'platform' => $platform,
'url' => $url,
],
'timeout' => 3.0,
'max_duration' => 8.0,
]);
$status = $response->getStatusCode();
if ($status >= 200 && $status < 300) {
$payload = $response->toArray(false);
if ($payload === [] || array_is_list($payload)) {
throw new ResolverFailure('malformed_response', $status);
}
return new NormalizedSocialIdentity($platform, $payload);
}
$retryable = $status === 429 || in_array($status, [502, 503, 504], true);
if ($retryable && $attempt < 3) {
$retryAfter = $response->getHeaders(false)['retry-after'][0] ?? null;
($this->sleeper)($this->delay($attempt, $retryAfter));
continue;
}
if ($status >= 400 && $status < 500) {
throw new ResolverFailure('request_rejected', $status);
}
throw new ResolverFailure('upstream_unavailable', $status);
} catch (DecodingExceptionInterface $exception) {
throw new ResolverFailure('malformed_response', null, $exception);
} catch (TransportExceptionInterface $exception) {
if ($attempt === 3) {
throw new ResolverFailure('transport_failure', null, $exception);
}
($this->sleeper)($this->delay($attempt, null));
}
}
throw new ResolverFailure('upstream_unavailable');
}
private function delay(int $attempt, ?string $retryAfter): int
{
if ($retryAfter !== null && ctype_digit($retryAfter)) {
return min(2_000_000, (int) $retryAfter * 1_000_000);
}
return min(2_000_000, 200_000 * (2 ** ($attempt - 1)));
}
}
Неисправен успешен одговор не се повторува бидејќи повторувањето на идентично барање веројатно нема да поправи проблем со шемата. Нумеричките вредности на Retry-After се почитуваат, но се ограничени на две секунди. Клиентот не испраќа колачиња, акредитиви или заглавија за авторизација.
Изложете ја рутата за нормализација на директориумот
Контролерот прифаќа достава насочена кон член, го повикува resolver-от, евидентира оперативен контекст без да ги евидентира целосниот URL на профилот или вратениот идентитет и произведува структурирани грешки на апликацијата:
<?php
namespace App\Controller;
use App\Identity\IdentityResolver;
use App\Identity\ResolverFailure;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
final class NormalizeSocialLinkController
{
#[Route('/directory/social-links/normalize', methods: ['POST'])]
public function __invoke(
Request $request,
IdentityResolver $resolver,
LoggerInterface $logger,
): JsonResponse {
try {
$input = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($input)) {
throw new \InvalidArgumentException('A JSON object is required.');
}
$platform = $input['platform'] ?? null;
$url = $input['url'] ?? null;
if (!is_string($platform) || !is_string($url)) {
throw new \InvalidArgumentException('Platform and URL must be strings.');
}
$identity = $resolver->resolveUrl($platform, $url);
return new JsonResponse($identity->toArray(), 200);
} catch (\JsonException|\InvalidArgumentException $exception) {
return new JsonResponse([
'error' => 'invalid_submission',
'message' => $exception->getMessage(),
], 422);
} catch (ResolverFailure $exception) {
$logger->warning('Identity resolution failed.', [
'reason' => $exception->reason,
'upstream_status' => $exception->upstreamStatus,
]);
$status = $exception->reason === 'request_rejected' ? 422 : 503;
return new JsonResponse([
'error' => $exception->reason,
], $status);
}
}
}
Рутата не прифаќа произволна далечинска крајна точка, па корисниците не можат да ја претворат во прокси за фалсификување барања на серверската страна. Доставениот URL се испраќа само како кодиран параметар на пребарување до конфигурираната крајна точка на resolver-от.
Тестирајте успех, облик на барањето и однесување при повторен обид
MockHttpClient ги прави тестовите детерминистички и спречува случаен мрежен сообраќај. Вештачките клучеви на фикстурите подолу тестираат транспарентно мапирање на границата; тие не тврдат дека ги документираат полињата на одговорот на услугата.
<?php
namespace App\Tests\Identity;
use App\Identity\IdentityResolver;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class IdentityResolverTest extends TestCase
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve';
public function testItMapsAResolvedIdentityAndSendsTheContractedQuery(): void
{
$http = new MockHttpClient(
function (string $method, string $url): MockResponse {
self::assertSame('GET', $method);
parse_str((string) parse_url($url, PHP_URL_QUERY), $query);
self::assertSame('instagram', $query['platform']);
self::assertSame(
'https://www.instagram.com/example/',
$query['url']
);
return new MockResponse(
json_encode(['fixture' => 'normalized'], JSON_THROW_ON_ERROR),
['http_code' => 200]
);
}
);
$resolver = new IdentityResolver($http, self::ENDPOINT);
$result = $resolver->resolveUrl(
'instagram',
'https://www.instagram.com/example/'
);
self::assertSame('instagram', $result->platform);
self::assertSame(['fixture' => 'normalized'], $result->publicIdentity);
}
public function testItRetriesATransientFailureWithoutActuallySleeping(): void
{
$http = new MockHttpClient([
new MockResponse('', ['http_code' => 503]),
new MockResponse(
json_encode(['fixture' => 'recovered'], JSON_THROW_ON_ERROR),
['http_code' => 200]
),
]);
$delays = [];
$resolver = new IdentityResolver(
$http,
self::ENDPOINT,
static function (int $delay) use (&$delays): void {
$delays[] = $delay;
}
);
$result = $resolver->resolveUrl(
'facebook',
'https://www.facebook.com/example/'
);
self::assertSame(['fixture' => 'recovered'], $result->publicIdentity);
self::assertSame([200_000], $delays);
self::assertSame(2, $http->getRequestsCount());
}
}
Извршете го пакетот со php bin/phpunit. Додајте тестови за контролерот за неисправен JSON и неподдржани платформи ако оваа крајна точка е дел од поголема безбедносна граница на апликацијата.
Безбедност, набљудливост и распоредување
Јавните врски на социјалните мрежи сè уште се податоци доставени од корисник. Наметнете ограничувања на големината на барањата на веб-серверот, барајте вообичаена автентикација на апликацијата каде што уредувањето на директориумот е ограничено и применете CSRF заштита на формуларите во прелистувачот. Не прикажувајте полиња од нормализираниот објект како доверлив HTML. Ескепирајте ги прикажаните вредности и дозволете само очекувани URL шеми.
Дневниците треба да ги содржат причината за неуспехот, upstream статусот, платформата кога е корисно, ID за корелација на барањето и изминатото време. Избегнувајте евидентирање на доставениот URL или целосниот одговор бидејќи кој било од нив може да содржи лични податоци или неочекувани параметри за пребарување. Следете ги стапките на request_rejected, transport_failure, malformed_response и HTTP 429 одговорите одделно.
При распоредување, дефинирајте IDENTITY_RESOLVER_ENDPOINT во секоја околина, загрејте го Symfony кешот и извршете го пакетот тестови пред да го пренасочите сообраќајот. Осигурете дека е дозволен појдовен HTTPS пристап до ai.mihajlo.mk. Задржете го истекувањето на PHP worker-от над максималното траење од осум секунди на клиентот, притоа зачувувајќи конечно истекување на барањето на ниво на апликација.
Вообичаени режими на неуспех
- Секое барање добива 422: потврдете дека телото е JSON и содржи вредности од тип string именувани
platformиurl. - Платформа е локално одбиена: користете точно
facebook,instagramилиlinkedin. - Upstream-от одбива барање: проверете дали избраната платформа одговара на доставениот јавен URL. Не повторувајте непроменети грешки на клиентот.
- Одговорите повремено стануваат 503: проверете ги дневниците за транспорт, истекување, 429 и upstream статус. Барањето веќе ги исцрпило ограничените повторни обиди.
- Тестовите ја контактираат вистинската услуга: инјектирајте
MockHttpClient; никогаш не правете ја достапноста на живото API дел од единичен тест. - Распоредувањето очекува тајна: отстранете ја претпоставката. Тековната јавна крајна точка не бара токен за сметка или API клуч.
Контролна листа за финална верификација
- Потврдете дека официјалната документација сè уште ја опишува јавната крајна точка како без токен.
- Испратете го минималното curl барање со соодветен URL на јавен профил.
- Извршете
php bin/phpunitи потврдете дека и тестовите за мапирање и повторен обид поминуваат. - Доставете Facebook, Instagram и LinkedIn URL-адреси преку рутата на директориумот.
- Потврдете дека успешните одговори ја содржат вашата стабилна обвивка со
platformиidentity. - Симулирајте неисправен JSON, неподдржана платформа, истекување, HTTP 429 и HTTP 503.
- Потврдете дека дневниците содржат структурирани метаподатоци за неуспех, но не и целосен URL на социјална мрежа или товар на идентитетот.
- Потврдете дека продукцијата дозволува појдовен HTTPS и ја доставува крајната точка преку конфигурација на околински променливи.
Направете ја нормализацијата граница, а не дополнителна мисла
Надежен директориум не треба да ја принудува секоја страница за пребарување, шаблон, извоз и проверувач на дупликати повторно да толкува произволни врски на социјални мрежи. Нормализирајте еднаш, во точката каде што податоците влегуваат во системот, и понатаму пренесувајте намерен доменски објект.
Најважниот избор на дизајнот не е самиот HTTP повик. Тоа е границата околу тој повик: документирани влезови, без измислена автентикација, одбранбено ракување со одговори, ограничено однесување при неуспех, дневници што ја почитуваат приватноста и тестови што никогаш не зависат од мрежата. Со овие делови поставени, неконзистентните врски на социјални мрежи стануваат предвидливи податоци во директориумот наместо трајна работа за чистење.