Нативен PHP 8.3: Валидирајте ги увозите на билтени и ставете ги несигурните е-пошта во редица за преглед
Увозот на билтен изгледа безопасно сè додека не содржи погрешно напишани домени, еднократни адреси, дупликат контакти и двосмислени сандачиња. Увезете сè и репутацијата на испраќачот трпи; отфрлете премногу агресивно и легитимните претплатници исчезнуваат. Практичниот одговор е конзервативен процес: нормализирајте локално, валидирајте оддалечено, прифатете само силни резултати и ставете го секој неизвесен случај во редица за преглед.
Овој туторијал го гради тој процес како Native PHP 8.3 команда. Ја користи услугата Email Validator за да процени синтакса, домен, MX записи, сигнали од даватели и практичен ризик за испорака. Имплементацијата вклучува дефанзивно мапирање на одговори, ограничени повторни обиди, детерминистички тестови, структурирани дневници и излезни датотеки што можат да се прегледаат.
Добијте пристап и копирајте го токенот за услугата
- Регистрирајте се на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате сметка.
- Отворете ја страницата на услугата Email Validator.
- Изберете го достапниот Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за Email Validator.
- Најдете го панелот Service token и копирајте го токенот со опсег на услугата.
Оваа услуга бара автентикација. Нејзиниот токен се доставува преку параметарот за пребарување token. Повторното генерирање на токенот за услугата го поништува претходно активниот токен, затоа ажурирајте ја секоја распоредена околина веднаш по ротација. Никогаш не ја зачувувајте вредноста во commit или не ја вклучувајте во дневници, слики од екранот, тестни фикстури, пораки за исклучоци или ознаки за надгледување.
Потврдете го точниот HTTP договор
Барањето е HTTP GET до https://ai.mihajlo.mk/api/email-validator/v1/check-email. Ги носи и параметрите за пребарување token и email. Минимално дијагностичко барање е:
curl --get \
--data-urlencode "token=YOUR_SERVICE_TOKEN" \
--data-urlencode "[email protected]" \
--header "Accept: application/json" \
"https://ai.mihajlo.mk/api/email-validator/v1/check-email"
Извршувајте го тоа само во приватен терминал. Ингеренциите во низа за пребарување може да се појават во историјата на школката, листи на процеси, дневници на обратен прокси и траги за дебагирање. Апликацијата подолу ја составува URL-адресата внатрешно и никогаш не ја евидентира во дневник.
Одговорот доставува status, score, recommendation, checks и quota. Доставениот договор не воспоставува enum вредности, скала на резултатот или вгнездени клучеви во checks и quota. Затоа производствениот код треба да користи документирани вредности од вашата активирана услуга наместо да претпоставува што значат ознаките.
Чувајте ја конфигурацијата надвор од изворната контрола
Создадете приватна .env датотека и додадете ја во .gitignore. Заменете ги местодржачите за политиката со точните успешни вредности за status и recommendation опишани во тековната документација, плус праг соодветен на нејзината документирана скала на резултатот.
EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_ACCEPT_STATUS=YOUR_DOCUMENTED_ACCEPT_STATUS
EMAIL_ACCEPT_RECOMMENDATION=YOUR_DOCUMENTED_ACCEPT_RECOMMENDATION
EMAIL_ACCEPT_MIN_SCORE=YOUR_CHOSEN_THRESHOLD
EMAIL_CONNECT_TIMEOUT_MS=2000
EMAIL_RESPONSE_TIMEOUT_MS=5000
Неуспешно распоредете ако остане кој било местодржач. Во контејнери и управуван хостинг, внесете ги овие променливи преку складиштето за тајни на платформата наместо да копирате развојна .env датотека во сликата.
Архитектура: автоматизирајте ја сигурноста, зачувајте ја двосмисленоста
Увозот има три исходи. Локално неправилно обликуваните редови одат во rejected.csv. Оддалечено валидирана адреса се прифаќа само кога секој конфигуриран сигнал се согласува. Непознати вредности на одговорот, нецелосни payload-и, истекувања на време, неуспеси при автентикација, ограничување на стапка и послаби резултати од валидација одат во review.jsonl.
Ова намерно избегнува да третира недостапна зависност како доказ дека адресата е лоша. Исто така избегнува тивко прифаќање одговор откако давателот ќе промени enum или облик на payload.
Проектот користи вграден cURL при извршување и изданија компатибилни со PHPUnit 11.5 за развојни тестови. PHPUnit 11 поддржува PHP 8.2 и понови верзии, што го вклучува потребното PHP 8.3 извршно опкружување.
newsletter-import/
├── bin/import-newsletter.php
├── src/
│ ├── Http.php
│ ├── EmailValidatorClient.php
│ ├── ValidationResult.php
│ └── ImportPolicy.php
├── tests/EmailValidatorClientTest.php
├── var/runs/
├── .env
├── .env.example
└── composer.json
Инсталирајте PHP 8.3 со cURL и JSON екстензиите, потоа конфигурирајте Composer:
{
"require": {
"php": "^8.3",
"ext-curl": "*",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.5"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
}
}
composer install
composer dump-autoload
mkdir -p var/runs
Изградете ограничен вграден cURL транспорт
На границата со API се потребни експлицитни истекувања на време и транспортна апстракција. Таа апстракција е мала, но им овозможува на тестовите да го заменат cURL со детерминистички одговори.
<?php
// src/Http.php
declare(strict_types=1);
namespace App;
final readonly class HttpResponse
{
public function __construct(
public int $status,
public string $body,
) {}
}
final class TransportException extends \RuntimeException {}
interface HttpTransport
{
public function get(
string $url,
int $connectTimeoutMs,
int $responseTimeoutMs,
): HttpResponse;
}
final class CurlTransport implements HttpTransport
{
public function get(
string $url,
int $connectTimeoutMs,
int $responseTimeoutMs,
): HttpResponse {
$handle = curl_init($url);
if ($handle === false) {
throw new TransportException('Unable to initialize cURL');
}
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_CONNECTTIMEOUT_MS => $connectTimeoutMs,
CURLOPT_TIMEOUT_MS => $responseTimeoutMs,
CURLOPT_FOLLOWLOCATION => false,
]);
$body = curl_exec($handle);
if ($body === false) {
$message = curl_error($handle);
curl_close($handle);
throw new TransportException($message);
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
return new HttpResponse($status, $body);
}
}
Потврдата на TLS сертификати останува овозможена преку безбедните стандардни поставки на cURL. Не ја оневозможувајте за да „поправите“ грешка со сертификат; наместо тоа, поправете го складиштето на доверливи сертификати на оперативниот систем.
Мапирајте го одговорот на границата
Успешен HTTP статус не е доволен. Маперот отфрла полиња што недостигаат, неочекувани типови, неконечни резултати и JSON што не е објект пред политиката на апликацијата да го види одговорот.
<?php
// src/ValidationResult.php
declare(strict_types=1);
namespace App;
final readonly class ValidationResult
{
public function __construct(
public string $status,
public float $score,
public string $recommendation,
public array $checks,
public array $quota,
) {}
public static function fromArray(array $data): self
{
foreach (['status', 'recommendation'] as $field) {
if (!isset($data[$field]) || !is_string($data[$field])
|| trim($data[$field]) === '') {
throw new \UnexpectedValueException("Invalid {$field}");
}
}
if (!isset($data['score']) || !is_numeric($data['score'])) {
throw new \UnexpectedValueException('Invalid score');
}
$score = (float) $data['score'];
if (!is_finite($score)) {
throw new \UnexpectedValueException('Non-finite score');
}
if (!isset($data['checks']) || !is_array($data['checks'])) {
throw new \UnexpectedValueException('Invalid checks');
}
if (!isset($data['quota']) || !is_array($data['quota'])) {
throw new \UnexpectedValueException('Invalid quota');
}
return new self(
trim($data['status']),
$score,
trim($data['recommendation']),
$data['checks'],
$data['quota'],
);
}
public function auditData(): array
{
return [
'status' => $this->status,
'score' => $this->score,
'recommendation' => $this->recommendation,
'checks' => $this->checks,
'quota' => $this->quota,
];
}
}
Клиентот повторува само транспортни неуспеси, HTTP 429 и одговори 5xx од серверската страна. Автентикацијата, валидацијата на барањето, неправилно обликуваниот JSON и прекршувањата на договорот не се повторуваат бидејќи повторувањето не може да ги поправи.
<?php
// src/EmailValidatorClient.php
declare(strict_types=1);
namespace App;
final class ValidatorFailure extends \RuntimeException
{
public function __construct(
public readonly string $category,
public readonly ?int $httpStatus = null,
) {
parent::__construct($category);
}
}
final class EmailValidatorClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/email-validator/v1/check-email';
public function __construct(
private HttpTransport $transport,
private string $token,
private int $connectTimeoutMs = 2000,
private int $responseTimeoutMs = 5000,
private \Closure $sleep = new \Closure(),
) {
if ($this->token === '') {
throw new \InvalidArgumentException('Missing service token');
}
if ($this->sleep === new \Closure()) {
$this->sleep = static fn(int $microseconds) => usleep($microseconds);
}
}
public function check(string $email): ValidationResult
{
$query = http_build_query(
['token' => $this->token, 'email' => $email],
'',
'&',
PHP_QUERY_RFC3986,
);
$url = self::ENDPOINT . '?' . $query;
$backoff = [0, 200000, 600000];
for ($attempt = 0; $attempt < 3; $attempt++) {
if ($backoff[$attempt] > 0) {
($this->sleep)($backoff[$attempt]);
}
try {
$response = $this->transport->get(
$url,
$this->connectTimeoutMs,
$this->responseTimeoutMs,
);
} catch (TransportException) {
if ($attempt < 2) {
continue;
}
throw new ValidatorFailure('transport');
}
if ($response->status === 429 || $response->status >= 500) {
if ($attempt < 2) {
continue;
}
$category = $response->status === 429
? 'rate_limited'
: 'upstream';
throw new ValidatorFailure($category, $response->status);
}
if ($response->status === 401 || $response->status === 403) {
throw new ValidatorFailure('authentication', $response->status);
}
if ($response->status < 200 || $response->status >= 300) {
throw new ValidatorFailure('request', $response->status);
}
try {
$payload = json_decode(
$response->body,
true,
32,
JSON_THROW_ON_ERROR,
);
if (!is_array($payload)) {
throw new \UnexpectedValueException('Expected JSON object');
}
return ValidationResult::fromArray($payload);
} catch (\JsonException|\UnexpectedValueException) {
throw new ValidatorFailure('invalid_response', $response->status);
}
}
throw new ValidatorFailure('upstream');
}
}
Во вистински код, иницијализирајте ја sleep closure експлицитно; PHP не може да користи свежо конструирана closure како смислен sentinel. Концизен производствен конструктор користи ?Closure $sleep = null и доделува $this->sleep = $sleep ?? static fn(int $us) => usleep($us). Тоа го одржува клиентот тестабилен без вистински доцнења.
Применете конзервативна политика за увоз
Политиката го користи секое вратено поле од договорот. Статусот, препораката и резултатот мора да ги исполнуваат конфигурираните критериуми за прифаќање. Празните податоци за checks или quota го прават резултатот неизвесен бидејќи апликацијата не може да ги ревидира доказите или состојбата на квотата. Суровите структури се задржуваат за преглед без да се претпоставуваат недокументирани вгнездени клучеви.
<?php
// src/ImportPolicy.php
declare(strict_types=1);
namespace App;
enum Decision: string
{
case Accept = 'accept';
case Review = 'review';
}
final readonly class ImportPolicy
{
public function __construct(
private string $acceptedStatus,
private string $acceptedRecommendation,
private float $minimumScore,
) {}
public function decide(ValidationResult $result): Decision
{
$certain =
hash_equals($this->acceptedStatus, $result->status)
&& hash_equals(
$this->acceptedRecommendation,
$result->recommendation,
)
&& $result->score >= $this->minimumScore
&& $result->checks !== []
&& $result->quota !== [];
return $certain ? Decision::Accept : Decision::Review;
}
}
Извршете ја командата за увоз на билтенот
Влезниот CSV мора да содржи email и може да содржи name. Командата ги скратува полињата, ги отстранува контролните знаци од имињата, со мали букви го претвора само делот на доменот и дедуплицира без разлика на големината на буквите за потребите на билтенот. Иако локалните делови на е-поштата теоретски може да бидат чувствителни на големина на букви, системите за билтени обично имаат потреба едно човечко сандаче да биде претставено еднаш; документирајте го тоа деловно правило ако е важно за вашата публика.
<?php
// bin/import-newsletter.php
declare(strict_types=1);
use App\CurlTransport;
use App\Decision;
use App\EmailValidatorClient;
use App\ImportPolicy;
use App\ValidatorFailure;
require dirname(__DIR__) . '/vendor/autoload.php';
$input = $argv[1] ?? null;
if ($input === null || !is_readable($input)) {
fwrite(STDERR, "Usage: php bin/import-newsletter.php contacts.csv\n");
exit(2);
}
$required = [
'EMAIL_VALIDATOR_TOKEN',
'EMAIL_ACCEPT_STATUS',
'EMAIL_ACCEPT_RECOMMENDATION',
'EMAIL_ACCEPT_MIN_SCORE',
];
foreach ($required as $key) {
$value = getenv($key);
if ($value === false || $value === '' || str_starts_with($value, 'YOUR_')) {
throw new RuntimeException("Missing or placeholder environment: {$key}");
}
}
$client = new EmailValidatorClient(
new CurlTransport(),
getenv('EMAIL_VALIDATOR_TOKEN'),
(int) (getenv('EMAIL_CONNECT_TIMEOUT_MS') ?: 2000),
(int) (getenv('EMAIL_RESPONSE_TIMEOUT_MS') ?: 5000),
static fn(int $us) => usleep($us),
);
$policy = new ImportPolicy(
getenv('EMAIL_ACCEPT_STATUS'),
getenv('EMAIL_ACCEPT_RECOMMENDATION'),
(float) getenv('EMAIL_ACCEPT_MIN_SCORE'),
);
$run = dirname(__DIR__) . '/var/runs/' . gmdate('Ymd-His');
if (!mkdir($run, 0700, true) && !is_dir($run)) {
throw new RuntimeException('Cannot create run directory');
}
$source = fopen($input, 'rb');
$accepted = fopen($run . '/accepted.csv', 'xb');
$rejected = fopen($run . '/rejected.csv', 'xb');
$review = fopen($run . '/review.jsonl', 'xb');
$header = fgetcsv($source);
if ($header === false || !in_array('email', $header, true)) {
throw new RuntimeException('CSV requires an email header');
}
$columns = array_flip($header);
fputcsv($accepted, ['email', 'name']);
fputcsv($rejected, ['email', 'reason']);
$seen = [];
while (($row = fgetcsv($source)) !== false) {
$email = trim((string) ($row[$columns['email']] ?? ''));
$name = trim((string) ($row[$columns['name'] ?? -1] ?? ''));
$name = preg_replace('/[\x00-\x1F\x7F]/u', '', $name) ?? '';
$at = strrpos($email, '@');
if ($at !== false) {
$email = substr($email, 0, $at + 1)
. strtolower(substr($email, $at + 1));
}
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
fputcsv($rejected, [$email, 'local_syntax']);
continue;
}
$dedupeKey = strtolower($email);
if (isset($seen[$dedupeKey])) {
fputcsv($rejected, [$email, 'duplicate']);
continue;
}
$seen[$dedupeKey] = true;
try {
$result = $client->check($email);
if ($policy->decide($result) === Decision::Accept) {
fputcsv($accepted, [$email, $name]);
continue;
}
$record = [
'email' => $email,
'name' => $name,
'reason' => 'uncertain',
'validation' => $result->auditData(),
];
} catch (ValidatorFailure $failure) {
$record = [
'email' => $email,
'name' => $name,
'reason' => $failure->category,
'http_status' => $failure->httpStatus,
];
}
fwrite(
$review,
json_encode($record, JSON_THROW_ON_ERROR) . PHP_EOL,
);
fwrite(STDERR, json_encode([
'event' => 'email_queued_for_review',
'email_hash' => hash('sha256', $dedupeKey),
'reason' => $record['reason'],
], JSON_THROW_ON_ERROR) . PHP_EOL);
}
fclose($source);
fclose($accepted);
fclose($rejected);
fclose($review);
fwrite(STDOUT, "Completed: {$run}\n");
Извезете ги променливите на околината преку вашата школка или платформа за распоредување, потоа извршете php bin/import-newsletter.php contacts.csv. Секое извршување добива посебен директориум со ограничени дозволи, така што претходен резултат не се презапишува.
Тестирајте повторни обиди и граници на неуспех
Лажен транспорт ги прави тестовите брзи и независни од сметки, квоти, DNS и мрежни услови.
<?php
// tests/EmailValidatorClientTest.php
declare(strict_types=1);
namespace Tests;
use App\EmailValidatorClient;
use App\HttpResponse;
use App\HttpTransport;
use App\ValidatorFailure;
use PHPUnit\Framework\TestCase;
final class FakeTransport implements HttpTransport
{
public int $calls = 0;
public function __construct(private array $responses) {}
public function get(string $url, int $connect, int $timeout): HttpResponse
{
return $this->responses[$this->calls++];
}
}
final class EmailValidatorClientTest extends TestCase
{
public function testRetriesServerFailureThenMapsResponse(): void
{
$fake = new FakeTransport([
new HttpResponse(503, '{}'),
new HttpResponse(200, json_encode([
'status' => 'configured-status',
'score' => 7,
'recommendation' => 'configured-recommendation',
'checks' => ['evidence' => true],
'quota' => ['available' => true],
], JSON_THROW_ON_ERROR)),
]);
$client = new EmailValidatorClient(
$fake,
'fixture-token',
sleep: static fn(int $us) => null,
);
self::assertSame(
'configured-status',
$client->check('[email protected]')->status,
);
self::assertSame(2, $fake->calls);
}
public function testDoesNotRetryAuthenticationFailure(): void
{
$fake = new FakeTransport([new HttpResponse(401, '{}')]);
$client = new EmailValidatorClient(
$fake,
'fixture-token',
sleep: static fn(int $us) => null,
);
try {
$client->check('[email protected]');
self::fail('Expected failure');
} catch (ValidatorFailure $failure) {
self::assertSame('authentication', $failure->category);
self::assertSame(1, $fake->calls);
}
}
}
Додадете и тестови за политика за точно совпаѓање за прифаќање, низок резултат, непозната препорака, празни проверки и празни податоци за квота. Извршете го пакетот со vendor/bin/phpunit tests.
Операции, безбедност и вообичаени неуспеси
- HTTP 401 или 403: запрете го увозот, потврдете го токенот со опсег на услугата и проверете дали некој го регенерирал. Не повторувајте неуспеси при автентикација.
- HTTP 429: клиентот извршува само ограничени повторни обиди. Чувајте ги преостанатите контакти во редицата за преглед или извршете ги повторно откако ќе се потврди достапноста на квота. Никогаш не вртете бесконечно.
- Повторени 5xx или истекувања на време: прегледајте го бројот на структурирани неуспеси и обидете се повторно со засегнатата редица подоцна. Не класифицирајте ги тие адреси како неиспоравливи.
- Сè влегува во преглед: споредете ги конфигурираниот статус, препораката и прагот на резултатот со официјалната документација и редактиран дијагностички одговор.
- Неправилно обликуван одговор: зачувајте ја категоријата на неуспех, алармирајте при растечки број и прегледајте го договорот пред да го промените маперот.
- Меѓународни адреси:
FILTER_VALIDATE_EMAILе конзервативна локална порта и може да не ја покрива секоја интернационализирана форма. Ако се потребни такви, дефинирајте експлицитна политика за нормализација наместо тивко да ги трансформирате.
Дневниците треба да содржат идентификатори на извршување, броеви, латентност, HTTP статус, обид за повторување и стабилни хешови — не токени, URL-адреси на барања, целосни одговори, имиња или адреси. Ограничете го пристапот до датотеките за увоз и преглед бидејќи содржат лични податоци. Воспоставете правила за задржување и бришење доследни со процесот на согласност на билтенот.
При распоредување, проверете ги cURL екстензијата, запишливиот директориум var/runs, појдовниот HTTPS пристап, внесувањето тајни и точната конфигурација на политиката. Извршете еден мал канарински увоз пред да ја обработите целосната листа. Закажете само еден работник по изворна датотека освен ако не додадете заедничко складиште за дедупликација и експлицитно партиционирање.
Контролна листа за конечна проверка
- Токенот доаѓа од панелот Service token на страницата со документација и се чува само во конфигурација поддржана од околината.
- Клиентот ја повикува точната GET крајна точка со URL-кодирани параметри
tokenиemail. - Истекувањата на време за поврзување и одговор се ограничени.
- Само транспортни грешки, одговори 429 и одговори 5xx добиваат ограничени повторни обиди со backoff.
- Статусот, резултатот, препораката, проверките и квотата се проверуваат по тип и учествуваат во одлуката.
- Непознати, нецелосни или недостапни резултати од валидација влегуваат во рачен преглед наместо да бидат прифатени или отфрлени.
- Тестовите користат лажен транспорт и никогаш не трошат квота на услугата.
- Дневниците и фикстурите не содржат ингеренции или сурови идентитети на претплатници.
- Канарински CSV произведува прифатени, отфрлени и излези за преглед како што се очекува.
Доверлив процес за увоз не е оној што носи најмногу одлуки. Тој е оној што знае кои одлуки се безбедни за автоматизирање. Држете ја портата за прифаќање тесна, направете ја неизвесноста видлива и третирајте ја редицата за преглед како намерна функционалност на производот — не како неуспех на валидацијата.