Native PHP 8.3: Разрешување врски до социјални медиуми за директориуми на заедници со Identity Resolver API
Директориумот на заедницата ретко добива уредни социјални идентитети. Еден член вметнува целосна URL-адреса од Instagram, друг поднесува мобилна врска од Facebook, а трет вклучува профил на LinkedIn со параметри за следење. Третирањето на тие низи како канонски идентификатори создава дупликат записи, кршливи врски и работа за чистење.
Овој туторијал гради продукциски ориентирана услуга во Native PHP 8.3 што прифаќа URL-адреси на профили од Facebook, Instagram и LinkedIn и ги испраќа до API-то Identity Resolver. API-то ја нормализира секоја јавна референца во стабилен објект за идентитет, додека нашата апликација се справува со валидација, истекувања на време, повторни обиди, неуспеси, евидентирање и одбранбено мапирање на одговорите.
Добијте пристап пред да напишете интеграциски код
Започнете со официјалната документација за Identity Resolver. Тековната јавна крајна точка не бара токен за сметка ниту API клуч, па нема акредитив што треба да го копирате пред вашето прво барање.
- Отворете ја документацијата и прегледајте ги поддржаните форми на влез.
- Прегледајте ја страницата за услугата и планот за тековни информации за услугата.
- Регистрација не е потребна за оваа јавна крајна точка.
- Најавување не е потребно пред да ја тестирате.
- Не додавајте измислен bearer токен, заглавие со API клуч или привремен акредитив. Ако барањата за автентикација се променат подоцна, следете ја официјалната документација наместо да нагаѓате шема за автентикација.
Точното барање е GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. Прифаќа platform плус поддржан параметар username, id, identifier, profile или url. Нашиот директориум користи url.
Заменете го местодржачот со вистински јавен профил и направете го првиот тест без заглавие за авторизација:
curl --get \
--url 'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve' \
--header 'Accept: application/json' \
--data-urlencode 'platform=instagram' \
--data-urlencode 'url=https://www.instagram.com/YOUR_PUBLIC_USERNAME/'
Договорот за услугата гарантира нормализиран одговор за јавен идентитет, но овој туторијал намерно не претпоставува недокументирани имиња на полиња. Апликацијата проверува дали одговорот е JSON-објект и го зачувува како upstream payload за идентитет.
Архитектура и компромиси
Проектот изложува POST /profiles/normalize кон предниот дел на директориумот. Ја проверува поднесената платформа и URL-адреса, ја повикува фиксната upstream крајна точка, го мапира резултатот во доменски објект и го враќа нормализираниот идентитет. Постоечко складиште на директориумот може да го зачува тој објект само откако повикот ќе успее.
Нормализацијата останува синхрона бидејќи обично е дел од прегледувањето или зачувувањето на еден профил. Редицата би додала оперативна сложеност и би ја отежнала непосредната валидација. Ако станат неопходни масовни увози, истата resolver класа подоцна може да работи во worker без промена на својата HTTP граница.
Native cURL ги задржува зависностите мали. Транспортот се наоѓа зад интерфејс, така што тестовите никогаш не контактираат со мрежата.
community-directory/
├── .env.example
├── composer.json
├── public/
│ └── index.php
├── src/
│ ├── Http.php
│ └── IdentityResolver.php
└── tests/
└── IdentityResolverTest.php
Предуслови и конфигурација
Ви треба PHP 8.3 или понов, екстензиите cURL и JSON, Composer и PHPUnit 11 за тестови.
{
"require": {
"php": "^8.3",
"ext-curl": "*",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
}
Инсталирајте ги зависностите со composer install. Ставете ги runtime поставките што не се тајни во .env.example и копирајте го во непратен .env за локален развој:
IDENTITY_RESOLVER_BASE_URL=https://ai.mihajlo.mk/api/identity-resolver
IDENTITY_RESOLVER_CONNECT_TIMEOUT_MS=1500
IDENTITY_RESOLVER_TIMEOUT_MS=5000
Намерно нема променлива за токен: на тековната крајна точка не ѝ треба акредитив. Во продукција, внесете ги овие вредности преку process manager или платформа за распоредување. За едноставната локална датотека погоре, стартувајте ја апликацијата со:
set -a
. ./.env
set +a
php -S 127.0.0.1:8080 -t public
Изградете заменлив cURL транспорт
Транспортот ја поседува cURL механиката, додека resolver-от ја поседува API политиката. Тој ги оневозможува пренасочувањата, применува одделни временски ограничувања за поврзување и вкупно време, ги собира заглавијата на одговорот и ги претвора мрежните грешки во типизиран исклучок.
<?php
// src/Http.php
declare(strict_types=1);
namespace App;
final readonly class HttpResponse
{
public function __construct(
public int $status,
public array $headers,
public string $body,
) {}
}
final class TransportException extends \RuntimeException {}
interface HttpTransport
{
public function get(
string $url,
array $query,
int $connectTimeoutMs,
int $timeoutMs,
): HttpResponse;
}
final class CurlTransport implements HttpTransport
{
public function get(
string $url,
array $query,
int $connectTimeoutMs,
int $timeoutMs,
): HttpResponse {
$headers = [];
$handle = curl_init($url . '?' . http_build_query(
$query,
'',
'&',
PHP_QUERY_RFC3986
));
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT_MS => $connectTimeoutMs,
CURLOPT_TIMEOUT_MS => $timeoutMs,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_USERAGENT => 'community-directory/1.0',
CURLOPT_HEADERFUNCTION => static function (
mixed $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 TransportException($message);
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
return new HttpResponse($status, $headers, $body);
}
}
Решавајте и мапирајте идентитети одбранбено
Класата на услугата ги прифаќа само трите платформи на директориумот, бара HTTPS и проверува дали името на хостот припаѓа на избраната платформа. Ова ги фаќа случајните несовпаѓања пред да потрошат upstream капацитет.
Привремените мрежни грешки, HTTP 429 и server-side 5xx одговорите добиваат најмногу три обиди. HTTP 4xx одговорите не се повторуваат слепо. Повлекувањето е ограничено, а целобројната вредност Retry-After се почитува до две секунди.
<?php
// src/IdentityResolver.php
declare(strict_types=1);
namespace App;
final readonly class IdentityResolution
{
public function __construct(
public string $platform,
public string $submittedUrl,
public array $publicIdentity,
) {}
}
final class ResolverException extends \RuntimeException
{
public function __construct(
public readonly string $kind,
string $message,
public readonly bool $retryable = false,
) {
parent::__construct($message);
}
}
final class IdentityResolver
{
private \Closure $sleep;
private \Closure $log;
public function __construct(
private HttpTransport $transport,
private string $baseUrl,
private int $connectTimeoutMs = 1500,
private int $timeoutMs = 5000,
?\Closure $sleep = null,
?\Closure $log = null,
) {
$this->sleep = $sleep ?? static fn (int $microseconds) =>
usleep($microseconds);
$this->log = $log ?? static function (array $context): void {};
}
public function resolve(string $platform, string $url): IdentityResolution
{
$platform = strtolower(trim($platform));
$domains = [
'facebook' => 'facebook.com',
'instagram' => 'instagram.com',
'linkedin' => 'linkedin.com',
];
if (!isset($domains[$platform])) {
throw new ResolverException('validation', 'Unsupported platform.');
}
if (strlen($url) > 2048 || filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new ResolverException('validation', 'Invalid profile URL.');
}
$parts = parse_url($url);
$host = strtolower(rtrim((string) ($parts['host'] ?? ''), '.'));
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
$domain = $domains[$platform];
if ($scheme !== 'https' ||
($host !== $domain && !str_ends_with($host, '.' . $domain))) {
throw new ResolverException(
'validation',
'The URL does not match the selected platform.'
);
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->transport->get(
rtrim($this->baseUrl, '/') . '/v1/resolve',
['platform' => $platform, 'url' => $url],
$this->connectTimeoutMs,
$this->timeoutMs,
);
} catch (TransportException $exception) {
($this->log)([
'event' => 'identity_resolver_transport_error',
'attempt' => $attempt,
]);
if ($attempt === 3) {
throw new ResolverException(
'transport',
'Identity service could not be reached.',
true
);
}
($this->sleep)(100000 * (2 ** ($attempt - 1)));
continue;
}
if ($response->status === 429 ||
($response->status >= 500 && $response->status <= 599)) {
if ($attempt === 3) {
$kind = $response->status === 429
? 'rate_limited'
: 'unavailable';
throw new ResolverException(
$kind,
'Identity service is temporarily unavailable.',
true
);
}
$retryAfter = $response->headers['retry-after'] ?? '';
$delay = ctype_digit($retryAfter)
? min(2000000, (int) $retryAfter * 1000000)
: 100000 * (2 ** ($attempt - 1));
($this->sleep)($delay);
continue;
}
if ($response->status < 200 || $response->status >= 300) {
throw new ResolverException(
'upstream_rejected',
'Identity reference was rejected.'
);
}
try {
$payload = json_decode(
$response->body,
true,
512,
JSON_THROW_ON_ERROR
);
} catch (\JsonException) {
throw new ResolverException(
'contract',
'Identity service returned invalid JSON.',
true
);
}
if (!is_array($payload) || $payload === [] || array_is_list($payload)) {
throw new ResolverException(
'contract',
'Identity service returned an unexpected response.',
true
);
}
($this->log)([
'event' => 'identity_resolver_success',
'platform' => $platform,
'attempt' => $attempt,
]);
return new IdentityResolution($platform, $url, $payload);
}
throw new \LogicException('Retry loop terminated unexpectedly.');
}
}
Изложете ја крајната точка на директориумот
Предниот контролер враќа предвидливи локални грешки без да открива cURL пораки или upstream тела на одговори. Неправилно поднесување добива 422, исцрпен привремен неуспех добива 503, а неочекуван upstream договор добива 502.
<?php
// public/index.php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use App\CurlTransport;
use App\IdentityResolver;
use App\ResolverException;
header('Content-Type: application/json; charset=utf-8');
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST' ||
$path !== '/profiles/normalize') {
http_response_code(404);
echo json_encode(['error' => 'not_found']);
exit;
}
try {
$input = json_decode(
file_get_contents('php://input'),
true,
512,
JSON_THROW_ON_ERROR
);
if (!is_array($input)) {
throw new JsonException('Expected an object.');
}
$resolver = new IdentityResolver(
new CurlTransport(),
getenv('IDENTITY_RESOLVER_BASE_URL')
?: 'https://ai.mihajlo.mk/api/identity-resolver',
(int) (getenv('IDENTITY_RESOLVER_CONNECT_TIMEOUT_MS') ?: 1500),
(int) (getenv('IDENTITY_RESOLVER_TIMEOUT_MS') ?: 5000),
log: static fn (array $context) =>
error_log(json_encode($context, JSON_THROW_ON_ERROR)),
);
$result = $resolver->resolve(
(string) ($input['platform'] ?? ''),
(string) ($input['url'] ?? ''),
);
echo json_encode(
['data' => $result->publicIdentity],
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
);
} catch (JsonException) {
http_response_code(400);
echo json_encode(['error' => 'invalid_json']);
} catch (ResolverException $exception) {
$status = match ($exception->kind) {
'validation' => 422,
'rate_limited', 'transport', 'unavailable' => 503,
default => 502,
};
http_response_code($status);
echo json_encode([
'error' => $exception->kind,
'retryable' => $exception->retryable,
]);
}
Тестирајте без да зависите од мрежата
Детерминистички fake го проверува однесувањето на мапирањето и повторните обиди. Тој исто така докажува дека неуспесите при валидација не прават upstream барање.
<?php
// tests/IdentityResolverTest.php
declare(strict_types=1);
namespace Tests;
use App\HttpResponse;
use App\HttpTransport;
use App\IdentityResolver;
use App\ResolverException;
use PHPUnit\Framework\TestCase;
final class IdentityResolverTest extends TestCase
{
public function testMapsObjectResponse(): void
{
$fake = new QueueTransport([
new HttpResponse(200, [], '{"stable":"public-value"}'),
]);
$result = (new IdentityResolver($fake, 'https://service.test'))
->resolve('instagram', 'https://www.instagram.com/example/');
self::assertSame(
['stable' => 'public-value'],
$result->publicIdentity
);
self::assertSame('instagram', $fake->queries[0]['platform']);
}
public function testRetriesRateLimitThenSucceeds(): void
{
$fake = new QueueTransport([
new HttpResponse(429, ['retry-after' => '1'], '{}'),
new HttpResponse(200, [], '{"stable":"value"}'),
]);
$delays = [];
$resolver = new IdentityResolver(
$fake,
'https://service.test',
sleep: static function (int $delay) use (&$delays): void {
$delays[] = $delay;
}
);
$resolver->resolve('linkedin', 'https://linkedin.com/in/example');
self::assertCount(2, $fake->queries);
self::assertSame([1000000], $delays);
}
public function testRejectsMismatchedDomainWithoutCallingTransport(): void
{
$fake = new QueueTransport([]);
$this->expectException(ResolverException::class);
try {
(new IdentityResolver($fake, 'https://service.test'))
->resolve('facebook', 'https://example.com/member');
} finally {
self::assertCount(0, $fake->queries);
}
}
}
final class QueueTransport implements HttpTransport
{
public array $queries = [];
public function __construct(private array $responses) {}
public function get(
string $url,
array $query,
int $connectTimeoutMs,
int $timeoutMs,
): HttpResponse {
$this->queries[] = $query;
return array_shift($this->responses);
}
}
Извршете го пакетот тестови со vendor/bin/phpunit tests.
Безбедност, набљудливост и распоредување
Фиксната upstream основна URL-адреса спречува server-side барања контролирани од корисникот. Валидацијата на URL-адресите отфрла HTTP врски, неповрзани домени, преголем влез и несовпаѓања на платформи. Применете ограничувања на телото на барањето и ограничувања на стапката по клиент и на веб-серверот или gateway-от.
URL-адресите на профилите можат да содржат лични идентификатори и параметри за следење. Не евидентирајте поднесени URL-адреси или целосни upstream payload-и. Примерот евидентира само тип на настан, платформа и број на обид. Во продукција, додадете идентификатор за корелација на барањето, латентност, конечен статус и број на повторни обиди без да снимате податоци за идентитет.
Распоредете со овозможена cURL екстензија, инсталирани продукциски зависности и променливи на околината внесени од runtime-от. Чувајте го .env надвор од контрола на верзии. Конфигурирајте ги PHP-FPM или околината на контејнерот експлицитно; не претпоставувајте дека развојната shell датотека ќе се вчита автоматски.
Користете здравствен мониторинг за промени во стапката на грешки и латентноста, но не правете live барање до трета страна од секоја health probe на апликацијата. Тоа ги претвора привремените upstream проблеми во непотребни локални рестартирања.
Вообичаени шеми на неуспех
- HTTP 422 локално: платформата не е поддржана, URL-адресата е неправилна или нејзиното име на хост не одговара на избраната платформа.
- HTTP 503 локално: услугата била недостапна, барањето било ограничено по стапка или услугата останала недостапна по ограничените повторни обиди.
- HTTP 502 локално: upstream-от ја одбил референцата или вратил податоци надвор од документираното очекување за JSON-објект од највисоко ниво.
- Непосреден cURL неуспех: потврдете дека
ext-curl, DNS, TLS сертификатите и излезниот HTTPS пристап се достапни. - Валиден, но неразрешен профил: потврдете дека профилот е јавен и дека избраната платформа и формата на влез се поддржани од официјалната документација.
Конечна контролна листа за проверка
- Апликацијата повикува точно
GET /api/identity-resolver/v1/resolve. - Секое барање испраќа
platformиurl. - Не се присутни токен, API клуч или измислено заглавие за автентикација.
- Хостовите на Facebook, Instagram и LinkedIn се проверуваат пред барањето.
- Временските ограничувања за поврзување и одговор се ограничени.
- Само мрежните грешки, одговорите 429 и 5xx се повторуваат.
- Одговорот се декодира и проверува на API границата.
- Тестовите користат детерминистички fake транспорт и никогаш не ја повикуваат јавната услуга.
- Дневниците ги исклучуваат поднесените URL-адреси, upstream payload-ите и акредитивите.
- Успешен нормализиран објект е подготвен за слојот за зачувување на директориумот.
Социјалните врски изгледаат како едноставни низи сè додека не станат долгорочни податоци на апликацијата. Трајното решение не е поголем куп регуларни изрази за чистење URL-адреси; тоа е тесна интеграциска граница со строги влезови, одбранбено мапирање на излезот, ограничено однесување при неуспех и тестови што ја прават мрежата заменлива. Со воспоставена таква граница, директориумот може да третира три неконзистентни социјални платформи како еден сигурен работен тек за идентитет.