Vodiči

Symfony Email Validation: Keep Registrations Alive During API Downtime

Symfony validacija e-pošte: Održavajte registracije aktivnima tijekom prekida rada API-ja

Obrazac za registraciju ima jedan zadatak: omogućiti legitimnim osobama stvaranje računa. Provjera rizika e-pošte može poboljšati taj tijek, ali samo ako integracija poštuje ključnu granicu: privremeni kvar ovisnosti nije dokaz da je korisnik unio neispravnu adresu.

Ovaj vodič izrađuje produkcijski usmjerenu Symfony integraciju koja provjerava adresu prije registracije, odbija samo nedvojben rezultat s niskim rezultatom te dopušta registraciju sa stanjem validacije na čekanju kada udaljena usluga nije dostupna. Lokalna provjera sintakse ostaje obavezna, dok potvrda vlasništva ostaje konačni autoritet.

Dobijte pristup i kopirajte token usluge

Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.

  1. Otvorite stranicu usluge Email Validator.
  2. Odaberite dostupni plan Free, Plus ili Pro i dovršite njegovu aktivaciju.
  3. Otvorite službenu dokumentaciju za Email Validator.
  4. Pronađite ploču Service token i kopirajte token ograničen na uslugu.
  5. Pohranite ga u konfiguraciju aplikacije temeljenu na varijablama okruženja, nikada u PHP izvorni kod.

Ova usluga zahtijeva token. Ponovno generiranje tokena opoziva prethodno aktivni token, stoga rotacija tokena mora ažurirati svako implementirano okruženje koje ga koristi.

Potvrdite točan zahtjev

API poziv je HTTP GET zahtjev prema https://ai.mihajlo.mk/api/email-validator/v1/check-email. Autentikacija koristi parametar upita token={serviceToken}, dok se adresa šalje putem parametra upita email.

Pokrenite jedan minimalni zahtjev s tokenom rezerviranog mjesta prije pisanja koda aplikacije:

curl --get 'https://ai.mihajlo.mk/api/email-validator/v1/check-email' \
  --data-urlencode 'token=YOUR_SERVICE_TOKEN' \
  --data-urlencode '[email protected]'

Ugovor odgovora pruža status, score, recommendation, checks i quota. Usluga procjenjuje sintaksu, podatke o domeni, MX zapise, signale pružatelja usluge i praktični rizik isporuke. Budući da podaci odgovora prelaze mrežnu granicu, aplikacija će i dalje validirati svako polje prije nego što mu povjeruje.

Lokalno pohranite vjerodajnicu

Za lokalnu Symfony instalaciju upotrijebite .env.local. Obično je isključen iz kontrole verzija. U hostiranim okruženjima umjesto toga unesite ove vrijednosti putem mehanizma platforme za tajne podatke ili varijable okruženja.

EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_VALIDATOR_REJECT_BELOW=40

Prag je pravilo aplikacije, a ne API jamstvo ni univerzalna mjera isporučivosti. Kalibrirajte ga prema trenutačnoj dokumentaciji i reprezentativnim rezultatima prije omogućavanja odbijanja u produkciji.

Arhitektura: dopustite prolaz bez nemara

Integracija ima tri granice. API klijent obavlja prijenos i mapiranje odgovora. Pravilo domene pretvara valjanu procjenu u accept, reject ili defer. Kontroler registracije provodi lokalnu validaciju, a zatim primjenjuje tu odluku.

  • Prihvati: usluga je vratila potpunu uspješnu procjenu iznad konfiguriranog praga.
  • Odbij: usluga je vratila potpunu uspješnu procjenu ispod praga.
  • Odgodi: zahtjev je istekao, interveniralo je ograničenje kvote ili stope, autentikacija nije uspjela, poslužitelj je vratio pogrešku ili je sadržaj bio nepotpun.

Odgođena registracija se nastavlja, ali račun bilježi stanje validacije e-pošte na čekanju. Ta je razlika važna: ponašanje koje dopušta prolaz pri kvaru čuva dostupnost, dok stanje na čekanju čuva operativnu vidljivost. Uobičajena potvrda vlasništva nad e-poštom i dalje bi trebala biti obavezna prije osjetljivih radnji.

Relevantna struktura projekta namjerno je mala:

src/
  Controller/RegistrationController.php
  EmailValidation/EmailAssessment.php
  EmailValidation/EmailGate.php
  EmailValidation/EmailValidatorClient.php
tests/
  EmailValidation/EmailValidatorClientTest.php
config/
  packages/framework.yaml
  services.yaml

Konfigurirajte Symfonyjev HTTP klijent

Ova implementacija cilja PHP 8.3 ili noviji te održavanu Symfony aplikaciju s izvornim komponentama HttpClient i Validator. Nedostajuće komponente instalirajte pomoću:

composer require symfony/http-client symfony/validator
composer require --dev symfony/test-pack

Konfigurirajte klijenta ograničenog na opseg s ograničenim vremenskim ograničenjima i usko ciljanim ponovnim pokušajima. Zahtjev za registraciju ne bi trebao neograničeno čekati na neobavezan signal rizika.

# config/packages/framework.yaml
framework:
  http_client:
    scoped_clients:
      email_validator.client:
        base_uri: 'https://ai.mihajlo.mk'
        timeout: 2.5
        max_duration: 6.0
        retry_failed:
          max_retries: 2
          delay: 200
          multiplier: 2
          max_delay: 1000
          jitter: 0.1
          http_codes: [429, 500, 502, 503, 504]

Pokušava se ponovno samo za prolazna stanja. Neuspjesi autentikacije i druge pogreške klijenta ne pokušavaju se ponovno naslijepo. Dva ograničena ponovna pokušaja povećavaju otpornost bez pretvaranja spore ovisnosti u dug prekid registracije.

Izričito povežite klijenta ograničenog na opseg i vrijednosti okruženja:

# config/services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true

  App\:
    resource: '../src/'

  App\EmailValidation\EmailValidatorClient:
    arguments:
      $http: '@email_validator.client'
      $serviceToken: '%env(string:EMAIL_VALIDATOR_TOKEN)%'

  App\EmailValidation\EmailGate:
    arguments:
      $rejectBelow: '%env(int:EMAIL_VALIDATOR_REJECT_BELOW)%'

Mapirajte udaljeni odgovor u objekt domene

Nemojte prosljeđivati dekodirano API polje kroz aplikaciju. Mali nepromjenjivi objekt izričito prikazuje neispravna i nedostupna stanja.

<?php
// src/EmailValidation/EmailAssessment.php
namespace App\EmailValidation;

final readonly class EmailAssessment
{
    private function __construct(
        public bool $available,
        public ?string $status,
        public int|float|null $score,
        public ?string $recommendation,
        public ?array $checks,
        public ?array $quota,
        public ?string $failure,
    ) {}

    public static function fromPayload(array $payload): self
    {
        $status = $payload['status'] ?? null;
        $score = $payload['score'] ?? null;
        $recommendation = $payload['recommendation'] ?? null;
        $checks = $payload['checks'] ?? null;
        $quota = $payload['quota'] ?? null;

        $valid = is_string($status)
            && $status !== ''
            && (is_int($score) || is_float($score))
            && is_string($recommendation)
            && $recommendation !== ''
            && is_array($checks)
            && is_array($quota);

        if (!$valid) {
            return self::unavailable('malformed_response');
        }

        return new self(
            true,
            $status,
            $score,
            $recommendation,
            $checks,
            $quota,
            null,
        );
    }

    public static function unavailable(string $failure): self
    {
        return new self(false, null, null, null, null, null, $failure);
    }
}

Mapper prihvaća samo dokumentirana polja najviše razine i njihove opravdano široke tipove. Ne nagađa o nedokumentiranim ključevima unutar checks ili quota. Obje strukture moraju biti prisutne prije nego što procjena može utjecati na odbijanje.

Izradite otporan API klijent

<?php
// src/EmailValidation/EmailValidatorClient.php
namespace App\EmailValidation;

use JsonException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final readonly class EmailValidatorClient
{
    public function __construct(
        private HttpClientInterface $http,
        private LoggerInterface $logger,
        private string $serviceToken,
    ) {}

    public function check(string $email): EmailAssessment
    {
        try {
            $response = $this->http->request('GET',
                '/api/email-validator/v1/check-email',
                ['query' => [
                    'token' => $this->serviceToken,
                    'email' => $email,
                ]]
            );

            $code = $response->getStatusCode();

            if ($code === 429) {
                return $this->failure('quota_or_rate_limit', $code);
            }

            if ($code === 401 || $code === 403) {
                return $this->failure('authentication_failure', $code);
            }

            if ($code !== 200) {
                return $this->failure('upstream_http_error', $code);
            }

            $payload = json_decode(
                $response->getContent(false),
                true,
                512,
                JSON_THROW_ON_ERROR
            );

            if (!is_array($payload)) {
                return $this->failure('malformed_response', $code);
            }

            $assessment = EmailAssessment::fromPayload($payload);

            if (!$assessment->available) {
                $this->logger->warning('Email validator response rejected', [
                    'reason' => $assessment->failure,
                ]);
            }

            return $assessment;
        } catch (TransportExceptionInterface $exception) {
            return $this->failure('transport_failure');
        } catch (JsonException $exception) {
            return $this->failure('invalid_json');
        }
    }

    private function failure(
        string $reason,
        ?int $httpStatus = null,
    ): EmailAssessment {
        $this->logger->warning('Email validator unavailable', [
            'reason' => $reason,
            'http_status' => $httpStatus,
        ]);

        return EmailAssessment::unavailable($reason);
    }
}

Primijetite što zapisnik izostavlja: adresu e-pošte, token, puni URL, tijelo odgovora i vrijednosti kvote. Token se mora pojaviti u upitu jer je to ugovor autentikacije, ali nikada se ne smije kopirati u zapisnike aplikacije ni izvještaje o pogreškama.

Pretvorite procjenu u odluku aplikacije

<?php
// src/EmailValidation/EmailGate.php
namespace App\EmailValidation;

enum EmailGateDecision: string
{
    case Accept = 'accept';
    case Reject = 'reject';
    case Defer = 'defer';
}

final readonly class EmailGate
{
    public function __construct(private int $rejectBelow) {}

    public function decide(EmailAssessment $assessment): EmailGateDecision
    {
        if (
            !$assessment->available
            || strtolower($assessment->status ?? '') !== 'success'
            || $assessment->score === null
            || $assessment->recommendation === null
            || $assessment->checks === null
            || $assessment->quota === null
        ) {
            return EmailGateDecision::Defer;
        }

        return $assessment->score < $this->rejectBelow
            ? EmailGateDecision::Reject
            : EmailGateDecision::Accept;
    }
}

Svih pet područja odgovora sudjeluje u odluci. Preporuka, provjere i kvota moraju biti prisutne; status mora naznačiti uspjeh; i tek tada rezultat može prijeći prag odbijanja aplikacije. Svaka nejasnoća postaje defer, nikada reject.

Zaštitite kontroler registracije

Sljedeći kontroler prikazuje točku integracije. AccountCreator predstavlja postojeću transakcijsku uslugu aplikacije za stvaranje računa; trebao bi raspršiti lozinku, provesti ograničenje jedinstvene e-pošte, pohraniti stanje validacije i poslati uobičajenu poruku za potvrdu vlasništva.

<?php
// src/Controller/RegistrationController.php
namespace App\Controller;

use App\EmailValidation\EmailGate;
use App\EmailValidation\EmailGateDecision;
use App\EmailValidation\EmailValidatorClient;
use App\Registration\AccountCreator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class RegistrationController extends AbstractController
{
    #[Route('/register', name: 'register', methods: ['POST'])]
    public function __invoke(
        Request $request,
        ValidatorInterface $validator,
        EmailValidatorClient $client,
        EmailGate $gate,
        AccountCreator $accounts,
    ): JsonResponse {
        if (!$this->isCsrfTokenValid(
            'register',
            (string) $request->request->get('_token')
        )) {
            return $this->json(['error' => 'Invalid form token.'], 400);
        }

        $email = trim((string) $request->request->get('email'));
        $password = (string) $request->request->get('password');

        $violations = $validator->validate($email, [
            new Assert\NotBlank(),
            new Assert\Email(),
        ]);

        if (count($violations) > 0 || strlen($password) < 12) {
            return $this->json(['error' => 'Check the submitted fields.'], 422);
        }

        $decision = $gate->decide($client->check($email));

        if ($decision === EmailGateDecision::Reject) {
            return $this->json([
                'error' => 'Please check the email address.',
            ], 422);
        }

        $state = $decision === EmailGateDecision::Defer
            ? 'pending'
            : 'checked';

        $accounts->createPending($email, $password, $state);

        return $this->json([
            'message' => 'Check your inbox to finish registration.',
        ], 201);
    }
}

Preglednik ne prima dijagnozu pružatelja usluge, rezultat ni internu preporuku. Detaljna objašnjenja odbijanja olakšavaju enumeraciju računa i ispitivanje pravila. Isti generički odgovor treba upotrijebiti i kada e-pošta već postoji.

Deterministički testirajte uspjeh i prekid rada

Symfonyjev MockHttpClient provjerava zahtjev bez kontaktiranja stvarne usluge. Ove testne vrijednosti provjeravaju navedeni ugovor; nisu predstavljene kao primjeri stvarne usluge.

<?php
// tests/EmailValidation/EmailValidatorClientTest.php
namespace App\Tests\EmailValidation;

use App\EmailValidation\EmailValidatorClient;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;

final class EmailValidatorClientTest extends TestCase
{
    public function testMapsACompleteResponse(): void
    {
        $response = new MockResponse(json_encode([
            'status' => 'success',
            'score' => 91,
            'recommendation' => 'fixture-recommendation',
            'checks' => ['fixture' => true],
            'quota' => ['fixture' => 1],
        ], JSON_THROW_ON_ERROR), ['http_code' => 200]);

        $http = new MockHttpClient(function (
            string $method,
            string $url,
            array $options,
        ) use ($response): MockResponse {
            self::assertSame('GET', $method);
            self::assertStringContainsString(
                '/api/email-validator/v1/check-email',
                $url
            );
            self::assertSame('[email protected]',
                $options['query']['email']);
            self::assertSame('test-token',
                $options['query']['token']);

            return $response;
        });

        $result = (new EmailValidatorClient(
            $http,
            new NullLogger(),
            'test-token',
        ))->check('[email protected]');

        self::assertTrue($result->available);
        self::assertSame(91, $result->score);
    }

    public function testServiceFailureBecomesUnavailable(): void
    {
        $http = new MockHttpClient(
            new MockResponse('', ['http_code' => 503])
        );

        $result = (new EmailValidatorClient(
            $http,
            new NullLogger(),
            'test-token',
        ))->check('[email protected]');

        self::assertFalse($result->available);
        self::assertSame('upstream_http_error', $result->failure);
    }
}

Dodajte testove pravila za rezultate s obje strane vašeg praga, kao i za neispravan JSON, polja ugovora koja nedostaju, HTTP 429, neuspjehe autentikacije i ponašanje kontrolera koje potvrđuje da defer i dalje poziva createPending.

Sigurnost, vidljivost i implementacija

Zadržite lokalnu provjeru sintakse iako vanjska usluga provjerava sintaksu. Brza je, deterministička i sprječava da očito neispravan unos troši kvotu. Zadržite i potvrdu vlasništva: procjena rizika i kontrola poštanskog sandučića odgovaraju na različita pitanja.

Produkcijske nadzorne ploče trebale bi pratiti broj odluka, razloge neuspjeha, uzvodne statusne kodove i latenciju zahtjeva. Upozorite na trajne neuspjehe autentikacije, nagli porast odgođenih odluka ili ponovljene odgovore o kvoti i ograničenju stope. Upotrebljavajte brojače umjesto zapisivanja osobnih podataka.

Prije implementacije postavite stvarni token u upravitelj tajni, odaberite konzervativan prag, pokrenite automatizirani paket i potvrdite da je Symfonyjev produkcijski profiler onemogućen. Tijekom rotacije implementirajte korisnike tokena prije opoziva starog tokena; ponovno generiranje tokena usluge odmah poništava prethodnu aktivnu vrijednost.

Uobičajeni načini kvara

  • Svaka adresa postaje na čekanju: provjerite aktivaciju plana, unošenje tokena, mapiranje odgovora i dokumentiranu uspješnu vrijednost status.
  • HTTP 401 ili 403: potvrdite da je token ograničen na uslugu trenutačan i da nije opozvan ponovnim generiranjem.
  • HTTP 429: provjerite kvotu plana i promet. Nemojte agresivno povećavati ponovne pokušaje; registracija bi se trebala odgoditi.
  • Neočekivana odbijanja: onemogućite strogo odbijanje smanjivanjem agresivnosti pravila, provjerite zbirne podatke o odlukama i ponovno kalibrirajte prag.
  • Spori obrasci: potvrdite obje granice vremenskog ograničenja i izmjerite ukupnu latenciju ponovnih pokušaja iz implementirane mreže.

Završni kontrolni popis za provjeru

  • Točna krajnja točka GET prima samo obavezne parametre upita email i token.
  • Token dolazi iz konfiguracije temeljene na varijablama okruženja i nikada se ne pojavljuje u zapisnicima ni testnim vrijednostima.
  • Lokalna provjera sintakse izvodi se prije zahtjeva koji troši kvotu.
  • Samo potpuni uspješni odgovori mogu odbiti registraciju.
  • Istek vremena, neispravni odgovori, ograničenja kvote, neuspjesi autentikacije i pogreške poslužitelja stvaraju stanje na čekanju.
  • Ponašanje ponovnih pokušaja ograničeno je i svodi se na prolazna stanja.
  • Registracija i dalje uspijeva kada API vrati 503.
  • Potvrda vlasništva nad e-poštom ostaje obavezna.
  • Metrike izlažu stanje ovisnosti bez bilježenja adresa ili tokena.

Trajna pouka šira je od validacije e-pošte: neobavezna usluga rizika trebala bi usavršiti odluku, a ne tiho postati prekidač dostupnosti za svakodnevnu funkciju. Potvrđene dokaze tretirajte kao dokaze, prekid rada kao neizvjesnost i ostavite vrata registracije otvorenima kada mreža ne može odgovoriti.

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.