Symfony: Otkrivajte promjene u tehnološkom paketu klijenta i odmah primajte obavijesti
Klijentova se web-stranica može promijeniti bez upozorenja. Redizajn može zamijeniti CMS, migracija hostinga može uvesti novi CDN ili dodatak za optimizaciju može tiho dodati infrastrukturu koju sada trebate podržavati. Čekanje da se nešto pokvari pretvara promjenu koju je moguće otkriti u incident koji se može izbjeći.
Ovaj vodič izrađuje produkcijski usmjeren Symfony nadzornik koji ispituje važnu javnu web-stranicu, bilježi otkriveni tehnološki skup i šalje e-poruku razvojnom programeru kada se taj skup promijeni. Koristi API Website Technology Detector kao granicu detekcije, Symfonyjev HTTP klijent za kontrolirane zahtjeve, mali domenski model za defenzivno parsiranje i atomsku lokalnu snimku za usporedbu.
Dizajn namjerno ostaje skroman. Planirana naredba lakše se postavlja i održava od reda čekanja za jednu ili nekoliko web-stranica. „Trenutačno” znači sljedeći interval ispitivanja; raspored od pet minuta često je razumna početna točka, ovisno o kvoti odabranog plana.
Pribavite pristup i kopirajte servisni token
- Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
- Otvorite stranicu usluge Website Technology Detector.
- Odaberite dostupni Free, Plus ili Pro plan i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte token ograničen na uslugu.
Ova usluga zahtijeva autentikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Upotrijebit ćemo Bearer token kako bi vjerodajnica ostala izvan URL-a i povezanih zapisnika pristupa. Ponovno generiranje servisnog tokena opoziva prethodno aktivni token, stoga odmah nakon ponovnog generiranja rotirajte konfiguraciju aplikacije.
Potvrdite krajnju točku prije pisanja Symfony koda
Točan zahtjev je POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies s JSON tijelom koje sadrži url. Uputite jedan minimalni zahtjev s pomoću rezerviranog mjesta umjesto da stvarni token stavite u povijest ljuske koju dijelite s bilo kim drugim:
curl --request POST \
--url https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies \
--header "Authorization: Bearer YOUR_SERVICE_TOKEN" \
--header "Content-Type: application/json" \
--data '{"url":"https://client.example"}'
Odgovor sadrži detekcije s ocjenom pouzdanosti, dokaze, verzije i informacije o preusmjeravanju. Tijekom integracije privatno spremite primjer odgovora, ali ga nikada nemojte predati u repozitorij ako njegovi dokazi sadrže pojedinosti specifične za klijenta.
Izradite konfiguraciju Symfony projekta
Ova implementacija cilja na PHP 8.3 ili noviji te postojeću Symfony aplikaciju s dostupnim Consoleom i ubrizgavanjem ovisnosti. Instalirajte izvorni HTTP klijent, mailer i komponente za zaključavanje, kao i Symfonyjev paket za testiranje:
composer require symfony/http-client symfony/mailer symfony/lock
composer require --dev symfony/test-pack
mkdir -p var/technology-watch
Lokalne vrijednosti držite u .env.local, koji ne treba predavati u repozitorij. Produkcija treba ubrizgati iste varijable putem platforme za hosting ili upravitelja tajnama:
WEBSITE_DETECTOR_TOKEN=YOUR_SERVICE_TOKEN
CLIENT_SITE_URL=https://client.example
[email protected]
[email protected]
MAILER_DSN=smtp://user:[email protected]:587
Projekt ima četiri odgovornosti: DetectorClient upravlja ugovorom s udaljenim API-jem, DetectorResult provjerava i normalizira odgovor, SnapshotStore trajno pohranjuje posljednje opažanje, a WatchTechnologyCommand koordinira usporedbu i obavijest.
Mapirajte udaljeni odgovor na granici
Udaljeni JSON ne smije prolaziti kroz aplikaciju kao neprovjereni niz. Sljedeći model zahtijeva da svaka detekcija ima naziv, numeričku pouzdanost, dokaz i niz verzija. Također provjerava stavke preusmjeravanja umjesto da pretpostavlja da je svaki odgovor potpun i ispravno tipiziran.
<?php
// src/Technology/DetectorResult.php
namespace App\Technology;
final readonly class Detection
{
public function __construct(
public string $name,
public float $confidence,
public array $versions,
public array|string $evidence,
) {}
}
final readonly class DetectorResult
{
/** @param list<Detection> $detections @param list<string> $redirects */
public function __construct(
public array $detections,
public array $redirects,
) {}
public static function fromPayload(array $payload): self
{
if (!isset($payload['detections']) || !is_array($payload['detections'])) {
throw new \UnexpectedValueException('Missing detections array.');
}
$detections = [];
foreach ($payload['detections'] as $row) {
if (!is_array($row)
|| !is_string($row['name'] ?? null)
|| $row['name'] === ''
|| !is_numeric($row['confidence'] ?? null)
|| !is_array($row['versions'] ?? null)
|| (!is_array($row['evidence'] ?? null)
&& !is_string($row['evidence'] ?? null))
) {
throw new \UnexpectedValueException('Malformed detection.');
}
$versions = array_values(array_filter(
$row['versions'],
static fn (mixed $v): bool => is_string($v) && $v !== ''
));
$detections[] = new Detection(
$row['name'],
(float) $row['confidence'],
$versions,
$row['evidence'],
);
}
$redirects = $payload['redirects'] ?? [];
if (!is_array($redirects)) {
throw new \UnexpectedValueException('Malformed redirect information.');
}
$redirects = array_values(array_filter(
$redirects,
static fn (mixed $url): bool => is_string($url) && $url !== ''
));
return new self($detections, $redirects);
}
public function fingerprint(): string
{
$stable = array_map(
static fn (Detection $d): array => [
'name' => $d->name,
'versions' => $d->versions,
],
$this->detections,
);
usort($stable, static fn (array $a, array $b): int =>
[$a['name'], $a['versions']] <=> [$b['name'], $b['versions']]
);
return hash('sha256', json_encode(
['detections' => $stable, 'redirects' => $this->redirects],
JSON_THROW_ON_ERROR
));
}
}
Otisak namjerno isključuje pouzdanost i dokaze. Oni mogu postati precizniji, a da ne predstavljaju stvarnu migraciju skupa tehnologija. Nazivi tehnologija, prijavljene verzije i putanje preusmjeravanja signali su promjene.
Izradite ograničeni API klijent koji prepoznaje ponovne pokušaje
HTTP klijent dopušta tri pokušaja za kvarove prijenosa, ograničavanje brzine i kvarove poslužitelja. Kvarovi autentikacije i provjere su završni: ponovni pokušaj s istim tokenom ili korisnim opterećenjem samo troši vrijeme i kvotu.
<?php
// src/Technology/DetectorClient.php
namespace App\Technology;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class DetectorException extends \RuntimeException
{
public function __construct(
public readonly string $kind,
public readonly ?int $status = null,
?\Throwable $previous = null,
) {
parent::__construct("Technology detection failed: {$kind}", 0, $previous);
}
}
final readonly class DetectorClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies';
public function __construct(
private HttpClientInterface $http,
private LoggerInterface $logger,
private string $detectorToken,
) {}
public function detect(string $url): DetectorResult
{
for ($attempt = 1; $attempt <= 3; ++$attempt) {
try {
$response = $this->http->request('POST', self::ENDPOINT, [
'auth_bearer' => $this->detectorToken,
'json' => ['url' => $url],
'timeout' => 10.0,
'max_duration' => 20.0,
]);
$status = $response->getStatusCode();
if ($status >= 200 && $status < 300) {
return DetectorResult::fromPayload($response->toArray(false));
}
if (in_array($status, [400, 401, 403, 422], true)) {
$kind = in_array($status, [401, 403], true)
? 'authentication'
: 'request_rejected';
throw new DetectorException($kind, $status);
}
if (($status === 429 || $status >= 500) && $attempt < 3) {
$headers = $response->getHeaders(false);
$retryAfter = $headers['retry-after'][0] ?? null;
$seconds = ctype_digit((string) $retryAfter)
? min(5, max(1, (int) $retryAfter))
: 1 << ($attempt - 1);
$this->logger->warning('Detector request will be retried.', [
'status' => $status,
'attempt' => $attempt,
'delay_seconds' => $seconds,
]);
usleep($seconds * 1_000_000);
continue;
}
throw new DetectorException(
$status === 429 ? 'rate_limited' : 'remote_error',
$status
);
} catch (DecodingExceptionInterface|\UnexpectedValueException $e) {
throw new DetectorException('invalid_response', null, $e);
} catch (TransportExceptionInterface $e) {
if ($attempt === 3) {
throw new DetectorException('transport', null, $e);
}
usleep((1 << ($attempt - 1)) * 1_000_000);
}
}
throw new DetectorException('exhausted');
}
}
Vremensko ograničenje mirovanja ograničava zastoj aktivnosti veze ili odgovora, dok max_duration ograničava cijeli zahtjev. Odgoda zbog ograničenja brzine poštuje jednostavnu numeričku vrijednost Retry-After, ali je ograničava na pet sekundi, čime planirani proces ostaje ograničen.
Trajno pohranite atomsku osnovnu snimku
Baza podataka bila bi nepotrebna za jedan nadzornik. Pohranite jednu JSON datoteku po URL-u i atomски je zamijenite. Korisnik web-poslužitelja ili raspoređivača treba pristup za pisanje u var/technology-watch.
<?php
// src/Technology/SnapshotStore.php
namespace App\Technology;
final readonly class SnapshotStore
{
public function __construct(private string $directory) {}
public function load(string $url): ?array
{
$path = $this->path($url);
if (!is_file($path)) {
return null;
}
$data = json_decode(
(string) file_get_contents($path),
true,
512,
JSON_THROW_ON_ERROR
);
if (!is_array($data) || !is_string($data['fingerprint'] ?? null)) {
throw new \RuntimeException('Stored snapshot is invalid.');
}
return $data;
}
public function save(string $url, DetectorResult $result): void
{
if (!is_dir($this->directory)
&& !mkdir($this->directory, 0770, true)
&& !is_dir($this->directory)
) {
throw new \RuntimeException('Cannot create snapshot directory.');
}
$path = $this->path($url);
$temporary = $path.'.tmp';
$payload = json_encode([
'url' => $url,
'observed_at' => (new \DateTimeImmutable())->format(DATE_ATOM),
'fingerprint' => $result->fingerprint(),
'detections' => array_map(
static fn (Detection $d): array => [
'name' => $d->name,
'confidence' => $d->confidence,
'versions' => $d->versions,
'evidence' => $d->evidence,
],
$result->detections
),
'redirects' => $result->redirects,
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
if (file_put_contents($temporary, $payload, LOCK_EX) === false
|| !rename($temporary, $path)
) {
throw new \RuntimeException('Cannot write snapshot.');
}
}
private function path(string $url): string
{
return $this->directory.'/'.hash('sha256', $url).'.json';
}
}
Koordinirajte detekciju i obavijest
Prvo uspješno pokretanje uspostavlja osnovnu snimku bez podizanja lažne uzbune. Kasnije se promjene šalju e-poštom prije ažuriranja snimke. Ako isporuka e-pošte ne uspije, staro stanje ostaje, a sljedeće pokretanje pokušava ponovno. To daje semantiku obavijesti barem jednom, pa rušenje nakon slanja, ali prije spremanja, može proizvesti dvostruku poruku.
<?php
// src/Command/WatchTechnologyCommand.php
namespace App\Command;
use App\Technology\DetectorClient;
use App\Technology\DetectorException;
use App\Technology\SnapshotStore;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
#[AsCommand(
name: 'app:watch-technology',
description: 'Detect and report changes to a client website technology stack.'
)]
final class WatchTechnologyCommand extends Command
{
public function __construct(
private readonly DetectorClient $detector,
private readonly SnapshotStore $store,
private readonly LockFactory $locks,
private readonly MailerInterface $mailer,
private readonly string $clientSiteUrl,
private readonly string $alertFrom,
private readonly string $alertTo,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$lock = $this->locks->createLock(
'technology-watch-'.hash('sha256', $this->clientSiteUrl),
60
);
if (!$lock->acquire()) {
$output->writeln('Another detection is already running.');
return Command::SUCCESS;
}
try {
$current = $this->detector->detect($this->clientSiteUrl);
$previous = $this->store->load($this->clientSiteUrl);
if ($previous === null) {
$this->store->save($this->clientSiteUrl, $current);
$output->writeln('Baseline created; no alert sent.');
return Command::SUCCESS;
}
if (hash_equals($previous['fingerprint'], $current->fingerprint())) {
$this->store->save($this->clientSiteUrl, $current);
$output->writeln('No technology change detected.');
return Command::SUCCESS;
}
$summary = array_map(
static fn ($d): string => sprintf(
'%s [%s], confidence %s',
$d->name,
implode(', ', $d->versions) ?: 'version unavailable',
$d->confidence
),
$current->detections
);
$this->mailer->send(
(new Email())
->from($this->alertFrom)
->to($this->alertTo)
->subject('Client website technology stack changed')
->text(
"A public stack change was detected for "
.$this->clientSiteUrl.".\n\n"
.implode("\n", $summary)
."\n\nRedirects:\n"
.implode("\n", $current->redirects)
)
);
$this->store->save($this->clientSiteUrl, $current);
$output->writeln('Change detected and notification sent.');
return Command::SUCCESS;
} catch (DetectorException $e) {
$output->writeln(sprintf(
'Detection failed: %s%s',
$e->kind,
$e->status === null ? '' : " (HTTP {$e->status})"
));
return Command::FAILURE;
} finally {
$lock->release();
}
}
}
Povežite vrijednosti okruženja s uslugama
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
bind:
$detectorToken: '%env(string:WEBSITE_DETECTOR_TOKEN)%'
$clientSiteUrl: '%env(string:CLIENT_SITE_URL)%'
$alertFrom: '%env(string:TECH_ALERT_FROM)%'
$alertTo: '%env(string:TECH_ALERT_TO)%'
App\:
resource: '../src/'
App\Technology\SnapshotStore:
arguments:
$directory: '%kernel.project_dir%/var/technology-watch'
Testirajte granicu API-ja bez mrežnog pristupa
MockHttpClient čini vanjsku interakciju determinističkom. Testirajte valjano mapiranje i odbacivanje neispravnih podataka; testovi naredbe mogu zasebno simulirati klijent, spremište i mailer kako bi pokrili putanje osnovne snimke, nepromijenjenog stanja, promjene i neuspjeha slanja e-pošte.
<?php
// tests/Technology/DetectorClientTest.php
namespace App\Tests\Technology;
use App\Technology\DetectorClient;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class DetectorClientTest extends TestCase
{
public function testMapsSuccessfulDetection(): void
{
$response = new MockResponse(json_encode([
'detections' => [[
'name' => 'Symfony',
'confidence' => 0.98,
'versions' => ['7'],
'evidence' => ['public-signal'],
]],
'redirects' => [],
], JSON_THROW_ON_ERROR), ['http_code' => 200]);
$client = new DetectorClient(
new MockHttpClient([$response]),
new NullLogger(),
'test-token'
);
$result = $client->detect('https://client.example');
self::assertSame('Symfony', $result->detections[0]->name);
self::assertSame(['7'], $result->detections[0]->versions);
self::assertSame([], $result->redirects);
}
public function testRejectsMalformedResponse(): void
{
$response = new MockResponse(
'{"detections":[{"name":"Symfony"}]}',
['http_code' => 200]
);
$client = new DetectorClient(
new MockHttpClient([$response]),
new NullLogger(),
'test-token'
);
$this->expectException(\App\Technology\DetectorException::class);
$client->detect('https://client.example');
}
}
php bin/phpunit
php bin/console app:watch-technology -vv
Postavite i upravljajte nadzornikom
Pokrećite naredbu pod istim Unix identitetom pri svakom pozivu kako bi mogla čitati i zamijeniti svoju snimku. S više instanci aplikacije lokalne datoteke i lokalno zaključavanje nisu dovoljni; planirani zadatak ili prikvačite na jednu instancu ili oboje zamijenite dijeljenom pohranom i dijeljenim Symfony Lock spremištem.
Unos u cron za svakih pet minuta jednostavan je:
*/5 * * * * cd /srv/site-watch && APP_ENV=prod php bin/console app:watch-technology --no-interaction >> var/log/technology-watch-cron.log 2>&1
Nadzirite izlaze koji nisu nula i ponovljene kvarove rate_limited, transport ili invalid_response. Zapisnici trebaju sadržavati URL, status, pokušaj i kategoriju kvara, ali nikada token ni puna autorizacijska zaglavlja. Odaberite interval ispitivanja koji poštuje kvotu aktiviranog plana.
Ograničite CLIENT_SITE_URL na pregledani javni URL klijenta. Nemojte ovu naredbu pretvoriti u neograničeni proxy za URL-ove koje dostavlja korisnik. Zaštitite varijable okruženja, držite datoteke snimaka izvan javnog korijena dokumenata i osigurajte da se vjerodajnicama za e-poštu upravlja kao i tokenom detektora.
Uobičajeni načini kvara
- HTTP 401 ili 403: provjerite servisni token i je li ga netko ponovno generirao, čime je opozvao prethodni token.
- HTTP 400 ili 422: provjerite sadrži li JSON valjani javni
url; promjena ponovnih pokušaja neće popraviti zahtjev. - HTTP 429: smanjite učestalost ispitivanja ili provjerite aktivni plan. Klijent kratko ponovno pokušava, a zatim vraća strukturirani kvar.
- Neispravan odgovor: sačuvajte postojeću osnovnu snimku i istražite prije promjene mappera. Nikada nemojte prepisati poznato dobro stanje neprovjerenim podacima.
- Nema e-pošte: pokrenite naredbu interaktivno, provjerite
MAILER_DSNi neovisno testirajte isporuku e-pošte. Neuspješno slanje namjerno ostavlja prethodnu snimku netaknutom. - Ponovljena upozorenja: potvrdite da je direktorij snimaka trajan i dostupan za pisanje između postavljanja i ponovnih pokretanja spremnika.
Završni kontrolni popis za provjeru
- Plan usluge je aktivan, a trenutačni token ograničen na uslugu dostupan je samo putem konfiguracije okruženja.
- Minimalni POST zahtjev uspijeva prema točnoj krajnjoj točki za detekciju.
php bin/phpunitprolazi bez stvarnog mrežnog zahtjeva.- Prvo pokretanje naredbe stvara osnovnu snimku i ne šalje upozorenje.
- Nepromijenjeni odgovor ažurira podatke opažanja bez slanja e-pošte.
- Promijenjeni otisak tehnologije, verzije ili preusmjeravanja šalje e-poštu prije predavanja nove snimke.
- Kvarovi autentikacije, provjere, kvote, prijenosa, neispravnog odgovora i e-pošte čuvaju prethodnu osnovnu snimku.
- Raspoređivač prijavljuje izlaze koji nisu nula, izbjegava preklapanje pokretanja i radi unutar ograničenja plana.
Korisni nadzornik nije samo API poziv na vremenskom rasporedu. Njegova vrijednost proizlazi iz tretiranja udaljenog odgovora kao nepouzdanog, razlikovanja kvarova za koje se može ponovno pokušati od trajnih kvarova i očuvanja stanja dok obavijest ne uspije. Kada su te granice postavljene, tiha promjena javne tehnologije postaje pravodoban, djelotvoran signal umjesto sutrašnjeg iznenađenja pri otklanjanju pogrešaka.