Laravel Creator Manager: Automatski generirajte kartice profila iz društvenih poveznica pomoću AI razlučivača identiteta
Upravitelj kontakata kreatora brzo postaje neuredan kada ista osoba stigne kao Instagram URL, referenca na LinkedIn profil i Facebook identifikator. Ako se svaki oblik pohranjuje doslovno, kartice profila postaju nedosljedne, otkrivanje duplikata postaje nepouzdano, a svako sučelje treba pravila parsiranja specifična za platformu.
Bolja granica je normalizirani identitet. U ovom vodiču izradit ćemo Laravel aplikaciju koja prihvaća podržane reference društvenih mreža, razrješava ih putem usluge Identity Resolver i pohranjuje stabilnu karticu profila spremnu za prikaz. Razrješavanje se izvršava u redu čekanja, neuspjesi ostaju vidljivi, a nesigurni podaci iz vanjskog izvora provjeravaju se prije nego što dosegnu model domene.
Dobijte pristup prije pisanja integracijskog koda
Počnite sa stranicom usluge Identity Resolver, zatim pročitajte službenu dokumentaciju. Trenutačni javni krajnji punkt ne zahtijeva token računa ni API ključ.
- Pregledajte podržane platforme i formate referenci na stranici usluge.
- Otvorite službenu dokumentaciju i potvrdite trenutačni ugovor o javnom pristupu.
- Budući da je krajnji punkt javan, upute za registraciju i zahtjevi za prijavu ne dodaju korak autentifikacije.
- Nemojte izmišljati ni kopirati bearer token. Trenutačno ne postoji polje vjerodajnice koje treba popuniti.
- Ako se autentifikacija uvede kasnije, tada slijedite dokumentaciju i dobivenu vjerodajnicu čuvajte u konfiguraciji koja se oslanja na okruženje, nikada u PHP izvornom kodu ili zapisnicima.
Točan zahtjev je GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. Pošaljite platform uz jedan podržani parametar username, id, identifier, profile ili url.
Napravite minimalni test s javnom referencom za čiju ste obradu ovlašteni:
curl --get \
--header "Accept: application/json" \
--data-urlencode "platform=instagram" \
--data-urlencode "url=https://www.instagram.com/example/" \
"https://ai.mihajlo.mk/api/identity-resolver/v1/resolve"
Provjerite stvarni odgovor u odnosu na službenu dokumentaciju umjesto da pretpostavite kako neobavezna polja uvijek postoje. Prije izrade značajke pohranite osnovni URL krajnjeg punkta u .env. Namjerno ne postoji varijabla tokena:
IDENTITY_RESOLVER_BASE_URL=https://ai.mihajlo.mk/api/identity-resolver
QUEUE_CONNECTION=database
Arhitektura i kompromisi
Aplikacija prihvaća platformu, vrstu reference i njezinu vrijednost. Odmah stvara zapis kontakta na čekanju, a zatim šalje zadatak u red čekanja. Zadatak poziva resolver, mapira odgovor u mali objekt domene i ažurira karticu.
Pozadinsko izvršavanje ovdje je korisno jer razrješavanje društvenih mreža nije potrebno za potvrdu predaje kontakta. Također sprječava da spor odgovor vanjskog izvora ili odgoda ponovnog pokušaja zauzmu web zahtjev. Kompromis je eventualna dosljednost: klijenti moraju prikazati stanje na čekanju i kasnije osvježiti karticu.
Relevantna struktura projekta namjerno je mala:
app/
Domain/Identity/ResolvedIdentity.php
Exceptions/IdentityResolutionException.php
Http/Controllers/CreatorProfileController.php
Jobs/ResolveCreatorIdentity.php
Models/CreatorProfile.php
Services/IdentityResolver.php
config/services.php
database/migrations/..._create_creator_profiles_table.php
routes/api.php
tests/Feature/CreatorProfileTest.php
tests/Unit/IdentityResolverTest.php
Izradite komponente Laravel projekta
Koristite PHP 8.3 ili noviji, podržanu Laravel instalaciju, konfiguriranu bazu podataka i pozadinu reda čekanja prikladnu za vašu implementaciju. Unutar postojeće Laravel aplikacije generirajte glavne komponente:
php artisan make:model CreatorProfile -m
php artisan make:controller CreatorProfileController
php artisan make:job ResolveCreatorIdentity
php artisan make:test CreatorProfileTest
php artisan make:test IdentityResolverTest --unit
php artisan queue:table
php artisan migrate
Dodajte URL usluge u config/services.php kako bi se konfiguracija mogla sigurno predmemorirati:
<?php
return [
// Existing services...
'identity_resolver' => [
'base_url' => env(
'IDENTITY_RESOLVER_BASE_URL',
'https://ai.mihajlo.mk/api/identity-resolver'
),
],
];
Tablica profila pohranjuje i predanu referencu i normaliziranu karticu. Također izlaže eksplicitna operativna stanja umjesto da svaki nedostajući identitet tretira kao isti neuspjeh.
<?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('creator_profiles', function (Blueprint $table): void {
$table->id();
$table->string('platform', 32);
$table->string('reference_type', 32);
$table->text('reference');
$table->string('resolution_state', 32)->default('pending');
$table->string('stable_identifier')->nullable();
$table->string('display_name')->nullable();
$table->text('profile_url')->nullable();
$table->text('avatar_url')->nullable();
$table->text('failure_message')->nullable();
$table->timestamp('resolved_at')->nullable();
$table->timestamps();
$table->index(['platform', 'stable_identifier']);
});
}
public function down(): void
{
Schema::dropIfExists('creator_profiles');
}
};
Konfigurirajte model s promišljenim masovnim dodjeljivanjem i pretvaranjem datuma:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
final class CreatorProfile extends Model
{
protected $fillable = [
'platform',
'reference_type',
'reference',
'resolution_state',
'stable_identifier',
'display_name',
'profile_url',
'avatar_url',
'failure_message',
'resolved_at',
];
protected function casts(): array
{
return ['resolved_at' => 'immutable_datetime'];
}
}
Mapirajte API odgovor na granici
Integracija ne bi trebala razasipati pristupe poljima niza kroz kontrolere i zadatke. Mapirač odgovora u nastavku zahtijeva JSON odgovor u obliku objekta i obrambeno odabire normalizirane vrijednosti. Nazivi kandidata rezervne su mogućnosti na granici, a ne tvrdnje da svaki odgovor sadrži svako polje.
<?php
namespace App\Domain\Identity;
use UnexpectedValueException;
final readonly class ResolvedIdentity
{
public function __construct(
public string $platform,
public string $stableIdentifier,
public ?string $displayName,
public ?string $profileUrl,
public ?string $avatarUrl,
) {}
public static function fromResponse(
array $payload,
string $requestedPlatform
): self {
$data = isset($payload['data']) && is_array($payload['data'])
? $payload['data']
: $payload;
$identifier = self::firstString(
$data,
['stable_identifier', 'identifier', 'id', 'username']
);
if ($identifier === null) {
throw new UnexpectedValueException(
'Resolver response has no usable identity identifier.'
);
}
return new self(
platform: self::firstString($data, ['platform'])
?? $requestedPlatform,
stableIdentifier: $identifier,
displayName: self::firstString(
$data,
['display_name', 'name', 'username']
),
profileUrl: self::firstString(
$data,
['profile_url', 'url']
),
avatarUrl: self::firstString(
$data,
['avatar_url', 'picture_url', 'image_url']
),
);
}
private static function firstString(
array $data,
array $keys
): ?string {
foreach ($keys as $key) {
$value = $data[$key] ?? null;
if (is_string($value) && trim($value) !== '') {
return trim($value);
}
}
return null;
}
}
Izradite ograničeni HTTP klijent svjestan ponovnih pokušaja
Klasa usluge koristi Laravelov ugrađeni HTTP klijent s odvojenim vremenskim ograničenjima povezivanja i ukupnog odgovora. Ponovno pokušava samo neuspjehe povezivanja, HTTP 408, HTTP 429 i pogreške poslužitelja. Provjera valjanosti i druge pogreške klijenta odmah ne uspijevaju jer ponavljanje istog zahtjeva ne može ih popraviti.
<?php
namespace App\Exceptions;
use RuntimeException;
final class IdentityResolutionException extends RuntimeException
{
public function __construct(
string $message,
public readonly bool $retryable,
public readonly ?int $status = null,
) {
parent::__construct($message);
}
}
<?php
namespace App\Services;
use App\Domain\Identity\ResolvedIdentity;
use App\Exceptions\IdentityResolutionException;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;
use UnexpectedValueException;
final class IdentityResolver
{
private const TYPES = [
'username', 'id', 'identifier', 'profile', 'url',
];
public function resolve(
string $platform,
string $type,
string $reference
): ResolvedIdentity {
if (!in_array($type, self::TYPES, true)) {
throw new IdentityResolutionException(
'Unsupported reference type.',
false
);
}
$endpoint = rtrim(
(string) config('services.identity_resolver.base_url'),
'/'
).'/v1/resolve';
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = Http::acceptJson()
->connectTimeout(2)
->timeout(8)
->get($endpoint, [
'platform' => $platform,
$type => $reference,
]);
} catch (ConnectionException $exception) {
if ($attempt === 3) {
throw new IdentityResolutionException(
'Resolver connection failed.',
true,
previous: $exception
);
}
usleep(250000 * $attempt);
continue;
}
if ($response->successful()) {
try {
$payload = $response->json();
if (!is_array($payload)) {
throw new UnexpectedValueException(
'Response is not a JSON object.'
);
}
return ResolvedIdentity::fromResponse(
$payload,
$platform
);
} catch (Throwable $exception) {
throw new IdentityResolutionException(
'Resolver returned an unusable response.',
false,
$response->status()
);
}
}
$retryable = $response->status() === 408
|| $response->status() === 429
|| $response->serverError();
Log::warning('Identity resolution request failed', [
'platform' => $platform,
'reference_type' => $type,
'status' => $response->status(),
'attempt' => $attempt,
]);
if (!$retryable || $attempt === 3) {
throw new IdentityResolutionException(
'Resolver rejected or could not complete the request.',
$retryable,
$response->status()
);
}
$retryAfter = (int) $response->header('Retry-After', 0);
$delayMs = $response->status() === 429
? min(max($retryAfter, 1), 5) * 1000
: 250 * $attempt;
usleep($delayMs * 1000);
}
throw new IdentityResolutionException(
'Resolver attempts exhausted.',
true
);
}
}
Zapisnik namjerno isključuje predanu referencu, tijelo odgovora i osobna polja profila. Status, platforma, vrsta i broj pokušaja dovoljni su za većinu operativne dijagnostike.
Razrješavajte identitete u zadatku reda čekanja
Zadatak ima povratno odgađanje na razini reda čekanja uz kratke ponovne pokušaje na razini zahtjeva. To pokriva dulji prekid rada bez zadržavanja radnika blokiranim. Trajni neuspjesi postaju stanje odbijanja; prolazni neuspjesi ponovno se bacaju kako bi ih red čekanja mogao pokušati ponovno izvršiti.
<?php
namespace App\Jobs;
use App\Exceptions\IdentityResolutionException;
use App\Models\CreatorProfile;
use App\Services\IdentityResolver;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
final class ResolveCreatorIdentity implements ShouldQueue
{
use Queueable;
public int $tries = 4;
public int $timeout = 15;
public function __construct(public readonly int $profileId) {}
public function backoff(): array
{
return [10, 60, 300];
}
public function handle(IdentityResolver $resolver): void
{
$profile = CreatorProfile::find($this->profileId);
if ($profile === null || $profile->resolution_state === 'resolved') {
return;
}
try {
$identity = $resolver->resolve(
$profile->platform,
$profile->reference_type,
$profile->reference
);
$profile->update([
'resolution_state' => 'resolved',
'stable_identifier' => $identity->stableIdentifier,
'display_name' => $identity->displayName,
'profile_url' => $identity->profileUrl,
'avatar_url' => $identity->avatarUrl,
'failure_message' => null,
'resolved_at' => now(),
]);
} catch (IdentityResolutionException $exception) {
if ($exception->retryable) {
throw $exception;
}
$profile->update([
'resolution_state' => 'rejected',
'failure_message' => $exception->getMessage(),
]);
}
}
}
Izložite dosljedne krajnje točke kartice profila
Kontroler provjerava platforme i nazive parametara prije slanja zadatka. URL se resolveru prosljeđuje kao podatak; ova aplikacija nikada sama ne dohvaća URL koji je dostavio korisnik, čime se izbjegava nepotrebna površina za krivotvorenje zahtjeva na strani poslužitelja.
<?php
namespace App\Http\Controllers;
use App\Jobs\ResolveCreatorIdentity;
use App\Models\CreatorProfile;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
final class CreatorProfileController extends Controller
{
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'platform' => [
'required',
Rule::in(['facebook', 'instagram', 'linkedin']),
],
'reference_type' => [
'required',
Rule::in([
'username', 'id', 'identifier', 'profile', 'url',
]),
],
'reference' => ['required', 'string', 'max:2048'],
]);
$profile = CreatorProfile::create([
...$data,
'resolution_state' => 'pending',
]);
ResolveCreatorIdentity::dispatch($profile->id);
return response()->json([
'id' => $profile->id,
'state' => 'pending',
], 202);
}
public function show(CreatorProfile $creatorProfile): JsonResponse
{
return response()->json([
'id' => $creatorProfile->id,
'state' => $creatorProfile->resolution_state,
'card' => [
'platform' => $creatorProfile->platform,
'identifier' => $creatorProfile->stable_identifier,
'display_name' => $creatorProfile->display_name,
'profile_url' => $creatorProfile->profile_url,
'avatar_url' => $creatorProfile->avatar_url,
],
'failure' => $creatorProfile->failure_message,
]);
}
}
<?php
use App\Http\Controllers\CreatorProfileController;
use Illuminate\Support\Facades\Route;
Route::post('/creator-profiles', [
CreatorProfileController::class,
'store',
]);
Route::get('/creator-profiles/{creatorProfile}', [
CreatorProfileController::class,
'show',
]);
Testirajte bez kontaktiranja stvarne usluge
Http::fake() čini mapiranje odgovora i tvrdnje o odlaznom upitu determinističkima. Test značajke zasebno provjerava da web zahtjev stvara zapis na čekanju i šalje zadatak.
<?php
namespace Tests\Unit;
use App\Services\IdentityResolver;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class IdentityResolverTest extends TestCase
{
public function test_it_maps_a_normalized_identity(): void
{
Http::fake([
'*/v1/resolve*' => Http::response([
'data' => [
'platform' => 'instagram',
'identifier' => 'creator-42',
'display_name' => 'Example Creator',
'profile_url' => 'https://www.instagram.com/example/',
'avatar_url' => 'https://cdn.example.test/avatar.jpg',
],
]),
]);
$identity = app(IdentityResolver::class)->resolve(
'instagram',
'username',
'example'
);
$this->assertSame('creator-42', $identity->stableIdentifier);
$this->assertSame('Example Creator', $identity->displayName);
Http::assertSent(fn ($request) =>
$request['platform'] === 'instagram'
&& $request['username'] === 'example'
&& $request->hasHeader('Accept', 'application/json')
);
}
}
<?php
namespace Tests\Feature;
use App\Jobs\ResolveCreatorIdentity;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
final class CreatorProfileTest extends TestCase
{
use RefreshDatabase;
public function test_it_creates_a_pending_profile_card(): void
{
Queue::fake();
$response = $this->postJson('/api/creator-profiles', [
'platform' => 'linkedin',
'reference_type' => 'url',
'reference' => 'https://www.linkedin.com/in/example/',
]);
$response->assertAccepted()
->assertJsonPath('state', 'pending');
$this->assertDatabaseHas('creator_profiles', [
'platform' => 'linkedin',
'resolution_state' => 'pending',
]);
Queue::assertPushed(ResolveCreatorIdentity::class);
}
}
Fixturei koriste rezervirane primjerne domene i sintetičke identitete. Dodajte testove za neispravne uspješne odgovore, HTTP 400, HTTP 429, pogreške poslužitelja i neuspjehe povezivanja. Za testove ponovnih pokušaja održite tvrdnje determinističkima tako da organizirate slijedove lažnih odgovora i izbjegavate ovisnost o vremenu stvarnog sata.
Sigurnost, vidljivost i implementacija
Zaštitite krajnje točke aplikacije pravilima autentifikacije i autorizacije prikladnima za upravitelj kontakata. Dodajte ograničavanje brzine po korisniku kako vaša krajnja točka ne bi mogla postati neograničeni proxy. Imena, URL-ove profila i URL-ove avatara tretirajte kao nepouzdan izlaz: izbjegnite tekst u HTML-u i pri prikazu poveznica ili slika dopustite samo očekivane URL sheme.
Nemojte zapisivati pune reference ni tijela iz vanjskih izvora. Zabilježite lokalni ID profila, statusni kod, trajanje, pokušaj i konačno stanje. Pratite starost zapisa na čekanju, odbijene zapise, neuspjehe reda čekanja, HTTP 429 odgovore i latenciju resolvera. Ti signali razlikuju loš unos od prekida rada ili nedovoljnog kapaciteta radnika.
Tijekom implementacije postavite IDENTITY_RESOLVER_BASE_URL, konfigurirajte produkcijski red čekanja, pokrenite php artisan migrate --force, a zatim ponovno izgradite predmemoriranu konfiguraciju s php artisan config:cache. Nakon objave koda ponovno pokrenite dugotrajne radnike reda čekanja kako bi učitali nove klase i konfiguraciju. Pokrenite radnike pod nadzorom procesnog supervisora i osigurajte da njihovo vremensko ograničenje na razini procesa ima dovoljno rezerve iznad vremenskog ograničenja zadatka od 15 sekundi.
Uobičajeni načini neuspjeha
- HTTP 400 ili 422: provjerite platformu, vrstu reference i predanu vrijednost. Nemojte ponovno pokušavati nepromijenjene neuspjehe provjere valjanosti.
- HTTP 429: poštujte ograničeni
Retry-After, zadržite povratno odgađanje reda čekanja i smanjite nepotrebno dvostruko razrješavanje. - HTTP 500 ili neuspjeh povezivanja: dopustite ograničene ponovne pokušaje, a zatim neka red čekanja pokuša ponovno kasnije.
- Uspješan, ali nepoznat JSON: odbijte ga na granici mapirača i usporedite stvarni sadržaj s službenom dokumentacijom.
- Profili ostaju na čekanju: provjerite radi li radnik reda čekanja i pregledajte neuspjele zadatke i strukturirane zapisnike.
- Konfiguracija izgleda zastarjelo: očistite ili ponovno izgradite Laravelovu predmemoriju konfiguracije nakon promjene vrijednosti okruženja.
Završni kontrolni popis za provjeru
- Javna dokumentacija je pregledana i nije konfiguriran nepostojeći API ključ.
- Aplikacija šalje točan GET zahtjev na
/api/identity-resolver/v1/resolvesplatformi jednim podržanim parametrom reference. - Vremenska ograničenja povezivanja i odgovora su ograničena.
- Ponovno se pokušavaju samo prolazni neuspjesi i ograničenja brzine.
- Zadaci reda čekanja izlažu stanja na čekanju, razriješeno i odbijeno.
- API odgovor se obrambeno mapira u stabilnu lokalnu karticu.
- Testovi koriste
Http::fake()i nikada ne pozivaju aktivnu uslugu. - Zapisnici izostavljaju reference društvenih mreža, tijela odgovora i vjerodajnice.
- Produkcijski radnik reda čekanja nadzire se i ponovno pokreće pri implementaciji.
Trajna pouka nije samo kako pozvati API za identitet. Riječ je o tome gdje smjestiti neizvjesnost. Društvene platforme, formati unosa i neobavezna polja mogu se razlikovati, ali ostatak aplikacije ne bi trebao mariti za to. Ograničavanjem te neizvjesnosti unutar jednog resolvera, jednog mapirača i jednog vidljivog tijeka rada reda čekanja, upravitelj kontakata dobiva dosljedne kartice profila bez vezivanja uz svaki oblik koji poveznica na društvenu mrežu može imati.