Vodiči

Native PHP 8.3: Streamline Onboarding with Public Social Profile Imports and Manual Review

Native PHP 8.3: Pojednostavite uvođenje uz uvoz javnih profila na društvenim mrežama i ručnu provjeru

Uvoz s društvenog profila izgleda kao praktičan prečac pri uvođenju korisnika dok tiho ne poveže pogrešnu osobu, ne prepiše pažljivo uneseno ime ili privremeni uzvodni kvar ne pretvori u neuspjelu registraciju. Pouzdan dizajn namjerno je manje magičan: razriješite javnu referencu, sačuvajte normalizirani rezultat i zahtijevajte da ga čovjek odobri prije nego što uvezeni podaci postanu mjerodavni.

Ovaj vodič izrađuje taj tijek rada u izvornom PHP-u 8.3. Mala krajnja točka za uvođenje poziva Identity Resolver, vraćeni javni identitet pohranjuje kao uvoz na čekanju i konačnu odluku o odobravanju ili odbijanju prepušta ograničenoj naredbi. Implementacija koristi izvorni cURL, SQLite, PHPUnit, ograničene ponovne pokušaje, obrambeno mapiranje odgovora i strukturirana stanja neuspjeha.

Pribavite pristup prije pisanja integracijskog koda

Započnite sa službenom stranicom usluge Identity Resolver, a zatim pročitajte službenu dokumentaciju. Usluga normalizira javne reference za Facebook, Instagram i LinkedIn u stabilan objekt identiteta.

Trenutačna javna krajnja točka ne zahtijeva token računa ni API ključ. Stoga su službeni zahtjevi za registraciju i zahtjevi za prijavu za ovu krajnju točku oba „nisu potrebni”. Prije prvog zahtjeva nema koraka odabira plana, zaslona za vjerodajnice ni polja za kopiranje tokena. Nemojte izmišljati autorizacijsko zaglavlje ni stavljati nepovezanu vjerodajnicu platforme u aplikaciju.

Točan ugovor je GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve. Pošaljite platform zajedno s jednim podržanim parametrom reference: username, id, identifier, profile ili url.

Uputite minimalni testni zahtjev, zamjenjujući primjer korisničkog imena javnom referencom koju smijete obraditi:

curl --get \
  --connect-timeout 3 \
  --max-time 10 \
  --data-urlencode 'platform=instagram' \
  --data-urlencode 'username=example' \
  'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve'

Izvorna PHP aplikacija nema univerzalnu konvenciju za datoteku okruženja, stoga ovaj projekt koristi .env.local za lokalni razvoj i stvarne varijable procesnog okruženja pri implementaciji. Izričito zabilježite stanje autentikacije. Vrijednost tokena prazna je jer je trenutačno nema odakle kopirati:

IDENTITY_RESOLVER_ENDPOINT=https://ai.mihajlo.mk/api/identity-resolver/v1/resolve
IDENTITY_RESOLVER_TOKEN=
DATABASE_PATH=var/app.sqlite

Nemojte slati IDENTITY_RESOLVER_TOKEN. Zadržavanje prazne postavke dokumentira trenutačni ugovor i pruža očitu točku konfiguracije ako se autentikacija kasnije uvede i dokumentira.

Arhitektura: uvezeno ne znači odobreno

Put zahtjeva ostaje sinkron kako bi osoba koja prolazi uvođenje odmah dobila smislen rezultat. Aplikacija provjerava referencu, poziva razrješivač, pohranjuje normalizirani objekt i vraća 202 Accepted s identifikatorom pregleda na čekanju. Ne kopira udaljene atribute u zapis korisnika.

Pregledavatelj zatim provjerava pohranjeni objekt i odobrava ga ili odbija putem naredbe dostupne samo ovlaštenim operaterima. Odobravanje je zaštićeni prijelaz stanja: samo pending_review može postati approved ili rejected.

  • Granica API-ja: sastavlja fiksni HTTPS zahtjev i klasificira uzvodne neuspjehe.
  • Granica domene: prihvaća JSON objekt bez pretpostavljanja nedokumentiranih polja odgovora.
  • Repozitorij: pohranjuje normalizirani sadržaj i jednosmjerni hash poslane reference.
  • Naredba za pregled: odvaja ljudsku procjenu od automatiziranog razrješavanja.

SQLite odgovara maloj aplikaciji na jednom poslužitelju. Za više instanci aplikacije zamijenite ga dijeljenom transakcijskom bazom podataka uz očuvanje uvjetnog ažuriranja pregleda.

Izradite projekt PHP 8.3

Potrebni su vam PHP 8.3 ili noviji, Composer, proširenje cURL, PDO SQLite i podrška za JSON. Izradite ovu strukturu:

profile-onboarding/
├── bin/review-import.php
├── public/index.php
├── src/IdentityResolver.php
├── src/ImportRepository.php
├── tests/IdentityResolverTest.php
├── var/
├── .env.local
└── composer.json

Koristite PHPUnit 11 za determinističke testove granica:

{
  "require": {
    "php": "^8.3",
    "ext-curl": "*",
    "ext-json": "*",
    "ext-pdo": "*",
    "ext-pdo_sqlite": "*"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}
composer install
set -a
. ./.env.local
set +a
mkdir -p var
php -S 127.0.0.1:8080 -t public

Nemojte predati .env.local ni SQLite bazu podataka. U produkciji ubrizgajte iste varijable putem upravitelja procesa ili platforme za implementaciju, umjesto učitavanja datoteke.

Izradite obrambenog klijenta razrješivača

Transport je sučelje pa testovi nikada ne kontaktiraju mrežu. cURL je ograničen na HTTPS, preusmjeravanja su onemogućena, provjera certifikata ostaje omogućena, a vrijeme povezivanja i ukupno vrijeme su ograničeni.

<?php
// src/IdentityResolver.php
namespace App;

use Closure;
use JsonException;
use RuntimeException;

final readonly class HttpResponse
{
    public function __construct(
        public int $status,
        public string $body,
        public ?int $retryAfterSeconds = null,
    ) {}
}

interface Transport
{
    public function get(string $url, int $connectTimeout, int $timeout): HttpResponse;
}

final class CurlTransport implements Transport
{
    public function get(string $url, int $connectTimeout, int $timeout): HttpResponse
    {
        $handle = curl_init($url);
        curl_setopt_array($handle, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => true,
            CURLOPT_CONNECTTIMEOUT => $connectTimeout,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
            CURLOPT_HTTPHEADER => ['Accept: application/json'],
        ]);

        $raw = curl_exec($handle);
        if ($raw === false) {
            $message = curl_error($handle);
            curl_close($handle);
            throw new RuntimeException('Transport failure: ' . $message);
        }

        $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
        $headerSize = (int) curl_getinfo($handle, CURLINFO_HEADER_SIZE);
        curl_close($handle);

        $headers = substr($raw, 0, $headerSize);
        $body = substr($raw, $headerSize);
        $retryAfter = preg_match('/^Retry-After:\s*(\d+)/mi', $headers, $match)
            ? (int) $match[1]
            : null;

        return new HttpResponse($status, $body, $retryAfter);
    }
}

final class ResolverFailure extends RuntimeException
{
    public function __construct(
        public readonly string $reason,
        public readonly bool $retryable,
        public readonly ?int $upstreamStatus = null,
    ) {
        parent::__construct($reason);
    }
}

final readonly class NormalizedIdentity
{
    private function __construct(public array $data) {}

    public static function fromJson(string $json): self
    {
        try {
            $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
        } catch (JsonException) {
            throw new ResolverFailure('malformed_response', false);
        }

        if (!is_array($data) || array_is_list($data)) {
            throw new ResolverFailure('malformed_response', false);
        }

        return new self($data);
    }
}

final class IdentityResolver
{
    private const PARAMETERS = ['username', 'id', 'identifier', 'profile', 'url'];
    private const PLATFORMS = ['facebook', 'instagram', 'linkedin'];

    public function __construct(
        private readonly Transport $transport,
        private readonly string $endpoint,
        private readonly Closure $sleep,
        private readonly Closure $log,
    ) {}

    public function resolve(
        string $platform,
        string $parameter,
        string $reference,
    ): NormalizedIdentity {
        $platform = strtolower(trim($platform));
        $reference = trim($reference);

        if (!in_array($platform, self::PLATFORMS, true)
            || !in_array($parameter, self::PARAMETERS, true)
            || $reference === ''
            || strlen($reference) > 500) {
            throw new ResolverFailure('invalid_input', false);
        }

        $url = $this->endpoint . '?' . http_build_query(
            ['platform' => $platform, $parameter => $reference],
            '',
            '&',
            PHP_QUERY_RFC3986,
        );

        for ($attempt = 1; $attempt <= 3; $attempt++) {
            try {
                $response = $this->transport->get($url, 3, 10);
            } catch (RuntimeException) {
                if ($attempt === 3) {
                    throw new ResolverFailure('upstream_unavailable', true);
                }
                $this->pause($attempt, null, 'transport');
                continue;
            }

            if ($response->status === 200) {
                return NormalizedIdentity::fromJson($response->body);
            }

            if ($response->status === 429 || $response->status >= 500) {
                if ($attempt === 3) {
                    $reason = $response->status === 429
                        ? 'rate_limited'
                        : 'upstream_unavailable';
                    throw new ResolverFailure($reason, true, $response->status);
                }
                $this->pause(
                    $attempt,
                    $response->retryAfterSeconds,
                    (string) $response->status,
                );
                continue;
            }

            if (in_array($response->status, [400, 404, 422], true)) {
                throw new ResolverFailure('reference_rejected', false, $response->status);
            }

            if (in_array($response->status, [401, 403], true)) {
                throw new ResolverFailure('authentication_configuration', false, $response->status);
            }

            throw new ResolverFailure('unexpected_upstream_response', false, $response->status);
        }

        throw new ResolverFailure('upstream_unavailable', true);
    }

    private function pause(int $attempt, ?int $retryAfter, string $cause): void
    {
        $milliseconds = $retryAfter === null
            ? 150 * (2 ** ($attempt - 1))
            : min(max($retryAfter, 0), 2) * 1000;

        ($this->log)('identity_resolver_retry', [
            'attempt' => $attempt,
            'cause' => $cause,
            'delay_ms' => $milliseconds,
        ]);
        ($this->sleep)($milliseconds);
    }
}

Pokušavaju se ponovno samo transportne pogreške, 429 i pogreške poslužitelja. Provjera valjanosti, autentikacija i druge pogreške klijenta odmah ne uspijevaju. Maksimalno kašnjenje ponovnog pokušaja ograničeno je, čime se sprječava da nepouzdana vrijednost Retry-After neograničeno zauzme PHP radnik.

Pohranite uvoz na čekanju

Repozitorij pohranjuje potpuni normalizirani objekt jer se ovdje ne pretpostavljaju nazivi njegovih polja. Hash zadržava korelacijsku vrijednost bez dupliciranja poslanog korisničkog imena ili URL-a.

<?php
// src/ImportRepository.php
namespace App;

use PDO;
use RuntimeException;

final class ImportRepository
{
    public function __construct(private readonly PDO $pdo)
    {
        $this->pdo->exec(
            'CREATE TABLE IF NOT EXISTS profile_imports (
                id TEXT PRIMARY KEY,
                platform TEXT NOT NULL,
                reference_type TEXT NOT NULL,
                reference_hash TEXT NOT NULL,
                normalized_json TEXT NOT NULL,
                status TEXT NOT NULL CHECK (
                    status IN ("pending_review", "approved", "rejected")
                ),
                reviewer TEXT,
                created_at TEXT NOT NULL,
                reviewed_at TEXT
            )'
        );
    }

    public function create(
        string $platform,
        string $type,
        string $reference,
        NormalizedIdentity $identity,
    ): string {
        $id = bin2hex(random_bytes(16));
        $statement = $this->pdo->prepare(
            'INSERT INTO profile_imports
             (id, platform, reference_type, reference_hash,
              normalized_json, status, created_at)
             VALUES (?, ?, ?, ?, ?, "pending_review", ?)'
        );
        $statement->execute([
            $id,
            $platform,
            $type,
            hash('sha256', $reference),
            json_encode($identity->data, JSON_THROW_ON_ERROR),
            gmdate('c'),
        ]);

        return $id;
    }

    public function find(string $id): ?array
    {
        $statement = $this->pdo->prepare(
            'SELECT * FROM profile_imports WHERE id = ?'
        );
        $statement->execute([$id]);
        return $statement->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    public function review(string $id, string $decision, string $reviewer): void
    {
        if (!in_array($decision, ['approved', 'rejected'], true)
            || trim($reviewer) === '') {
            throw new RuntimeException('Invalid review decision');
        }

        $statement = $this->pdo->prepare(
            'UPDATE profile_imports
             SET status = ?, reviewer = ?, reviewed_at = ?
             WHERE id = ? AND status = "pending_review"'
        );
        $statement->execute([$decision, $reviewer, gmdate('c'), $id]);

        if ($statement->rowCount() !== 1) {
            throw new RuntimeException('Import missing or already reviewed');
        }
    }
}

Izložite krajnju točku za uvođenje

Kontroler prihvaća JSON i vraća javni identifikator tijeka rada, a ne uzvodni odgovor. Postojeća aplikacija trebala bi taj identifikator povezati sa svojom autentificiranom sesijom uvođenja i provesti CSRF zaštitu kada su uključeni kolačići preglednika.

<?php
// public/index.php
use App\CurlTransport;
use App\IdentityResolver;
use App\ImportRepository;
use App\ResolverFailure;

require dirname(__DIR__) . '/vendor/autoload.php';

header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] !== 'POST'
    || parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
        !== '/onboarding/profile-import') {
    http_response_code(404);
    echo json_encode(['error' => 'not_found']);
    exit;
}

try {
    $input = json_decode(
        file_get_contents('php://input'),
        true,
        32,
        JSON_THROW_ON_ERROR,
    );

    if (!is_array($input)) {
        throw new JsonException();
    }

    $logger = static function (string $event, array $context): void {
        error_log(json_encode(
            ['event' => $event, 'context' => $context],
            JSON_THROW_ON_ERROR,
        ));
    };

    $resolver = new IdentityResolver(
        new CurlTransport(),
        getenv('IDENTITY_RESOLVER_ENDPOINT')
            ?: 'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve',
        static fn (int $milliseconds) => usleep($milliseconds * 1000),
        $logger,
    );

    $identity = $resolver->resolve(
        (string) ($input['platform'] ?? ''),
        (string) ($input['reference_type'] ?? ''),
        (string) ($input['reference'] ?? ''),
    );

    $database = getenv('DATABASE_PATH') ?: dirname(__DIR__) . '/var/app.sqlite';
    $pdo = new PDO('sqlite:' . $database, null, null, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]);
    $repository = new ImportRepository($pdo);
    $id = $repository->create(
        strtolower(trim((string) $input['platform'])),
        (string) $input['reference_type'],
        (string) $input['reference'],
        $identity,
    );

    http_response_code(202);
    echo json_encode(['import_id' => $id, 'status' => 'pending_review']);
} catch (ResolverFailure $failure) {
    $status = $failure->reason === 'invalid_input'
        || $failure->reason === 'reference_rejected' ? 422 : 502;
    http_response_code($status);
    echo json_encode([
        'error' => $failure->reason,
        'retryable' => $failure->retryable,
    ]);
} catch (JsonException) {
    http_response_code(400);
    echo json_encode(['error' => 'invalid_json']);
} catch (Throwable $error) {
    error_log(json_encode(['event' => 'profile_import_failed']));
    http_response_code(500);
    echo json_encode(['error' => 'internal_error']);
}

Produkcijski kod trebao bi primijeniti i ograničenje zahtjeva po korisniku. Lokalna bi se baza podataka inače mogla popuniti čak i ako uzvodna usluga uspješno obradi svaki zahtjev.

Učinite ručni pregled izričitim

Naredba za pregled najprije podržava inspekciju, a zatim zasebnu odluku. Pristup operacijskom sustavu ili spremniku postaje granica autorizacije, stoga ovu skriptu nemojte izlagati putem web poslužitelja.

<?php
// bin/review-import.php
use App\ImportRepository;

require dirname(__DIR__) . '/vendor/autoload.php';

[$script, $action, $id, $reviewer] = array_pad($argv, 4, null);

$database = getenv('DATABASE_PATH') ?: dirname(__DIR__) . '/var/app.sqlite';
$pdo = new PDO('sqlite:' . $database, null, null, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$repository = new ImportRepository($pdo);

if ($action === 'show' && $id !== null) {
    $record = $repository->find($id);
    if ($record === null) {
        fwrite(STDERR, "Import not found\n");
        exit(1);
    }

    echo json_encode([
        'id' => $record['id'],
        'platform' => $record['platform'],
        'status' => $record['status'],
        'identity' => json_decode(
            $record['normalized_json'],
            true,
            512,
            JSON_THROW_ON_ERROR,
        ),
    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL;
    exit;
}

if (in_array($action, ['approved', 'rejected'], true)
    && $id !== null
    && $reviewer !== null) {
    $repository->review($id, $action, $reviewer);
    echo "Review recorded\n";
    exit;
}

fwrite(
    STDERR,
    "Usage: php bin/review-import.php show ID\n"
    . "   or: php bin/review-import.php approved|rejected ID REVIEWER\n"
);
exit(2);

Najprije pregledajte, usporedite normalizirani javni identitet s informacijama dostavljenima tijekom uvođenja i zabilježite odluku:

php bin/review-import.php show IMPORT_ID
php bin/review-import.php approved IMPORT_ID [email protected]

Zasebna aplikacijska usluga može reagirati na odobrenje i selektivno kopirati pregledane atribute. Neka ta operacija bude idempotentna i zabilježite koje su vrijednosti došle iz uvoza. Odbijanje treba ostaviti ručno unesene podatke korisnika netaknutima.

Testirajte ponovne pokušaje i provjeru granica

Deterministička zamjena dokazuje politiku ponovnih pokušaja bez mirovanja ili oslanjanja na aktivnu uslugu:

<?php
// tests/IdentityResolverTest.php
use App\HttpResponse;
use App\IdentityResolver;
use App\ResolverFailure;
use App\Transport;
use PHPUnit\Framework\TestCase;

final class FakeTransport implements Transport
{
    public array $urls = [];

    public function __construct(private array $responses) {}

    public function get(string $url, int $connectTimeout, int $timeout): HttpResponse
    {
        $this->urls[] = $url;
        $next = array_shift($this->responses);
        if ($next instanceof Throwable) {
            throw $next;
        }
        return $next;
    }
}

final class IdentityResolverTest extends TestCase
{
    private function resolver(FakeTransport $transport): IdentityResolver
    {
        return new IdentityResolver(
            $transport,
            'https://ai.mihajlo.mk/api/identity-resolver/v1/resolve',
            static fn (int $milliseconds) => null,
            static fn (string $event, array $context) => null,
        );
    }

    public function testMapsAnObjectWithoutAssumingItsFields(): void
    {
        $fake = new FakeTransport([
            new HttpResponse(200, '{"fixture_value":true}'),
        ]);

        $identity = $this->resolver($fake)
            ->resolve('instagram', 'username', 'example');

        self::assertTrue($identity->data['fixture_value']);
        self::assertStringContainsString(
            'username=example',
            $fake->urls[0],
        );
    }

    public function testRetriesRateLimitThenSucceeds(): void
    {
        $fake = new FakeTransport([
            new HttpResponse(429, '', 1),
            new HttpResponse(200, '{"fixture_value":true}'),
        ]);

        $this->resolver($fake)
            ->resolve('linkedin', 'url', 'https://example.test/profile');

        self::assertCount(2, $fake->urls);
    }

    public function testDoesNotRetryValidationFailure(): void
    {
        $fake = new FakeTransport([new HttpResponse(400, '{}')]);

        try {
            $this->resolver($fake)
                ->resolve('facebook', 'id', '123');
            self::fail('Expected ResolverFailure');
        } catch (ResolverFailure $failure) {
            self::assertSame('reference_rejected', $failure->reason);
            self::assertFalse($failure->retryable);
            self::assertCount(1, $fake->urls);
        }
    }

    public function testRejectsNonObjectJson(): void
    {
        $fake = new FakeTransport([new HttpResponse(200, '[]')]);

        $this->expectException(ResolverFailure::class);
        $this->resolver($fake)
            ->resolve('instagram', 'identifier', 'example');
    }
}
vendor/bin/phpunit tests

Sigurnost, vidljivost i implementacija

Javno ne znači bez posljedica. Normalizirani odgovor tretirajte kao osobni podatak: definirajte zadržavanje, ograničite pristup pregledavatelja, šifrirajte pohranu gdje je prikladno i omogućite ponašanje pri brisanju. Prije razrješavanja nečijeg profila pribavite valjanu svrhu uvođenja. Nikada nemojte bilježiti poslanu referencu, vraćeno tijelo, kolačiće ni buduće vjerodajnice.

Korisni strukturirani zapisi uključuju događaj, broj pokušaja, kategoriju uzvodnog statusa, ograničeno kašnjenje, konačni razlog i interni identifikator uvoza. Dodajte brojače za uspjehe, odbijanja provjere valjanosti, ograničenja stope, pogreške poslužitelja i ishode pregleda. Upozoravajte na trajne omjere neuspjeha, a ne na pojedinačni istek vremena.

Implementirajte iza pravog web poslužitelja s TLS-om, ograničenjima veličine zahtjeva, autentificiranim sesijama uvođenja, CSRF zaštitom i ograničavanjem na razini aplikacije. Osigurajte da PHP radnik može pisati samo u predviđeni SQLite direktorij. Izradu baze podataka ili migracije pokrenite tijekom implementacije, a ne istodobno među mnogim radnicima. Krajnja točka za provjeru stanja trebala bi testirati aplikaciju i bazu podataka bez pozivanja vanjskog razrješivača pri svakoj provjeri.

Česti neuspjesi

  • Neočekivani 401 ili 403: nemojte nastaviti s ponovnim pokušajima. Javna krajnja točka trenutačno ne treba token, stoga provjerite dokumentirani ugovor, krajnju točku, ponašanje proxyja i svako slučajno ubrizgano autorizacijsko zaglavlje.
  • Ponovljeni odgovori 429: sačuvajte stanje uvođenja na čekanju, poštujte ograničeno odgađanje i zamolite korisnika da pokuša ponovno kasnije umjesto stvaranja neograničene petlje zahtjeva.
  • Valjan JSON neočekivanog oblika: klasificirajte ga kao malformed_response. Nikada ne nagađajte nazive polja niti djelomično odobravajte uvoz.
  • Duplicirani pokušaji pregleda: uvjetno ažuriranje odbija drugu odluku, sprječavajući da odobrenje tiho prepiše odbijanje.
  • Zaključavanje SQLitea pod opterećenjem: skratite transakcije ili premjestite repozitorij u dijeljenu bazu podataka prije dodavanja više web instanci.

Završni kontrolni popis provjere

  • Zahtjev koristi točno GET https://ai.mihajlo.mk/api/identity-resolver/v1/resolve.
  • Za trenutačnu javnu krajnju točku ne šalje se token ni API ključ.
  • platform i točno jedan podržani parametar reference kodirani su za URL.
  • Vremena povezivanja i ukupna vremena ograničena su.
  • Ponovno se pokušavaju samo transportni neuspjesi, ograničenja stope i neuspjesi poslužitelja.
  • Nepoznata polja odgovora čuvaju se, dok se neispravni podaci najviše razine odbijaju.
  • Uvezeni identitet ostaje pending_review dok ovlaštena osoba ne poduzme radnju.
  • Sirove reference i normalizirani odgovori nisu prisutni u operativnim zapisima.
  • PHPUnit paket s lažnim transportom prolazi bez mrežnog pristupa.
  • Odobravanje je idempotentno na granici baze podataka i ne prepisuje automatski korisničke podatke.

Važna inženjerska odluka nije cURL poziv. To je odbijanje da se uspješno razrješavanje zamijeni za pouzdano stanje aplikacije. Kada uvezeni podaci identiteta prijeđu obrambenu granicu, uđu u vidljivo stanje na čekanju i zasluže ljudsko odobrenje prije izmjene računa, uvođenje postaje brže bez nepromišljenosti.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.