Izvorni PHP: Nepogrešiv uvoz biltena uz AI provjeru e-pošte i ručni pregled
Uvoz newslettera izgleda bezopasno sve dok pogrešno upisana domena, jednokratni poštanski sandučić, duplicirana adresa ili prolazni kvar API-ja ne dospije na popis za slanje. Siguran odgovor nije izbrisati svaki sumnjivi zapis. Potrebno je automatizirati odluke s visokom razinom pouzdanosti i sačuvati neizvjesne slučajeve za čovjeka.
Ovaj vodič izrađuje taj tijek rada u nativnom PHP-u 8.3. Naredba čita CSV datoteku, normalizira i uklanja duplikate adresa, poziva uslugu Email Validator, uvozi samo zapise koji odgovaraju izričitoj lokalnoj politici te sve ostalo pohranjuje u red za ručnu provjeru koji koristi SQLite. Dizajn je namjerno malen: sinkrona obrada, nativni cURL, PDO i transportna granica koja se može testirati bez pristupa mreži.
Dobijte pristup prije pisanja integracijskog koda
- Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
- Otvorite stranicu usluge Email Validator. Odaberite dostupni plan Free, Plus ili Pro i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge. Pronađite ploču Service token i kopirajte token ograničen na uslugu.
- Spremite taj token u konfiguraciju okruženja projekta. Nikada ga nemojte predati u repozitorij, staviti u testni primjer ni ispisati u zapisnicima.
Ova usluga zahtijeva token. Njegova regeneracija opoziva prethodni aktivni token, stoga rotaciju treba uskladiti s implementacijom: instalirajte novu vrijednost svugdje gdje se uvoznik izvršava, a zatim provjerite naredbu malom datotekom prije nastavka redovne obrade.
Potvrdite točan zahtjev
Integracija upotrebljava GET https://ai.mihajlo.mk/api/email-validator/v1/check-email. Autentikacija se prosljeđuje putem parametra upita token, dok se adresa prosljeđuje putem email.
curl --get 'https://ai.mihajlo.mk/api/email-validator/v1/check-email' \
--data-urlencode 'token=YOUR_SERVICE_TOKEN' \
--data-urlencode '[email protected]' \
--connect-timeout 3 \
--max-time 10
Pokrenite ovo jednom s adresom koja nije produkcijska i provjerite dokumentirani oblik odgovora. Aplikacija u nastavku obrambeno provjerava status, score, recommendation, checks i quota. Ne nagađa nedokumentirana značenja vrijednosti statusa ili preporuke; povoljne vrijednosti i prag bodova predstavljaju politiku implementacije izvedenu iz službene dokumentacije i vaše tolerancije rizika.
Struktura projekta i kompromisi
Uvoznik je CLI proces, a ne web-zahtjev. Time se izbjegavaju vremenska ograničenja preglednika i pojednostavljuje zakazano izvršavanje. Svaka se adresa odmah trajno pohranjuje, pa kasniji kvar ne briše ranije obavljen rad. SQLite je prikladan za mali tim ili implementaciju na jednom poslužitelju; repozitoriji se kasnije mogu premjestiti u drugu PDO bazu podataka bez promjene granice API-ja.
newsletter-import/
├── bin/import.php
├── src/EmailValidator.php
├── src/ImportPolicy.php
├── tests/EmailValidatorTest.php
├── var/
├── .env
└── composer.json
Izradite composer.json i instalirajte ovisnosti. PHPUnit 11 upotrebljava se samo za testove; produkcijski HTTP ostaje nativni cURL.
{
"require": {
"php": "^8.3",
"ext-curl": "*",
"ext-json": "*",
"ext-pdo": "*",
"ext-pdo_sqlite": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"Newsletter\\": "src/"
}
}
}
composer install
mkdir -p var
chmod 700 var
chmod 600 .env
Stavite vrijednosti specifične za okruženje u .env. Primjeri rezerviranih mjesta za politiku namjerni su: zamijenite ih povoljnim vrijednostima i pragom potvrđenim u službenoj dokumentaciji. Odbijanje pokretanja s rezerviranim mjestima sigurnije je od tihog usvajanja izmišljenih semantika usluge.
EMAIL_VALIDATOR_TOKEN=YOUR_SERVICE_TOKEN
EMAIL_VALIDATOR_AUTO_STATUS=YOUR_CONFIRMED_STATUS
EMAIL_VALIDATOR_AUTO_RECOMMENDATION=YOUR_CONFIRMED_RECOMMENDATION
EMAIL_VALIDATOR_MIN_SCORE=YOUR_DOCUMENTED_SCORE_THRESHOLD
IMPORT_DATABASE=var/newsletter.sqlite
Izgradite strogu granicu API-ja
Transport odvojeno vraća status, zaglavlja i tijelo. Validator ponavlja pokušaje samo kod mrežnih kvarova, HTTP-a 429 i poslužiteljskih pogrešaka. Kvarovi autentikacije i uobičajeni odgovori provjere nikada se ne ponavljaju naslijepo. Odgoda je ograničena, a brojčana vrijednost Retry-After poštuje se unutar tog ograničenja.
<?php
// src/EmailValidator.php
namespace Newsletter;
final readonly class HttpResponse
{
public function __construct(
public int $status,
public array $headers,
public string $body
) {}
}
interface Transport
{
public function get(string $url, int $connectMs, int $totalMs): HttpResponse;
}
final class NetworkException extends \RuntimeException {}
final class ApiException extends \RuntimeException {}
final class CurlTransport implements Transport
{
public function get(string $url, int $connectMs, int $totalMs): HttpResponse
{
$headers = [];
$handle = curl_init($url);
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => $connectMs,
CURLOPT_TIMEOUT_MS => $totalMs,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_HEADERFUNCTION => static function ($curl, string $line) use (&$headers): int {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return strlen($line);
},
]);
$body = curl_exec($handle);
if ($body === false) {
$message = curl_error($handle);
curl_close($handle);
throw new NetworkException($message);
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
return new HttpResponse($status, $headers, $body);
}
}
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 fromJson(string $json): self
{
try {
$data = json_decode($json, true, 32, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new ApiException('Validator returned invalid JSON.', 0, $e);
}
if (!is_array($data)
|| !is_string($data['status'] ?? null)
|| !is_numeric($data['score'] ?? null)
|| !is_string($data['recommendation'] ?? null)
|| !is_array($data['checks'] ?? null)
|| !is_array($data['quota'] ?? null)
) {
throw new ApiException('Validator response does not match the required contract.');
}
return new self(
trim($data['status']),
(float) $data['score'],
trim($data['recommendation']),
$data['checks'],
$data['quota']
);
}
}
final class EmailValidator
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/email-validator/v1/check-email';
public function __construct(
private Transport $transport,
private string $token
) {}
public function check(string $email): ValidationResult
{
$query = http_build_query(
['token' => $this->token, 'email' => $email],
'',
'&',
PHP_QUERY_RFC3986
);
$url = self::ENDPOINT . '?' . $query;
for ($attempt = 0; $attempt < 3; $attempt++) {
try {
$response = $this->transport->get($url, 3000, 10000);
} catch (NetworkException $e) {
if ($attempt === 2) {
throw $e;
}
usleep(200000 * (2 ** $attempt));
continue;
}
if ($response->status === 200) {
return ValidationResult::fromJson($response->body);
}
$retryable = $response->status === 429
|| ($response->status >= 500 && $response->status <= 599);
if (!$retryable || $attempt === 2) {
throw new ApiException(
'Validator HTTP failure: ' . $response->status
);
}
$retryAfter = ctype_digit($response->headers['retry-after'] ?? '')
? (int) $response->headers['retry-after']
: 0;
$delayMs = min(2000, max(200 * (2 ** $attempt), $retryAfter * 1000));
usleep($delayMs * 1000);
}
throw new ApiException('Validator retry loop ended unexpectedly.');
}
}
Prosljeđivanje vjerodajnica u nizu upita dio je zahtijevanog ugovora, ali URL-ovi se često bilježe u proxyjima i međuopremi za otklanjanje pogrešaka. Nemojte zapisivati potpuni URL zahtjeva. Osigurajte da svaki obrnuti proxy ili sustav za praćenje izlaznog prometa redigira nizove upita.
Pretvorite dokaz usluge u lokalnu odluku
API pruža dokaz; vaša aplikacija donosi odluku. Ova politika uvozi samo kada se podudaraju konfigurirani status i preporuka, rezultat doseže konfigurirani prag te su prisutni i podaci provjera i kvote. Sve dvosmisleno postaje dostupno za provjeru umjesto da nestane.
<?php
// src/ImportPolicy.php
namespace Newsletter;
enum ImportDecision: string
{
case Import = 'import';
case Review = 'review';
}
final readonly class ImportPolicy
{
public function __construct(
private string $acceptedStatus,
private string $acceptedRecommendation,
private float $minimumScore
) {}
public function decide(ValidationResult $result): ImportDecision
{
$complete = $result->status !== ''
&& $result->recommendation !== ''
&& $result->checks !== []
&& $result->quota !== [];
$approved = hash_equals($this->acceptedStatus, $result->status)
&& hash_equals(
$this->acceptedRecommendation,
$result->recommendation
)
&& $result->score >= $this->minimumScore;
return $complete && $approved
? ImportDecision::Import
: ImportDecision::Review;
}
}
Implementirajte naredbu za uvoz
Naredba učitava datoteku okruženja bez prepisivanja varijabli koje pruža upravitelj procesa. Stvara dvije tablice, lokalno odbacuje neispravne CSV retke, uklanja duplikate unutar datoteke i u zapisnicima koristi sažete adrese. Baza podataka zadržava potpuni dokaz validatora potreban pregledavateljima.
<?php
// bin/import.php
use Newsletter\{
ApiException, CurlTransport, EmailValidator,
ImportDecision, ImportPolicy, NetworkException
};
require dirname(__DIR__) . '/vendor/autoload.php';
foreach (file(dirname(__DIR__) . '/.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (str_starts_with(ltrim($line), '#') || !str_contains($line, '=')) {
continue;
}
[$key, $value] = explode('=', $line, 2);
if (getenv(trim($key)) === false) {
putenv(trim($key) . '=' . trim($value));
}
}
$required = [
'EMAIL_VALIDATOR_TOKEN',
'EMAIL_VALIDATOR_AUTO_STATUS',
'EMAIL_VALIDATOR_AUTO_RECOMMENDATION',
'EMAIL_VALIDATOR_MIN_SCORE',
'IMPORT_DATABASE',
];
foreach ($required as $key) {
if (($value = getenv($key)) === false || $value === '' || str_starts_with($value, 'YOUR_')) {
throw new RuntimeException("Missing production configuration: {$key}");
}
}
if (!is_numeric(getenv('EMAIL_VALIDATOR_MIN_SCORE'))) {
throw new RuntimeException('EMAIL_VALIDATOR_MIN_SCORE must be numeric.');
}
$csvPath = $argv[1] ?? null;
if ($csvPath === null || !is_readable($csvPath)) {
fwrite(STDERR, "Usage: php bin/import.php contacts.csv\n");
exit(64);
}
$pdo = new PDO('sqlite:' . dirname(__DIR__) . '/' . getenv('IMPORT_DATABASE'), null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->exec(
'CREATE TABLE IF NOT EXISTS contacts (
email TEXT PRIMARY KEY, imported_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS manual_review (
email TEXT PRIMARY KEY, reason TEXT NOT NULL,
evidence_json TEXT, created_at TEXT NOT NULL
);'
);
$validator = new EmailValidator(
new CurlTransport(),
getenv('EMAIL_VALIDATOR_TOKEN')
);
$policy = new ImportPolicy(
getenv('EMAIL_VALIDATOR_AUTO_STATUS'),
getenv('EMAIL_VALIDATOR_AUTO_RECOMMENDATION'),
(float) getenv('EMAIL_VALIDATOR_MIN_SCORE')
);
$import = $pdo->prepare(
'INSERT OR IGNORE INTO contacts(email, imported_at) VALUES(?, ?)'
);
$review = $pdo->prepare(
'INSERT OR REPLACE INTO manual_review
(email, reason, evidence_json, created_at) VALUES(?, ?, ?, ?)'
);
$file = new SplFileObject($csvPath);
$file->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY);
$seen = [];
foreach ($file as $row) {
$raw = is_array($row) ? trim((string) ($row[0] ?? '')) : '';
if ($raw === '' || strtolower($raw) === 'email') {
continue;
}
$at = strrpos($raw, '@');
$email = $at === false
? $raw
: substr($raw, 0, $at) . '@' . strtolower(substr($raw, $at + 1));
$logId = hash('sha256', $email);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$review->execute([$email, 'local_syntax_failure', null, gmdate(DATE_ATOM)]);
continue;
}
if (isset($seen[$email])) {
continue;
}
$seen[$email] = true;
try {
$result = $validator->check($email);
$evidence = json_encode([
'status' => $result->status,
'score' => $result->score,
'recommendation' => $result->recommendation,
'checks' => $result->checks,
'quota' => $result->quota,
], JSON_THROW_ON_ERROR);
if ($policy->decide($result) === ImportDecision::Import) {
$import->execute([$email, gmdate(DATE_ATOM)]);
} else {
$review->execute([$email, 'uncertain', $evidence, gmdate(DATE_ATOM)]);
}
} catch (NetworkException | ApiException $e) {
$review->execute([
$email,
'validation_unavailable',
null,
gmdate(DATE_ATOM),
]);
fwrite(STDERR, json_encode([
'event' => 'validation_failed',
'email_hash' => $logId,
'error_type' => $e::class,
]) . PHP_EOL);
}
}
Ova implementacija ne pretvara lokalni dio prije znaka @ u mala slova; normalizira se samo domena. Taj konzervativni izbor izbjegava nametanje pretpostavki o ekvivalentnosti poštanskih sandučića koje nisu univerzalne.
Testirajte bez kontaktiranja usluge
Deterministički lažni transport provjerava sastavljanje upita, mapiranje odgovora i ponašanje u slučaju kvara bez trošenja kvote ili izlaganja tokena.
<?php
// tests/EmailValidatorTest.php
use Newsletter\{
EmailValidator, HttpResponse, Transport, ApiException
};
use PHPUnit\Framework\TestCase;
final class FakeTransport implements Transport
{
public string $requestedUrl = '';
public function __construct(private HttpResponse $response) {}
public function get(string $url, int $connectMs, int $totalMs): HttpResponse
{
$this->requestedUrl = $url;
return $this->response;
}
}
final class EmailValidatorTest extends TestCase
{
public function testMapsACompleteResponseAndEncodesQuery(): void
{
$fake = new FakeTransport(new HttpResponse(200, [], json_encode([
'status' => 'documented-status',
'score' => 42,
'recommendation' => 'documented-recommendation',
'checks' => ['documented-check' => true],
'quota' => ['documented-quota-value' => 7],
], JSON_THROW_ON_ERROR)));
$result = (new EmailValidator($fake, 'test-token'))
->check('[email protected]');
self::assertSame(42.0, $result->score);
self::assertStringContainsString(
'email=name%2Bnews%40example.com',
$fake->requestedUrl
);
self::assertStringContainsString('token=test-token', $fake->requestedUrl);
}
public function testRejectsAnIncompleteContract(): void
{
$fake = new FakeTransport(
new HttpResponse(200, [], '{"status":"present"}')
);
$this->expectException(ApiException::class);
(new EmailValidator($fake, 'test-token'))->check('[email protected]');
}
public function testDoesNotTreatAuthenticationFailureAsData(): void
{
$fake = new FakeTransport(new HttpResponse(401, [], '{}'));
$this->expectException(ApiException::class);
(new EmailValidator($fake, 'bad-token'))->check('[email protected]');
}
}
vendor/bin/phpunit --bootstrap vendor/autoload.php tests
php bin/import.php contacts.csv
sqlite3 var/newsletter.sqlite \
'SELECT email, reason, created_at FROM manual_review ORDER BY created_at;'
Očvršćivanje produkcije i uobičajeni kvarovi
Držite .env izvan objavljenih direktorija i omogućite čitanje samo korisniku za implementaciju. Pohranjene adrese i dokaz provjere tretirajte kao osobne podatke: ograničite pristup bazi podataka, odredite razdoblje zadržavanja zapisa za provjeru i izbrišite dokaz kada više ne služi operativnoj svrsi.
Emitirajte brojače za uvezene, pregledane, lokalno nevaljane, neuspjele API retke, ponovne pokušaje i duplicirane retke. Upozorite na trajne kvarove autentikacije, odgovore povezane s kvotom ili nagli porast neispravnih ugovora. Zapisnici trebaju sadržavati jednosmjerni sažetak adrese i kategoriju pogreške, nikada token, puni URL ili neobrađenu e-adresu.
401 ili 403 obično označava nedostajući, opozvani ili netočan token usluge; popravite konfiguraciju umjesto ponavljanja pokušaja. Ponovljeni odgovori 429 znače da obradu treba pauzirati ili ponovno zakazati prema dostupnoj kvoti i ograničenjima plana. Istek vremena i odgovori 5xx dobivaju samo već prikazane ograničene ponovne pokušaje. Neočekivani oblik JSON-a predstavlja kvar ugovora i odlazi na provjeru, sprječavajući slučajne uvoze nakon uzvodne promjene.
Za veće datoteke stvarajte kontrolne točke prema izvornoj datoteci i broju retka ili dodajte identifikator izvođenja uvoza i jedinstveni izvorni ključ. Izbjegavajte pokretanje mnogih instanci naredbe samo radi povećanja brzine: konkurentnost može iscrpiti kvotu i stvoriti oluju ponovnih pokušaja. Počnite s jednim zakazanim radnikom, izmjerite propusnost i povećavajte konkurentnost samo unutar dokumentiranih ograničenja.
Završni kontrolni popis za provjeru
- Aktivirani plan i token ograničen na uslugu pripadaju usluzi Email Validator.
- Token se oslanja na okruženje, isključen je iz kontrole verzija i ne pojavljuje se u zapisnicima.
- Vrijednosti politike i semantika rezultata potvrđene su prema službenoj dokumentaciji.
- Poznato dobar redak dolazi do
contacts, dok neizvjesni i neispravni reci dolaze domanual_review. - Ponovni pokušaji za mrežu, 429 i 5xx ograničeni su; kvarovi autentikacije i ugovora ne ponavljaju se naslijepo.
- PHPUnit prolazi bez stvarnog HTTP zahtjeva.
- Dozvole baze podataka, zadržavanje dokaza, nadzor kvote i rotacija tokena dokumentirani su za operatere.
Najjači cjevovod za uvoz nije onaj koji donosi najviše automatskih odluka. To je onaj koji zna gdje njegova sigurnost prestaje. Kombiniranjem stroge provjere granica, prilagodljive politike prihvaćanja, ograničenog rukovanja kvarovima i izričitog reda za provjeru, ova mala PHP naredba pretvara nepouzdanu primopredaju proračunske tablice u kontrolirana vrata objavljivanja.