Туториали

Symfony Email Validation: Keep Registrations Alive During API Downtime

Symfony валидација на е-пошта: Одржете ги регистрациите активни при прекин на API-то

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

Овој туторијал гради интеграција за Symfony ориентирана кон продукција, која проверува адреса пред регистрација, одбива само конечен резултат со низок резултат и овозможува регистрација со состојба на валидација на чекање кога оддалечената услуга не е достапна. Локалната валидација на синтаксата останува задолжителна, додека потврдата на сопственоста останува конечниот авторитет.

Добијте пристап и копирајте го токенот на услугата

Регистрирајте се на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате сметка.

  1. Отворете ја страницата на услугата Email Validator.
  2. Изберете го достапниот Free, Plus или Pro план и завршете ја неговата активација.
  3. Отворете ја официјалната документација за Email Validator.
  4. Најдете го панелот Service token и копирајте го токенот со опфат на услугата.
  5. Чувајте го во конфигурацијата на апликацијата поддржана од околината, никогаш во PHP изворен код.

Оваа услуга бара токен. Повторното генерирање на токенот го отповикува претходно активниот токен, па ротирањето на токенот мора да ја ажурира секоја распоредена околина што го користи.

Потврдете го точниот повик

API повикот е HTTP GET барање до https://ai.mihajlo.mk/api/email-validator/v1/check-email. Автентикацијата го користи параметарот за барање token={serviceToken}, додека адресата се доставува преку параметарот за барање email.

Извршете едно минимално барање со заменски токен пред да пишувате код за апликацијата:

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

Договорот за одговор обезбедува status, score, recommendation, checks и quota. Услугата ги оценува синтаксата, информациите за доменот, MX записите, сигналите од давателот и практичниот ризик за испорака. Бидејќи податоците од одговорот преминуваат мрежна граница, апликацијата сепак ќе го валидира секое поле пред да му верува.

Чувајте ги акредитивите локално

Користете .env.local за локална Symfony инсталација. Вообичаено е исклучена од контрола на верзии. Наместо тоа, во хостирани околини внесете ги овие вредности преку механизмот на платформата за тајни или променливи на околината.

EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_VALIDATOR_REJECT_BELOW=40

Прагот е политика на апликацијата, а не API гаранција или универзална мерка за испорачливост. Калибрирајте го според тековната документација и репрезентативни резултати пред да овозможите одбивање во продукција.

Архитектура: отворен неуспех без невнимателност

Интеграцијата има три граници. API клиент врши транспорт и мапирање на одговорот. Доменска политика претвора валидна проценка во accept, reject или defer. Контролерот за регистрација врши локална валидација и потоа ја применува таа одлука.

  • Прифати: услугата вратила целосна успешна проценка над конфигурираниот праг.
  • Одбиј: услугата вратила целосна успешна проценка под прагот.
  • Одложи: барањето истекло, интервенирало ограничување на квота или стапка, автентикацијата не успеала, серверот вратил грешка или товарот бил нецелосен.

Одложената регистрација продолжува, но сметката бележи состојба на валидација на е-пошта на чекање. Таа разлика е важна: однесувањето со отворен неуспех ја зачувува достапноста, додека состојбата на чекање ја зачувува оперативната видливост. Нормалната потврда на сопственоста на е-поштата сè уште треба да биде задолжителна пред чувствителни дејства.

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

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

Конфигурирајте го HTTP клиентот на Symfony

Оваа имплементација е наменета за PHP 8.3 или понов и одржувана Symfony апликација со првостепените компоненти HttpClient и Validator. Инсталирајте ги компонентите што недостигаат со:

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

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

# 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]

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

Експлицитно поврзете го клиентот со ограничен опсег и вредностите од околината:

# 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)%'

Мапирајте го оддалечениот одговор во доменски објект

Не пренесувајте декодирана API низа низ апликацијата. Мал непроменлив објект ги прави експлицитни неисправните и недостапните состојби.

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

Маперот ги прифаќа само документираните полиња од највисоко ниво и нивните одбранливи широки типови. Не погодува недокументирани клучеви во checks или quota. Двете структури мора да бидат присутни пред проценката да може да влијае на одбивањето.

Изградете отпорен API клиент

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

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

Претворете ја проценката во одлука на апликацијата

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

Сите пет области на одговорот учествуваат во одлуката. Препораката, проверките и квотата мора да бидат присутни; статусот мора да означува успех; и само тогаш резултатот може да го премине прагот за одбивање на апликацијата. Секоја двосмисленост станува defer, никогаш reject.

Заштитете го контролерот за регистрација

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

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

Прелистувачот не добива дијагноза од давателот, резултат или внатрешна препорака. Деталните објаснувања за одбивање го олеснуваат набројувањето на сметки и испитувањето на правила. Истиот генерички одговор треба да се користи и кога е-поштата веќе постои.

Тестирајте успех и недостапност детерминистички

MockHttpClient на Symfony го проверува барањето без контактирање на вистинската услуга. Овие фикстури го вежбаат дадениот договор; не се претставени како примери од активна услуга.

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

Додадете тестови за политика за резултати од двете страни на вашиот праг, како и за неисправен JSON, полиња што недостигаат од договорот, HTTP 429, неуспеси на автентикацијата и однесување на контролерот што потврдува дека defer сè уште повикува createPending.

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

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

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

Пред распоредување, поставете го вистинскиот токен во управувачот со тајни, изберете конзервативен праг, извршете го автоматизираниот пакет и потврдете дека продукцискиот профајлер на Symfony е оневозможен. Распоредете ги потрошувачите на токенот пред да отповикате стар токен при ротација; повторното генерирање на токенот на услугата веднаш ја поништува претходната активна вредност.

Чести начини на неуспех

  • Секоја адреса станува на чекање: проверете ја активацијата на планот, внесувањето на токенот, мапирањето на одговорот и документираната успешна вредност на status.
  • HTTP 401 или 403: потврдете дека токенот со опфат на услугата е тековен и не бил отповикан со повторно генерирање.
  • HTTP 429: проверете ја квотата на планот и сообраќајот. Не ги зголемувајте агресивно повторните обиди; регистрацијата треба да се одложи.
  • Неочекувани одбивања: оневозможете строго одбивање со намалување на агресивноста на политиката, проверете ги збирните податоци за одлуки и повторно калибрирајте го прагот.
  • Бавни формулари: потврдете ги двете граници на временско ограничување и измерете ја вкупната латентност на повторниот обид од распоредената мрежа.

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

  • Точната GET крајна точка ги прима само задолжителните параметри за барање email и token.
  • Токенот доаѓа од конфигурација поддржана од околината и никогаш не се појавува во дневници или фикстури.
  • Локалната валидација на синтаксата се извршува пред барањето што троши квота.
  • Само целосните успешни одговори можат да одбијат регистрација.
  • Временските ограничувања, неисправните одговори, ограничувањата на квотата, неуспесите на автентикацијата и серверските грешки создаваат состојба на чекање.
  • Однесувањето на повторните обиди е ограничено и ограничено на привремени статуси.
  • Регистрацијата сè уште успева кога API враќа 503.
  • Потврдата на сопственоста на е-поштата останува задолжителна.
  • Метриките ја откриваат здравствената состојба на зависноста без да бележат адреси или токени.

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

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

Mihajlo

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