Symfony: Otkrivajte promjene tehnološkog stoga na klijentskim web-stranicama pomoću API-ja za otkrivanje web-tehnologija
Klijentova se web-stranica može promijeniti bez vaše implementacije. Redizajn može zamijeniti njezin CMS, migracija hostinga može ukloniti CDN, a analitička oznaka može nestati tijekom inače bezopasnog ažuriranja teme. Te su promjene važne kada održavate integracije, pratite performanse ili savjetujete klijenta o sigurnosti.
Ovaj vodič izrađuje Symfony naredbu spremnu za produkciju koja skenira jednu važnu klijentovu web-stranicu, bilježi osnovno stanje i šalje e-poruku razvojnom programeru kada se tehnologije dodaju, uklone ili prijave drugu verziju. Koristi API za Website Technology Detector kao granicu detekcije te namjerno zadržava raspoređivanje, usporedbu, pohranu i obavijesti unutar aplikacije.
Dobijte pristup i izradite servisni token
Registrirajte se putem stranice za registraciju ili upotrijebite stranicu za prijavu ako već imate račun.
- Otvorite stranicu usluge Website Technology Detector.
- Odaberite dostupni plan Free, Plus ili Pro i dovršite aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte njezin token ograničen na uslugu.
Usluga zahtijeva autentifikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Ovaj projekt koristi oblik Bearer kako bi vjerodajnica ostala izvan URL-ova i zapisnika pristupa.
Ponovno generiranje servisnog tokena opoziva prethodno aktivni token. Rotaciju tretirajte kao koordiniranu implementaciju: odmah nakon ponovnog generiranja ažurirajte tajnu aplikacije, a zatim pokrenite provjerno skeniranje.
Potvrdite točan API poziv
Integracija šalje zahtjeve POST na https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Njegovo JSON tijelo sadrži obaveznu vrijednost url.
curl --request POST \
--url https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies \
--header "Authorization: Bearer YOUR_SERVICE_TOKEN" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '{"url":"https://client.example"}'
Koristite javnu web-stranicu za čije ste praćenje ovlašteni. Odgovor sadrži detekcije s ocjenom pouzdanosti, dokaze, podatke o verziji i informacije o preusmjeravanju. Validirat ćemo te vrijednosti umjesto da pretpostavimo da je svako neobavezno polje prisutno.
Pohranite vjerodajnicu prije pisanja aplikacijskog koda
Za lokalni razvoj stavite token u .env.local, koji treba ostati izvan kontrole verzija. Produkcija treba ubrizgati iste varijable putem hosting platforme ili upravitelja tajni.
TECH_DETECTOR_TOKEN=YOUR_SERVICE_TOKEN
TECH_DETECTOR_ENDPOINT=https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies
CLIENT_WEBSITE_URL=https://client.example
[email protected]
[email protected]
MAILER_DSN=smtp://USERNAME:[email protected]:587
URL-kodirajte rezervirane znakove u SMTP vjerodajnicama. Nikada ne stavljajte stvarni servisni token u izvorni kod, fixture podatke, zapisnike, snimke zaslona ili poruke iznimki.
Izradite Symfony projekt
Ova implementacija cilja PHP 8.3 ili noviji te koristi Symfony komponente prve strane. Zakazana naredba ovdje je poželjnija od Messengera: svako je skeniranje mala, periodična jedinica rada, pa bi trajno pokrenut radnik dodao operativni trošak bez poboljšanja ishoda.
composer create-project symfony/skeleton tech-watch
cd tech-watch
composer require symfony/console symfony/http-client symfony/mailer symfony/monolog-bundle
composer require --dev symfony/test-pack
mkdir -p var/data var/lock
Relevantna struktura projekta namjerno je sažeta:
src/
Command/WatchTechnologyStackCommand.php
Domain/DetectionReport.php
Domain/StackDiff.php
Infrastructure/TechnologyDetector.php
Infrastructure/ReportStore.php
Infrastructure/DeveloperNotifier.php
tests/
Infrastructure/TechnologyDetectorTest.php
config/
services.yaml
var/data/
technology-report.json
Konfigurirajte skalarne argumente konstruktora, a Symfonyju prepustite automatsko povezivanje framework usluga:
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
App\Infrastructure\TechnologyDetector:
arguments:
$apiToken: '%env(TECH_DETECTOR_TOKEN)%'
$endpoint: '%env(TECH_DETECTOR_ENDPOINT)%'
App\Infrastructure\DeveloperNotifier:
arguments:
$recipient: '%env(DEVELOPER_EMAIL)%'
$sender: '%env(NOTIFICATION_FROM)%'
App\Command\WatchTechnologyStackCommand:
arguments:
$websiteUrl: '%env(CLIENT_WEBSITE_URL)%'
Mapirajte odgovor na granici aplikacije
Podaci o detekciji opisuju vanjska opažanja, a ne pouzdane domenske objekte. Mapper u nastavku zahtijeva kolekciju detections, prihvaća unose i u obliku popisa i u obliku s ključem naziva, validira vrijednosti pouzdanosti, normalizira nizove verzija, čuva dokaze te prikuplja polja najviše razine povezana s preusmjeravanjem bez ovisnosti o jednom neobaveznom obliku preusmjeravanja.
<?php
// src/Domain/DetectionReport.php
namespace App\Domain;
final readonly class DetectionReport
{
public function __construct(
public array $detections,
public array $redirectInfo,
) {}
public static function fromPayload(array $payload): self
{
$rows = $payload['detections'] ?? null;
if (!is_array($rows)) {
throw new \UnexpectedValueException(
'The API response has no valid detections collection.'
);
}
$detections = [];
foreach ($rows as $key => $row) {
if (!is_array($row)) {
continue;
}
$name = is_string($row['name'] ?? null)
? trim($row['name'])
: (is_string($key) ? trim($key) : '');
if ($name === '') {
continue;
}
$confidence = $row['confidence'] ?? null;
$confidence = is_int($confidence) || is_float($confidence)
? $confidence
: null;
$versionInput = $row['versions'] ?? ($row['version'] ?? []);
$versions = is_array($versionInput)
? $versionInput
: [$versionInput];
$versions = array_values(array_unique(array_filter(
array_map(
static fn (mixed $value): string =>
is_scalar($value) ? trim((string) $value) : '',
$versions
),
static fn (string $value): bool => $value !== ''
)));
sort($versions, SORT_NATURAL | SORT_FLAG_CASE);
$evidence = $row['evidence'] ?? [];
$evidence = is_array($evidence) ? array_values($evidence) : [$evidence];
$detections[] = [
'name' => $name,
'confidence' => $confidence,
'versions' => $versions,
'evidence' => $evidence,
];
}
$redirectInfo = [];
foreach ($payload as $key => $value) {
if (is_string($key)
&& str_contains(strtolower($key), 'redirect')) {
$redirectInfo[$key] = $value;
}
}
return new self($detections, $redirectInfo);
}
public function toArray(): array
{
return [
'detections' => $this->detections,
'redirect_info' => $this->redirectInfo,
'observed_at' => gmdate(DATE_ATOM),
];
}
}
Pouzdanost i dokazi ostaju dostupni za dijagnostiku, ali ne pokreću upozorenja. Dokazi se mogu promijeniti kada se promijeni markup, a same fluktuacije pouzdanosti mogu stvoriti bučne obavijesti. Smislena usporedba skupa tehnologija temelji se na normaliziranim nazivima tehnologija i verzijama.
Izgradite otporan HTTP klijent
Klijent koristi ograničena vremenska ograničenja povezivanja i ukupnog odgovora. Ponovno pokušava pri transportnim kvarovima, HTTP 429 odgovorima i 5xx odgovorima na strani poslužitelja, do tri pokušaja. Autentifikacija i druge klijentske pogreške odmah ne uspijevaju jer ponavljanje nevažećeg zahtjeva samo troši vrijeme i kvotu.
<?php
// src/Infrastructure/TechnologyDetector.php
namespace App\Infrastructure;
use App\Domain\DetectionReport;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
final class DetectorException extends \RuntimeException {}
final class TechnologyDetector
{
private \Closure $sleep;
public function __construct(
private HttpClientInterface $http,
private LoggerInterface $logger,
private string $apiToken,
private string $endpoint,
?callable $sleeper = null,
) {
$this->sleep = $sleeper === null
? static fn (int $microseconds) => usleep($microseconds)
: \Closure::fromCallable($sleeper);
}
public function detect(string $url): DetectionReport
{
if (filter_var($url, FILTER_VALIDATE_URL) === false
|| !in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)) {
throw new \InvalidArgumentException('A valid HTTP(S) URL is required.');
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->http->request('POST', $this->endpoint, [
'headers' => [
'Authorization' => 'Bearer '.$this->apiToken,
'Accept' => 'application/json',
],
'json' => ['url' => $url],
'timeout' => 5.0,
'max_duration' => 20.0,
]);
$status = $response->getStatusCode();
if ($status >= 200 && $status < 300) {
$payload = json_decode(
$response->getContent(false),
true,
512,
JSON_THROW_ON_ERROR
);
if (!is_array($payload)) {
throw new \UnexpectedValueException(
'The API response is not a JSON object.'
);
}
return DetectionReport::fromPayload($payload);
}
$retryable = $status === 429 || $status >= 500;
if (!$retryable || $attempt === 3) {
throw new DetectorException(
sprintf('Technology detection failed with HTTP %d.', $status)
);
}
$this->logger->warning('technology_detector.retry', [
'status' => $status,
'attempt' => $attempt,
'host' => parse_url($url, PHP_URL_HOST),
]);
$this->pause($response, $attempt);
} catch (TransportExceptionInterface $exception) {
if ($attempt === 3) {
throw new DetectorException(
'Technology detection failed after transport retries.',
0,
$exception
);
}
$this->logger->warning('technology_detector.transport_retry', [
'attempt' => $attempt,
'host' => parse_url($url, PHP_URL_HOST),
]);
($this->sleep)((2 ** ($attempt - 1)) * 1_000_000);
} catch (\JsonException|\UnexpectedValueException $exception) {
throw new DetectorException(
'Technology detector returned an invalid response.',
0,
$exception
);
}
}
throw new \LogicException('Retry loop terminated unexpectedly.');
}
private function pause(ResponseInterface $response, int $attempt): void
{
$header = $response->getHeaders(false)['retry-after'][0] ?? null;
$seconds = is_string($header) && ctype_digit($header)
? max(1, min(15, (int) $header))
: 2 ** ($attempt - 1);
($this->sleep)($seconds * 1_000_000);
}
}
Iznimka namjerno izostavlja tijela odgovora i vjerodajnice. Zapisnici identificiraju događaj, pokušaj, status i ciljni host bez otkrivanja tokena.
Trajno pohranite i usporedite snimke stanja
Jedna nadzirana stranica ne opravdava bazu podataka. Atomska JSON datoteka dovoljna je, lako ju je pregledati i jednostavno sigurnosno kopirati. Ako kasnije pratite mnogo klijenata, premjestite isti ugovor repozitorija u bazu podataka s jednim retkom po kanonskom URL-u.
<?php
// src/Infrastructure/ReportStore.php
namespace App\Infrastructure;
final class ReportStore
{
private string $path;
public function __construct(string $projectDir)
{
$this->path = $projectDir.'/var/data/technology-report.json';
}
public function load(): ?array
{
if (!is_file($this->path)) {
return null;
}
$data = json_decode(
(string) file_get_contents($this->path),
true,
512,
JSON_THROW_ON_ERROR
);
if (!is_array($data)) {
throw new \UnexpectedValueException('Stored report is invalid.');
}
return $data;
}
public function save(array $report): void
{
$directory = dirname($this->path);
if (!is_dir($directory) && !mkdir($directory, 0770, true)) {
throw new \RuntimeException('Cannot create report directory.');
}
$temporary = tempnam($directory, 'report-');
if ($temporary === false) {
throw new \RuntimeException('Cannot create temporary report.');
}
$json = json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
if (file_put_contents($temporary, $json, LOCK_EX) === false
|| !rename($temporary, $this->path)) {
@unlink($temporary);
throw new \RuntimeException('Cannot save technology report.');
}
}
}
<?php
// src/Domain/StackDiff.php
namespace App\Domain;
final class StackDiff
{
public static function between(array $before, array $after): array
{
$old = self::index($before);
$new = self::index($after);
$added = array_values(array_diff_key($new, $old));
$removed = array_values(array_diff_key($old, $new));
$versionChanges = [];
foreach (array_intersect_key($new, $old) as $key => $current) {
if ($old[$key]['versions'] !== $current['versions']) {
$versionChanges[] = [
'name' => $current['name'],
'before' => $old[$key]['versions'],
'after' => $current['versions'],
];
}
}
return array_filter([
'added' => $added,
'removed' => $removed,
'version_changes' => $versionChanges,
]);
}
private static function index(array $detections): array
{
$indexed = [];
foreach ($detections as $detection) {
if (!is_array($detection)
|| !is_string($detection['name'] ?? null)) {
continue;
}
$key = strtolower(trim($detection['name']));
if ($key !== '') {
$indexed[$key] = [
'name' => trim($detection['name']),
'versions' => is_array($detection['versions'] ?? null)
? array_values($detection['versions'])
: [],
];
}
}
ksort($indexed);
return $indexed;
}
}
Pošaljite obavijest i pokrenite naredbu
<?php
// src/Infrastructure/DeveloperNotifier.php
namespace App\Infrastructure;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
final class DeveloperNotifier
{
public function __construct(
private MailerInterface $mailer,
private string $recipient,
private string $sender,
) {}
public function send(string $url, array $changes, array $report): void
{
$body = "Public technology stack changes were detected for {$url}.\n\n"
."Changes:\n"
.json_encode($changes, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)
."\n\nCurrent validated report:\n"
.json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
$this->mailer->send(
(new Email())
->from($this->sender)
->to($this->recipient)
->subject('Client website technology stack changed')
->text($body)
);
}
}
<?php
// src/Command/WatchTechnologyStackCommand.php
namespace App\Command;
use App\Domain\StackDiff;
use App\Infrastructure\DeveloperNotifier;
use App\Infrastructure\ReportStore;
use App\Infrastructure\TechnologyDetector;
use Psr\Log\LoggerInterface;
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\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:watch-tech-stack',
description: 'Detects and reports client website technology changes.'
)]
final class WatchTechnologyStackCommand extends Command
{
public function __construct(
private TechnologyDetector $detector,
private ReportStore $store,
private DeveloperNotifier $notifier,
private LoggerInterface $logger,
private string $websiteUrl,
) {
parent::__construct();
}
protected function execute(
InputInterface $input,
OutputInterface $output
): int {
$io = new SymfonyStyle($input, $output);
try {
$current = $this->detector->detect($this->websiteUrl)->toArray();
$previous = $this->store->load();
if ($previous === null) {
$this->store->save($current);
$io->success('Initial technology baseline stored.');
return Command::SUCCESS;
}
$changes = StackDiff::between(
$previous['detections'] ?? [],
$current['detections']
);
if ($changes !== []) {
$this->notifier->send(
$this->websiteUrl,
$changes,
$current
);
$this->logger->notice('technology_stack.changed', [
'host' => parse_url($this->websiteUrl, PHP_URL_HOST),
'change_groups' => array_keys($changes),
]);
}
// Save after notification so a mail failure is retried next run.
$this->store->save($current);
$io->success(
$changes === [] ? 'No stack change detected.' : 'Developer notified.'
);
return Command::SUCCESS;
} catch (\Throwable $exception) {
$this->logger->error('technology_watch.failed', [
'exception' => $exception,
'host' => parse_url($this->websiteUrl, PHP_URL_HOST),
]);
$io->error('Technology watch failed; inspect application logs.');
return Command::FAILURE;
}
}
}
Prvo uspješno pokretanje stvara osnovno stanje bez slanja e-poruke. Naknadna pokretanja tiho spremaju uobičajena opažanja. Kada se pojavi smislena promjena, naredba najprije šalje e-poruku, a tek zatim ažurira snimku stanja, čime se osigurava da privremeni prekid e-pošte trajno ne potisne upozorenje.
Testirajte bez pozivanja stvarne usluge
MockHttpClient čini API granicu determinističkom. Testovi provjeravaju ugovor zahtjeva, obrambeno mapiranje i ograničeno ponašanje ponovnih pokušaja bez trošenja kvote ili izlaganja tokena.
<?php
// tests/Infrastructure/TechnologyDetectorTest.php
namespace App\Tests\Infrastructure;
use App\Infrastructure\TechnologyDetector;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class TechnologyDetectorTest extends TestCase
{
public function testItSendsTheContractAndMapsTheReport(): void
{
$http = new MockHttpClient(
function (string $method, string $url, array $options): MockResponse {
self::assertSame('POST', $method);
self::assertSame('https://service.test/detect', $url);
self::assertSame(
['url' => 'https://client.example'],
json_decode($options['body'], true, 512, JSON_THROW_ON_ERROR)
);
return new MockResponse(json_encode([
'detections' => [[
'name' => 'Example CMS',
'confidence' => 0.95,
'versions' => ['6.1'],
'evidence' => ['public marker'],
]],
'redirects' => ['https://client.example/'],
], JSON_THROW_ON_ERROR), ['http_code' => 200]);
}
);
$detector = new TechnologyDetector(
$http,
new NullLogger(),
'test-token',
'https://service.test/detect',
static fn (int $microseconds) => null
);
$report = $detector->detect('https://client.example');
self::assertSame('Example CMS', $report->detections[0]['name']);
self::assertSame(['6.1'], $report->detections[0]['versions']);
self::assertArrayHasKey('redirects', $report->redirectInfo);
}
public function testItRetriesAServiceFailure(): void
{
$http = new MockHttpClient([
new MockResponse('', ['http_code' => 503]),
new MockResponse('{"detections":[]}', ['http_code' => 200]),
]);
$detector = new TechnologyDetector(
$http,
new NullLogger(),
'test-token',
'https://service.test/detect',
static fn (int $microseconds) => null
);
self::assertSame(
[],
$detector->detect('https://client.example')->detections
);
self::assertSame(2, $http->getRequestsCount());
}
}
php bin/phpunit
php bin/console app:watch-tech-stack -vv
Implementirajte, rasporedite i pratite
Implementirajte s produkcijskim ovisnostima, pružite varijable okruženja putem mehanizma platforme za tajne, provjerite da je var/data trajan i upisiv te konfigurirajte stvarni transport e-pošte. Kontejneri s efemernim datotečnim sustavima trebaju montirani volumen ili spremište izvještaja koje koristi bazu podataka.
Na uobičajenom Linux hostu pokrenite naredbu iz crona i koristite flock kako biste spriječili preklapajuća skeniranja:
17 6 * * * cd /srv/tech-watch && /usr/bin/flock -n var/lock/tech-watch.lock /usr/bin/php bin/console app:watch-tech-stack --env=prod >> var/log/tech-watch-cron.log 2>&1
Pratite izlazne kodove naredbe i strukturirane događaje technology_detector.retry, technology_detector.transport_retry, technology_stack.changed i technology_watch.failed. Uspješno izvršavanje bez promjena također bi trebalo biti vidljivo u povijesti raspoređivača kako se tišina ne bi zamijenila za ispravnost.
Česti produkcijski kvarovi
- HTTP 401 ili 403: potvrdite token ograničen na uslugu i je li nedavno ponovno generiran. Ti se kvarovi namjerno ne pokušavaju ponovno.
- HTTP 429: smanjite učestalost skeniranja ili provjerite aktivni plan. Klijent poštuje numeričku vrijednost
Retry-Afterunutar ograničenog odgađanja. - Ponovljeni 5xx ili transportni kvarovi: provjerite izlazni HTTPS, DNS, pravila proxyja i dostupnost usluge. Naredba neuspješno završava nakon što potroši predviđeni broj ponovnih pokušaja.
- Nevažeći JSON ili nedostajuće detekcije: zadržite zapisnik o kvaru i usporedite ugovor odgovora sa službenom dokumentacijom. Nemojte tiho pohraniti prazno osnovno stanje.
- Nema e-poruke: neovisno testirajte Symfony transport e-pošte i pregledajte mailer zapisnike. Budući da se snimka stanja ne ažurira nakon neuspjeha e-pošte, sljedeće pokretanje ponovno pokušava poslati obavijest.
- Osnovno stanje nestaje: učinite
var/datatrajnim između izdanja i zamjena kontejnera.
Završni kontrolni popis provjere
- Plan usluge je aktivan, a token dolazi iz ploče Service token na stranici dokumentacije.
- Token postoji samo u konfiguraciji podržanoj varijablama okruženja.
- Minimalni autentificirani zahtjev uspijeva za konfigurirani javni URL.
php bin/phpunitprolazi bez mrežnog pristupa.- Prvo pokretanje naredbe stvara
var/data/technology-report.json. - Kontrolirana promjena fixture podataka ili testne snimke stanja proizvodi jednu e-poruku razvojnom programeru.
- Autentifikacijski se kvarovi ne pokušavaju ponovno; 429, 5xx i transportni kvarovi imaju ograničene ponovne pokušaje.
- Raspoređivač sprječava preklapajuća pokretanja i prijavljuje izlazne kodove različite od nule.
- Zapisnici sadrže operativni kontekst, ali nikada token ni neobrađeno tijelo odgovora.
Korisni monitor tehnologija nije samo HTTP zahtjev na vremenskom rasporedu. Njegova vrijednost proizlazi iz stabilnih pravila usporedbe, pažljivog rukovanja nesigurnim vanjskim podacima, pouzdane semantike obavijesti i dovoljno opažljivosti da se razlikuje „ništa se nije promijenilo” od „ništa nije pokrenuto”. Uz postavljene granice, tiha naredba postaje sustav ranog upozorenja za promjene na web-stranici koje bi vas inače kasnije iznenadile.