Туториали

Laravel Creator Manager: Auto-Generate Profile Cards from Social Links with AI Identity Resolver

Laravel Creator Manager: Автоматски генерирајте картички за профили од социјални врски со AI Identity Resolver

Управувачот со контакти на креатори брзо станува неуреден кога истата личност пристигнува како URL на Instagram, референца за профил на LinkedIn и идентификатор на Facebook. Ако секој облик се складира буквално, картичките за профили стануваат неконзистентни, откривањето дупликати станува ненадежно, а на секој интерфејс му требаат правила за парсирање специфични за платформата.

Подобра граница е нормализиран идентитет. Во ова упатство, ќе изградиме Laravel апликација што прифаќа поддржани референци за социјални мрежи, ги разрешува преку услугата Identity Resolver и зачувува стабилна, подготвена за прикажување картичка за профил. Разрешувањето се извршува во редица, неуспесите остануваат видливи, а несигурните надворешни податоци се валидираат пред да стигнат до доменскиот модел.

Обезбедете пристап пред да пишувате интеграциски код

Започнете со страницата на услугата Identity Resolver, а потоа прочитајте ја официјалната документација. Тековната јавна крајна точка не бара токен за сметка ниту API клуч.

  1. Прегледајте ги поддржаните платформи и формати на референци на страницата на услугата.
  2. Отворете ја официјалната документација и потврдете го тековниот договор за јавен пристап.
  3. Бидејќи крајната точка е јавна, упатствата за регистрација и барањата за најава не додаваат чекор за автентикација.
  4. Не измислувајте и не копирајте bearer токен. Во моментов нема поле за акредитиви што треба да се пополни.
  5. Ако подоцна се воведе автентикација, тогаш следете ја документацијата и чувајте го добиениот акредитив во конфигурација поддржана од околината, никогаш во PHP изворниот код или дневниците.

Точното барање е GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. Испратете platform плус еден поддржан параметар username, id, identifier, profile или url.

Направете минимален тест со јавна референца за чие обработување сте овластени:

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"

Проверете го вистинскиот одговор според официјалната документација, наместо да претпоставувате дека незадолжителните полиња секогаш постојат. Пред да ја изградите функционалноста, зачувајте ја основната URL-адреса на крајната точка во .env. Намерно нема променлива за токен:

IDENTITY_RESOLVER_BASE_URL=https://ai.mihajlo.mk/api/identity-resolver
QUEUE_CONNECTION=database

Архитектура и компромиси

Апликацијата прифаќа платформа, тип на референца и нејзина вредност. Таа веднаш создава запис за контакт што чека обработка, а потоа испраќа задача во редица. Задачата го повикува разрешувачот, го мапира одговорот во мал доменски објект и ја ажурира картичката.

Извршувањето во заднина е оправдано тука бидејќи разрешувањето на социјални референци не е потребно за да се потврди поднесување контакт. Исто така, спречува бавен надворешен одговор или доцнење при повлекување да зафаќа веб-барање. Компромисот е евентуална конзистентност: клиентите мора да прикажуваат состојба на чекање и подоцна да ја освежат картичката.

Релевантната структура на проектот е намерно мала:

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

Создадете ги компонентите на Laravel проектот

Користете PHP 8.3 или понов, поддржана Laravel инсталација, конфигурирана база на податоци и заднински систем за редици соодветен за вашето распоредување. Во постоечка Laravel апликација, генерирајте ги главните компоненти:

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

Додајте ја URL-адресата на услугата во config/services.php за конфигурацијата да може безбедно да се кешира:

<?php

return [
    // Existing services...

    'identity_resolver' => [
        'base_url' => env(
            'IDENTITY_RESOLVER_BASE_URL',
            'https://ai.mihajlo.mk/api/identity-resolver'
        ),
    ],
];

Табелата за профили ја чува и поднесената референца и нормализираната картичка. Таа исто така изложува експлицитни оперативни состојби, наместо секој недостасувачки идентитет да го третира како ист неуспех.

<?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');
    }
};

Конфигурирајте го моделот со намерно масовно доделување и претворање датуми:

<?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'];
    }
}

Мапирајте го API одговорот на границата

Интеграцијата не треба да распрскува пристапи до низи низ контролери и задачи. Маперот на одговорот подолу бара JSON одговор во облик на објект и дефанзивно избира нормализирани вредности. Имињата-кандидати се резервни опции на границата, а не тврдења дека секој одговор го содржи секое поле.

<?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;
    }
}

Изградете ограничен HTTP клиент што е свесен за повторни обиди

Класата на услугата го користи вградениот HTTP клиент на Laravel со одделни временски ограничувања за поврзување и за целосен одговор. Таа повторува само при неуспеси на поврзување, HTTP 408, HTTP 429 и серверски грешки. Валидацијата и другите клиентски грешки откажуваат веднаш бидејќи повторувањето на истото барање не може да ги поправи.

<?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
        );
    }
}

Дневникот намерно ја исклучува поднесената референца, телото на одговорот и личните полиња на профилот. Статусот, платформата, типот и бројот на обидот се доволни за повеќето оперативни дијагнози.

Разрешувајте идентитети во задача од редица

Задачата има повлекување на ниво на редица покрај кратките повторни обиди на ниво на барање. Ова покрива подолг прекин без да држи работник блокиран. Трајните неуспеси стануваат состојба на одбивање; минливите неуспеси повторно се фрлаат за редицата да може повторно да ги обиде.

<?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(),
            ]);
        }
    }
}

Изложете конзистентни крајни точки за картички на профили

Контролерот ги валидира платформите и имињата на параметрите пред испраќањето. URL-адресата му се предава на разрешувачот како податок; оваа апликација никогаш самата не ја презема таа URL-адреса поднесена од корисникот, со што се избегнува непотребна површина за фалсификување барања на серверска страна.

<?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',
]);

Тестирајте без да ја контактирате вистинската услуга

Http::fake() ги прави мапирањето на одговорите и тврдењата за излезните прашалници детерминистички. Функционалниот тест одделно потврдува дека веб-барањето создава запис што чека обработка и испраќа работа.

<?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);
    }
}

Фикстурите користат резервирани пример-домени и синтетички идентитети. Додајте тестови за неправилни успешни одговори, HTTP 400, HTTP 429, серверски грешки и неуспеси на поврзување. За тестови за повторни обиди, држете ги тврдењата детерминистички со поставување секвенци на лажни одговори и избегнување зависност од тајмингот на реалниот часовник.

Безбедност, набљудливост и распоредување

Заштитете ги крајните точки на апликацијата со правилата за автентикација и авторизација соодветни за управувачот со контакти. Додајте ограничување на стапката по корисник за вашата крајна точка да не може да стане неограничен прокси. Третирајте ги имињата, URL-адресите на профилите и URL-адресите на аватарите како недоверлив излез: избегнувајте текст во HTML и дозволувајте само очекувани URL шеми при прикажување врски или слики.

Не евидентирајте целосни референци или надворешни тела. Запишувајте локален ID на профил, статусен код, траење, обид и конечна состојба. Следете ја староста на записите што чекаат обработка, одбиените записи, неуспесите во редицата, HTTP 429 одговорите и латентноста на разрешувачот. Овие сигнали разликуваат лош влез од прекин или недоволен капацитет на работници.

При распоредување, поставете IDENTITY_RESOLVER_BASE_URL, конфигурирајте ја производната редица, извршете php artisan migrate --force, а потоа повторно изградете ја кешираната конфигурација со php artisan config:cache. Рестартирајте ги долготрајните работници во редицата по објавувањето на кодот за да ги вчитаат новите класи и конфигурација. Извршувајте ги работниците под надзор на процесен супервизор и дајте му на нивното временско ограничување на ниво на процес доволно простор над 15-секундното ограничување на задачата.

Вообичаени начини на неуспех

  • HTTP 400 или 422: проверете ги платформата, типот на референца и поднесената вредност. Не обидувајте повторно непроменети неуспеси при валидација.
  • HTTP 429: почитувајте ограничен Retry-After, задржете го повлекувањето на редицата и намалете го непотребното дуплирано разрешување.
  • HTTP 500 или неуспех на поврзување: дозволете ограничени повторни обиди, а потоа дозволете редицата да обиде повторно подоцна.
  • Успешен, но непознат JSON: одбијте го на границата на маперот и споредете го вистинскиот товар со официјалната документација.
  • Профилите остануваат во чекање: проверете дали работник во редицата работи и прегледајте ги неуспешните задачи и структурираните дневници.
  • Конфигурацијата изгледа застарена: исчистете или повторно изградете го кешот на конфигурацијата на Laravel по промена на вредностите на околината.

Конечна контролна листа за потврда

  • Јавната документација е прегледана и не е конфигуриран непостоечки API клуч.
  • Апликацијата испраќа точно GET барање до /api/identity-resolver/v1/resolve со platform и еден поддржан параметар за референца.
  • Временските ограничувања за поврзување и одговор се ограничени.
  • Само минливите неуспеси и ограничувањата на стапката се обидуваат повторно.
  • Задачите во редицата изложуваат состојби на чекање, разрешено и одбиено.
  • API одговорот се мапира дефанзивно во стабилна локална картичка.
  • Тестовите користат Http::fake() и никогаш не ја повикуваат живата услуга.
  • Дневниците изоставуваат референци за социјални мрежи, тела на одговори и акредитиви.
  • Производниот работник во редицата е надгледуван и се рестартира при распоредување.

Трајната поука не е само како да се повика API за идентитет. Таа е каде да се смести неизвесноста. Социјалните платформи, влезните формати и незадолжителните полиња може да варираат, но остатокот од апликацијата не треба да мора да се грижи за тоа. Со ограничување на таа неизвесност во еден разрешувач, еден мапер и еден набљудлив тек на работа со редица, управувачот со контакти добива конзистентни картички за профили без да стане поврзан со секој облик што може да го има една социјална врска.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.