Laravel Onboarding: Разрешете ги профилите на социјалните мрежи и поедноставете ги корисничките рецензии
Полето за профил на социјална мрежа изгледа безопасно сè додека од него не зависи процесот на воведување. Луѓето внесуваат кориснички имиња, URL-адреси на профили, нумерички идентификатори и референци копирани од мобилни апликации. Зачувувањето на тој необработен стринг ја префрла двосмисленоста во секој подоцнежен работен тек.
Оваа Laravel имплементација ги разрешува јавните референци за Facebook, Instagram и LinkedIn во нормализиран објект на идентитет, го зачувува резултатот без да претпоставува недокументирани полиња и го става во редица за рачна проверка. Надворешното пребарување се извршува асинхроно, неуспесите остануваат видливи, а одобрувањето е намерна човечка одлука наместо автоматски спореден ефект.
Добијте пристап пред да пишувате код за интеграција
Започнете со страницата на услугата Identity Resolver, а потоа отворете ја официјалната документација за да ги потврдите поддржаните платформи и параметрите на барањето.
Тековниот јавен крајни точки не бара ниту токен за сметка ниту API клуч. Следствено, нема акредитив што треба да се копира. Документацијата е и авторитетен извор за насоки за регистрација и насоки за најава за оваа интеграција: регистрација и најава не се потребни пред првото барање. Не измислувајте заглавие за авторизација и не ставајте токен-заместител во апликацијата.
Точниот повик е GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. Прифаќа platform плус еден поддржан параметар за референца: username, id, identifier, profile или url. Овој проект користи identifier за една форма да може да прифати и корисничко име и URL-адреса на профил.
curl --get 'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve' \
--data-urlencode 'platform=linkedin' \
--data-urlencode 'identifier=YOUR_PUBLIC_PROFILE_REFERENCE' \
--header 'Accept: application/json'
Проверете го вистинскиот одговор од документацијата и тест-барањето пред да дизајнирате логика за прикажување. Договорот ветува нормализиран одговор за јавен идентитет, но ова упатство намерно не измислува поединечни полиња од одговорот.
Нема акредитив што треба да се чува во .env. Чувајте ја само конфигурирабилната локација на услугата:
IDENTITY_RESOLVER_BASE_URL=https://ai.mihajlo.mk/api/identity-resolver
QUEUE_CONNECTION=database
Архитектура: прво разрешување, потоа одобрување
Патеката на барањето треба да биде брза и предвидлива. Контролерот ја валидира поднесената платформа и референца, создава запис за воведување и испраќа задача во редица. Задачата го повикува resolver-от преку посветен клиент и го зачувува нормализираниот одговор. Потоа, проверувач одобрува или одбива запис преку заштитени рути.
Ова воведува евентуална конзистентност: POST одговорот значи „прифатено за разрешување“, а не „профилот е потврден“. Тој компромис е корисен бидејќи бавна или привремено недостапна надворешна услуга повеќе не го задржува отворено барањето за воведување. Исто така, му дава на тимот за поддршка проверлива состојба на неуспех.
Релевантната структура на проектот е:
app/
Domain/Identity/ResolvedIdentity.php
Exceptions/IdentityResolutionException.php
Http/Controllers/OnboardingProfileController.php
Http/Controllers/ProfileReviewController.php
Jobs/ResolveOnboardingProfile.php
Models/OnboardingProfile.php
Services/IdentityResolverClient.php
config/services.php
database/migrations/..._create_onboarding_profiles_table.php
routes/web.php
tests/Feature/OnboardingProfileTest.php
tests/Unit/IdentityResolverClientTest.php
Создадете граница на перзистентност
Користете JSON колона за целосниот нормализиран одговор. Така се зачувува објектот на услугата без поврзување на шемата на базата со полиња што не се дел од доставениот договор. Колоната за статус го претставува работниот тек на апликацијата, а не надворешниот доставувач.
<?php
// database/migrations/2026_01_01_000000_create_onboarding_profiles_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('onboarding_profiles', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('platform', 20);
$table->string('submitted_reference', 2048);
$table->string('status', 20)->default('pending')->index();
$table->json('resolved_identity')->nullable();
$table->string('failure_code', 50)->nullable();
$table->text('failure_message')->nullable();
$table->foreignId('reviewed_by')->nullable()
->constrained('users')->nullOnDelete();
$table->timestamp('reviewed_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('onboarding_profiles');
}
};
<?php
// app/Models/OnboardingProfile.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
final class OnboardingProfile extends Model
{
protected $fillable = [
'user_id',
'platform',
'submitted_reference',
'status',
];
protected function casts(): array
{
return [
'resolved_identity' => 'array',
'reviewed_at' => 'immutable_datetime',
];
}
}
Изградете дефанзивен API клиент
Додајте го коренот на крајната точка во конфигурацијата на Laravel услуги поддржана од околински променливи:
<?php
// config/services.php
return [
// Existing services...
'identity_resolver' => [
'base_url' => env(
'IDENTITY_RESOLVER_BASE_URL',
'https://ai.mihajlo.mk/api/identity-resolver'
),
],
];
Доменското DTO прифаќа само JSON објект што не е листа. Намерно изложува генерички атрибути, бидејќи именувањето полиња како име за прикажување, аватар или канонска URL-адреса без документирана гаранција би ја направило интеграцијата кревка.
<?php
// app/Domain/Identity/ResolvedIdentity.php
namespace App\Domain\Identity;
use App\Exceptions\IdentityResolutionException;
final readonly class ResolvedIdentity
{
private function __construct(public array $attributes) {}
public static function fromResponse(array $payload): self
{
if ($payload === [] || array_is_list($payload)) {
throw new IdentityResolutionException(
'invalid_response',
'Resolver returned an unexpected JSON shape.'
);
}
return new self($payload);
}
}
Клиентот применува ограничени временски ограничувања и повторува само при неуспеси на врската, HTTP 429 одговори и серверски грешки. Одговорите 4xx од тип на валидација се враќаат веднаш. Одложувањето е кратко и ограничено, па една задача не може неограничено да зафаќа worker.
<?php
// app/Exceptions/IdentityResolutionException.php
namespace App\Exceptions;
use RuntimeException;
final class IdentityResolutionException extends RuntimeException
{
public function __construct(public readonly string $failureCode, string $message)
{
parent::__construct($message);
}
}
// app/Services/IdentityResolverClient.php
namespace App\Services;
use App\Domain\Identity\ResolvedIdentity;
use App\Exceptions\IdentityResolutionException;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Factory;
use Illuminate\Http\Client\Response;
final class IdentityResolverClient
{
public function __construct(private readonly Factory $http) {}
public function resolve(string $platform, string $identifier): ResolvedIdentity
{
$delays = [150_000, 500_000, 1_000_000];
$lastConnectionError = null;
foreach ($delays as $attempt => $delay) {
try {
$response = $this->request($platform, $identifier);
$retryable = $response->status() === 429 || $response->serverError();
if (! $retryable) {
return $this->map($response);
}
if ($attempt === array_key_last($delays)) {
$code = $response->status() === 429
? 'rate_limited'
: 'upstream_unavailable';
throw new IdentityResolutionException(
$code,
"Resolver request failed with HTTP {$response->status()}."
);
}
} catch (ConnectionException $exception) {
$lastConnectionError = $exception;
if ($attempt === array_key_last($delays)) {
throw new IdentityResolutionException(
'connection_failed',
'Could not connect to the identity resolver.'
);
}
}
usleep($delay);
}
throw new IdentityResolutionException(
'connection_failed',
$lastConnectionError?->getMessage() ?? 'Resolver request failed.'
);
}
private function request(string $platform, string $identifier): Response
{
return $this->http
->baseUrl((string) config('services.identity_resolver.base_url'))
->acceptJson()
->connectTimeout(3)
->timeout(8)
->get('/v1/resolve', [
'platform' => $platform,
'identifier' => $identifier,
]);
}
private function map(Response $response): ResolvedIdentity
{
if (! $response->successful()) {
throw new IdentityResolutionException(
'request_rejected',
"Resolver rejected the request with HTTP {$response->status()}."
);
}
$payload = $response->json();
if (! is_array($payload)) {
throw new IdentityResolutionException(
'invalid_response',
'Resolver returned invalid JSON.'
);
}
return ResolvedIdentity::fromResponse($payload);
}
}
Ставете го увозот во редица и изложете дејства за преглед
Задачата бележи структурирани неуспеси, но не зачувува тела на одговори или поднесени референци за профили во логовите. Овие вредности може да содржат лични податоци дури и кога изворниот профил е јавен.
<?php
// app/Jobs/ResolveOnboardingProfile.php
namespace App\Jobs;
use App\Exceptions\IdentityResolutionException;
use App\Models\OnboardingProfile;
use App\Services\IdentityResolverClient;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
final class ResolveOnboardingProfile implements ShouldQueue
{
use Queueable;
public function __construct(public readonly int $profileId) {}
public function handle(IdentityResolverClient $resolver): void
{
$profile = OnboardingProfile::findOrFail($this->profileId);
$profile->update(['status' => 'resolving']);
try {
$identity = $resolver->resolve(
$profile->platform,
$profile->submitted_reference
);
$profile->update([
'status' => 'ready',
'resolved_identity' => $identity->attributes,
'failure_code' => null,
'failure_message' => null,
]);
} catch (IdentityResolutionException $exception) {
$profile->update([
'status' => 'failed',
'failure_code' => $exception->failureCode,
'failure_message' => $exception->getMessage(),
]);
Log::warning('Identity resolution failed', [
'onboarding_profile_id' => $profile->id,
'failure_code' => $exception->failureCode,
]);
}
}
}
<?php
// app/Http/Controllers/OnboardingProfileController.php
namespace App\Http\Controllers;
use App\Jobs\ResolveOnboardingProfile;
use App\Models\OnboardingProfile;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
final class OnboardingProfileController
{
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'platform' => ['required', Rule::in([
'facebook', 'instagram', 'linkedin',
])],
'identifier' => ['required', 'string', 'max:2048'],
]);
$profile = OnboardingProfile::create([
'user_id' => $request->user()->id,
'platform' => $data['platform'],
'submitted_reference' => trim($data['identifier']),
'status' => 'pending',
]);
ResolveOnboardingProfile::dispatch($profile->id);
return response()->json([
'id' => $profile->id,
'status' => $profile->status,
], 202);
}
}
Дејствата за преглед мора да бидат автентицирани и овластени. Дефинирајте review-onboarding gate користејќи ги постојните улоги или дозволи на вашата апликација, а потоа направете ги транзициите атомски:
<?php
// app/Http/Controllers/ProfileReviewController.php
namespace App\Http\Controllers;
use App\Models\OnboardingProfile;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
final class ProfileReviewController
{
public function approve(Request $request, OnboardingProfile $profile): JsonResponse
{
DB::transaction(function () use ($request, $profile): void {
$locked = OnboardingProfile::lockForUpdate()->findOrFail($profile->id);
if ($locked->status !== 'ready') {
throw ValidationException::withMessages([
'status' => 'Only a resolved profile can be approved.',
]);
}
$locked->update([
'status' => 'approved',
'reviewed_by' => $request->user()->id,
'reviewed_at' => now(),
]);
});
return response()->json(['status' => 'approved']);
}
public function reject(Request $request, OnboardingProfile $profile): JsonResponse
{
abort_unless(in_array($profile->status, ['ready', 'failed'], true), 409);
$profile->update([
'status' => 'rejected',
'reviewed_by' => $request->user()->id,
'reviewed_at' => now(),
]);
return response()->json(['status' => 'rejected']);
}
}
<?php
// routes/web.php
use App\Http\Controllers\OnboardingProfileController;
use App\Http\Controllers\ProfileReviewController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->group(function (): void {
Route::post('/onboarding/social-profile',
[OnboardingProfileController::class, 'store']);
Route::middleware('can:review-onboarding')->group(function (): void {
Route::post('/profile-reviews/{profile}/approve',
[ProfileReviewController::class, 'approve']);
Route::post('/profile-reviews/{profile}/reject',
[ProfileReviewController::class, 'reject']);
});
});
Тестирајте успех, ограничување на барања и граници на работниот тек
Http::fake() ја прави интеграцијата детерминистичка и спречува тестовите да контактираат со јавната услуга.
<?php
// tests/Unit/IdentityResolverClientTest.php
namespace Tests\Unit;
use App\Exceptions\IdentityResolutionException;
use App\Services\IdentityResolverClient;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class IdentityResolverClientTest extends TestCase
{
public function test_it_maps_a_normalized_object(): void
{
Http::fake([
'*/v1/resolve*' => Http::response([
'stable_reference' => 'fixture-value',
'public' => true,
]),
]);
$result = app(IdentityResolverClient::class)
->resolve('linkedin', 'example-reference');
$this->assertSame('fixture-value', $result->attributes['stable_reference']);
Http::assertSent(fn ($request) =>
$request['platform'] === 'linkedin'
&& $request['identifier'] === 'example-reference'
&& ! $request->hasHeader('Authorization')
);
}
public function test_it_reports_persistent_rate_limiting(): void
{
Http::fake(['*/v1/resolve*' => Http::response([], 429)]);
$this->expectException(IdentityResolutionException::class);
app(IdentityResolverClient::class)
->resolve('instagram', 'example-reference');
}
}
Додајте feature тестови за автентицираниот POST, неподдржаните платформи, задачата во редицата, овластувањето на проверувачот, одобрувањето од ready и одбивањето од failed. Исто така, потврдете дека одобрување од pending е одбиено. Овие тестови ја штитат границата на човечкиот преглед, која е поважна од статусен код за успешна патека.
Безбедност, набљудливост и распоредување
- Применете ограничување на обидите за најава и ограничувач на стапка за рутите за поднесување при воведување, за еден корисник да не може да создаде неограничена редица.
- Прикажувајте ги нормализираните вредности како escape-нат текст. Јавниот профил и понатаму е недоверлив влез; никогаш не вметнувајте вратен HTML во екран за преглед.
- Ограничете ги рутите за преглед преку вистинска улога или permission gate. Самата автентикација не е доволна.
- Дефинирајте правила за задржување за необработените поднесоци, нормализираните податоци за идентитет, неуспесите и историјата на прегледи.
- Логирајте идентификатори на записи, времетраење, класа на статус, број на обиди и кодови за неуспех. Избегнувајте URL-адреси, кориснички имиња, цели тела на одговори и акредитиви.
- Следете ја староста на редицата, стапката на неуспешно разрешување, зачестеноста на HTTP 429, зачестеноста на серверски грешки и времето поминато во чекање на рачен преглед.
Распоредете ја миграцијата, кеширајте ја продукциската конфигурација и рестартирајте ги долготрајните queue workers за да го вчитаат новиот код и околината. За редица поддржана од база на податоци, создадете ја нејзината табела ако апликацијата веќе нема таква, мигрирајте и извршете надгледуван worker:
php artisan queue:table
php artisan migrate
php artisan config:cache
php artisan queue:work --queue=default --tries=1 --timeout=30
Извршете queue:table само кога таа миграција веќе не постои. Задачата користи еден обид во редицата бидејќи клиентот веќе изведува ограничени повторувања; додавањето повторувања на редицата врз нив би ги засилило прекините и ограничувањата на стапката. Неуспешното деловно разрешување останува достапно за преглед во базата на податоци наместо да исчезне во табелата failed-jobs.
Чести неуспеси и финална проверка
HTTP 4xx одговор обично укажува на неподдржана платформа, погрешно форматирана референца или изменето очекување на барањето; поправете го влезот наместо да го повторувате. HTTP 429 значи дека услугата ги ограничува барањата, додека 5xx и неуспесите на врската укажуваат на привремени проблеми кај upstream услугата. Невалиден JSON или неочекувана листа на највисоко ниво треба да стане invalid_response, а не PHP грешка на тип.
Пред објавување, проверете ја целосната патека:
- Документацијата потврдува дека јавната крајна точка сè уште не бара токен.
- Минимално GET барање успева со
platformи еден поддржан параметар за референца. - Апликацијата не испраќа заглавие за авторизација и не логира референца за профил.
- Поднесувањето враќа HTTP 202 и создава една задача во редицата.
- Успешното пребарување го преместува записот од
pendingпрекуresolvingвоready. - Ограничувањата на стапката и upstream неуспесите стануваат структурирани, видливи состојби на неуспех.
- Обичен корисник не може да пристапи до ниту едно дејство за преглед.
- Проверувачот може да одобри само запис
readyи може да одбие записreadyилиfailed. - Автоматизираните тестови не прават вистински мрежни повици.
- Продукциските workers, предупредувањата, задржувањето и сопственоста на проверувачите се документирани.
Важниот дизајнерски избор не е HTTP барањето. Тоа е одбивањето да се меша нормализацијата со довербата. Разрешете ја јавната референца на дисциплинирана граница, зачувајте го одговорот дефанзивно и дозволете човек да ја донесе последичната одлука. Таа мала поделба претвора кревка кратенка за воведување во работен тек со кој растечкиот тим може да работи со сигурност.