Laravel uvođenje: riješite društvene profile i pojednostavite korisničke recenzije
Polje za društveni profil djeluje bezazleno sve dok o njemu ne počne ovisiti uvođenje korisnika. Ljudi lijepe korisnička imena, URL-ove profila, numeričke ID-jeve i reference kopirane iz mobilnih aplikacija. Spremanje tog neobrađenog niza prenosi dvosmislenost na svaki kasniji tijek rada.
Ova implementacija u Laravelu razrješava javne Facebook, Instagram i LinkedIn reference u normalizirani objekt identiteta, pohranjuje rezultat bez pretpostavljanja nedokumentiranih polja i stavlja ga u red za ručni pregled. Vanjsko pretraživanje izvodi se asinkrono, neuspjesi ostaju vidljivi, a odobrenje je promišljena ljudska odluka, a ne automatska nuspojava.
Pribavite pristup prije pisanja integracijskog koda
Započnite sa stranicom usluge Identity Resolver, a zatim otvorite službenu dokumentaciju kako biste potvrdili podržane platforme i parametre zahtjeva.
Trenutačni javni krajnji punkt ne zahtijeva ni token računa ni API ključ. Slijedom toga, nema vjerodajnice za kopiranje. Dokumentacija je ujedno mjerodavna za upute za registraciju i upute za prijavu za ovu integraciju: registracija i prijava nisu potrebne prije prvog zahtjeva. Nemojte izmišljati zaglavlje za autorizaciju niti stavljati zamjenski token u aplikaciju.
Točan poziv je GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. Prihvaća platform i jedan podržani parametar reference: username, id, identifier, profile ili url. Ovaj projekt koristi identifier kako bi jedan obrazac mogao prihvatiti korisničko ime ili URL profila.
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'
Pregledajte stvarni odgovor iz dokumentacije i testnog zahtjeva prije oblikovanja logike prikaza. Ugovor obećava normalizirani odgovor javnog identiteta, ali ovaj vodič namjerno ne izmišlja pojedinačna polja odgovora.
Nema vjerodajnice za pohranu u .env. Pohranite samo podesivu lokaciju usluge:
IDENTITY_RESOLVER_BASE_URL=https://ai.mihajlo.mk/api/identity-resolver
QUEUE_CONNECTION=database
Arhitektura: prvo razrješavanje, zatim odobrenje
Put zahtjeva trebao bi biti brz i predvidljiv. Kontroler provjerava poslanu platformu i referencu, stvara zapis o uvođenju korisnika i šalje zadatak u red. Zadatak poziva razrješivač putem namjenskog klijenta i pohranjuje normalizirani odgovor. Pregledavatelj zatim odobrava ili odbija zapis putem zaštićenih ruta.
Time se uvodi eventualna konzistentnost: odgovor na POST znači „prihvaćeno za razrješavanje”, a ne „profil provjeren”. Taj je kompromis koristan jer spora ili privremeno nedostupna vanjska usluga više ne drži otvoren zahtjev za uvođenje korisnika. Također pruža osoblju podrške provjerljivo stanje neuspjeha.
Relevantna struktura projekta je:
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
Stvorite granicu postojanosti
Upotrijebite JSON stupac za potpuni normalizirani odgovor. Time se čuva objekt usluge bez povezivanja sheme baze podataka s poljima koja nisu dio isporučenog ugovora. Stupac statusa predstavlja tijek rada aplikacije, a ne vanjskog pružatelja usluge.
<?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',
];
}
}
Izradite obrambeni API klijent
Dodajte korijen krajnjeg punkta u Laravelovu konfiguraciju usluge koja se oslanja na okruženje:
<?php
// config/services.php
return [
// Existing services...
'identity_resolver' => [
'base_url' => env(
'IDENTITY_RESOLVER_BASE_URL',
'https://ai.mihajlo.mk/api/identity-resolver'
),
],
];
DTO domene prihvaća samo JSON objekt koji nije popis. Namjerno izlaže generičke atribute jer bi imenovanje polja poput prikaznog imena, avatara ili kanonskog URL-a bez dokumentiranog jamstva učinilo integraciju krhkom.
<?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);
}
}
Klijent primjenjuje ograničena vremenska ograničenja i ponovno pokušava samo kod neuspjeha veze, HTTP 429 odgovora i pogrešaka poslužitelja. Odgovori 4xx tipa provjere vraćaju se odmah. Odgoda između pokušaja kratka je i ograničena, tako da jedan zadatak ne može neograničeno zauzimati radnik.
<?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);
}
}
Stavite uvoz u red i izložite radnje pregleda
Zadatak bilježi strukturirane neuspjehe, ali ne pohranjuje tijela odgovora ni poslane reference profila u zapisnike. Te vrijednosti mogu sadržavati osobne podatke čak i kada je izvorni profil javan.
<?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);
}
}
Radnje pregleda moraju biti autentificirane i autorizirane. Definirajte pristupnik review-onboarding koristeći postojeće uloge ili dozvole svoje aplikacije, a zatim prijelaze učinite atomarnima:
<?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']);
});
});
Testirajte uspjeh, ograničavanje i granice tijeka rada
Http::fake() čini integraciju determinističkom i sprječava testove da kontaktiraju javnu uslugu.
<?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');
}
}
Dodajte funkcionalne testove za autentificirani POST, nepodržane platforme, zadatak u redu, autorizaciju pregledavatelja, odobrenje iz stanja ready i odbijanje iz stanja failed. Također potvrdite da se odobrenje iz stanja pending odbija. Ti testovi štite granicu ljudskog pregleda, koja je važnija od statusnog koda sretnog puta.
Sigurnost, vidljivost i implementacija
- Primijenite ograničavanje pokušaja prijave i ograničivač stope ruta na prijave za uvođenje korisnika kako jedan korisnik ne bi mogao stvoriti neograničen red.
- Normalizirane vrijednosti prikažite kao izbjegnuti tekst. Javni profil i dalje je nepouzdan unos; nikada ne umećite vraćeni HTML u zaslon za pregled.
- Ograničite rute za pregled stvarnim pristupnikom uloge ili dozvole. Sama autentifikacija nije dovoljna.
- Definirajte pravila zadržavanja za neobrađene prijave, normalizirane podatke identiteta, neuspjehe i povijest pregleda.
- Zabilježite ID-jeve zapisa, trajanje, klasu statusa, broj pokušaja i kodove neuspjeha. Izbjegavajte URL-ove, korisnička imena, puna tijela odgovora i vjerodajnice.
- Pratite starost reda, stopu neuspjelog razrješavanja, učestalost HTTP 429 odgovora, učestalost pogrešaka poslužitelja i vrijeme čekanja na ručni pregled.
Implementirajte migraciju, predmemorirajte produkcijsku konfiguraciju i ponovno pokrenite dugotrajne radnike reda kako bi učitali novi kôd i okruženje. Za red koji se temelji na bazi podataka stvorite njegovu tablicu ako je aplikacija već nema, migrirajte i pokrenite nadziranog radnika:
php artisan queue:table
php artisan migrate
php artisan config:cache
php artisan queue:work --queue=default --tries=1 --timeout=30
Pokrenite queue:table samo ako ta migracija već ne postoji. Zadatak koristi jedan pokušaj reda jer klijent već izvodi ograničena ponavljanja; dodavanje ponavljanja reda povrh toga pojačalo bi prekide i ograničenja stope. Neuspješno poslovno razrješavanje ostaje dostupno za pregled u bazi podataka umjesto da nestane u tablici neuspjelih zadataka.
Uobičajeni neuspjesi i završna provjera
HTTP 4xx odgovor obično upućuje na nepodržanu platformu, nepravilno oblikovanu referencu ili promijenjeno očekivanje zahtjeva; ispravite unos umjesto da ga ponovno pokušavate. HTTP 429 znači da usluga ograničava zahtjeve, dok 5xx i neuspjesi veze ukazuju na privremene poteškoće uzvodne usluge. Nevaljani JSON ili neočekivani popis na najvišoj razini trebali bi postati invalid_response, a ne PHP pogreška tipa.
Prije objave provjerite cijeli put:
- Dokumentacija potvrđuje da javni krajnji punkt i dalje ne zahtijeva token.
- Minimalni GET zahtjev uspijeva s parametrima
platformi jednim podržanim parametrom reference. - Aplikacija ne šalje zaglavlje za autorizaciju i ne zapisuje referencu profila.
- Prijava vraća HTTP 202 i stvara jedan zadatak u redu.
- Uspješno pretraživanje premješta zapis iz stanja
pendingprekoresolvinguready. - Ograničenja stope i uzvodni neuspjesi postaju strukturirana, vidljiva stanja neuspjeha.
- Običan korisnik ne može pristupiti nijednoj radnji pregleda.
- Pregledavatelj može odobriti samo zapis u stanju
readyi može odbiti zapis u stanjureadyilifailed. - Automatizirani testovi ne obavljaju stvarne mrežne pozive.
- Produkcijski radnici, upozorenja, zadržavanje i vlasništvo pregledavatelja dokumentirani su.
Važan izbor dizajna nije HTTP zahtjev. To je odbijanje da se normalizacija zamijeni za povjerenje. Razriješite javnu referencu na discipliniranoj granici, obrambeno zadržite odgovor i prepustite osobi donošenje odluke s posljedicama. To malo razdvajanje pretvara krhki prečac za uvođenje korisnika u tijek rada kojim rastući tim može upravljati s pouzdanjem.