Symfony и ВИ: Ревизија на веб-страниците на клиентите за понуди за редизајн со API за детектор на технологии
Цитат за редизајн изграден само од слики на екранот е кревок. Видливите страници може да изгледаат едноставно, додека основната страница содржи CMS, ознаки за аналитика, повеќе JavaScript рамки, стари додатоци и пренасочувања што ја комплицираат миграцијата. Откривањето на тие детали по договор за цена го претвора проценувањето во контрола на штетата.
Овој туторијал гради Symfony команда ориентирана кон продукција што проверува јавна веб-страница на клиент пред да се подготви цитат. Тој повикува на API за детектор на технологии на веб-страници, го претвора неговиот одговор во објекти во сопственост на апликацијата и печати концизен извештај за стек. Интеграцијата вклучува ограничени тајмаути, селективни повторни обиди, структурирани неуспеси, безбедно логгирање и детерминистички тестови.
Добијте пристап и копирајте токен со опсег на услуги
Започнете со креирање сметка на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате една.
- Отворете ја страницата за услуги Детектор на технологии на веб-страници.
- Изберете достапен бесплатен, Плус или Про план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата.
- Пронајдете го делот Токен на услугата и копирајте го токенот со опсег на услуги прикажан таму.
- Зачувајте го тој токен во конфигурацијата на проектот поддржана од околината.
Оваа услуга бара автентикација. Прифаќа Bearer токен, заглавие X-API-Token или параметар за барање token. Имлементацијата подолу користи Bearer токен бидејќи ги задржува ингеренциите надвор од URL-адресите, логовите за пристап до прокси, историјата на прелистувачот и системите за аналитика.
Повторното генерирање на токенот на услугата го поништува претходно активниот токен. Третирајте го повторното генерирање како ротација на ингеренции: ажурирајте секое распоредено опкружување што ја користи услугата, проверете го новиот токен и дури потоа сметајте ја ротацијата за завршена.
Потврдете го точното API барање
Детекторот ја користи оваа точна порака:
POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies
Се очекува JSON тело што содржи url. Пред да пишувате Symfony код, направете едно минимално барање од доверлив терминал:
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://example.com"}'
Не лепете го вистинскиот токен во историјата на обвивката на споделена машина. Променлива на околината или работен тек на терминал свесен за тајни е побезбеден.
Креирајте го Symfony проектот
Функционалноста е намерно синхрона. Развивачот ја извршува додека го квалификува барањето за редизајн, па додавањето на Messenger и редица би вовело операционална машинерија без да го подобри работниот тек. Ако ревизиите подоцна станат процес во големи количини или закажани, истиот клиент може да се повика од Messenger ракувач.
composer create-project symfony/skeleton quote-auditor
cd quote-auditor
composer require symfony/http-client symfony/console symfony/monolog-bundle
composer require --dev symfony/test-pack
Релевантната структура на проектот е мала:
quote-auditor/
├── config/
│ └── services.yaml
├── src/
│ ├── Command/AuditWebsiteCommand.php
│ └── WebsiteAudit/
│ ├── AuditApiException.php
│ ├── AuditMapper.php
│ ├── TechnologyDetection.php
│ ├── WebsiteAudit.php
│ └── WebsiteTechnologyClient.php
└── tests/
└── WebsiteAudit/WebsiteTechnologyClientTest.php
Чувајте ги ингеренциите во конфигурацијата на околината
Ставете локални тајни во .env.local, што не треба да се пријави:
WEBSITE_DETECTOR_TOKEN=YOUR_SERVICE_TOKEN
WEBSITE_DETECTOR_ENDPOINT=https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies
Поврзете ги тие вредности со клиентот преку config/services.yaml:
parameters:
website_detector.endpoint: '%env(string:WEBSITE_DETECTOR_ENDPOINT)%'
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
App\WebsiteAudit\WebsiteTechnologyClient:
arguments:
$endpoint: '%website_detector.endpoint%'
$token: '%env(string:WEBSITE_DETECTOR_TOKEN)%'
Во продукција, инјектирајте ги истите имиња на променливи преку продавницата за тајни на платформата за хостирање. Крајната точка е конфигурабилна за тестирање и операционална контрола, но распоредувањата треба да ја користат точната официјална HTTPS крајна точка.
Поседувајте го одговорот на границата на апликацијата
Надворешниот JSON не треба да протекува низ целата апликација. Детекторот враќа откривања со резултати на доверба, поддржувачки докази, информации за верзијата и пренасочувања, но секоја вредност сè уште бара проверки на типот. Недостатокот на опционални податоци треба да произведе нецелосен извештај, а не PHP тип грешка.
Креирајте TechnologyDetection.php и WebsiteAudit.php:
<?php
// src/WebsiteAudit/TechnologyDetection.php
namespace App\WebsiteAudit;
final readonly class TechnologyDetection
{
public function __construct(
public string $name,
public ?float $confidence,
public array $versions,
public array $evidence,
) {}
}
// src/WebsiteAudit/WebsiteAudit.php
namespace App\WebsiteAudit;
final readonly class WebsiteAudit
{
/** @param list<TechnologyDetection> $detections */
public function __construct(
public array $detections,
public array $redirects,
) {}
}
Подолушниот мапер ги валидира концептуалните полиња на одговорот одбранливо. Записите за пренасочување остануваат непроѕирни бидејќи командата за цитирање треба само да го означи нивното присуство; кодот што подоцна прикажува индивидуални скокови треба да воведе друг DTO врз основа на точниот облик документиран од услугата.
<?php
// src/WebsiteAudit/AuditMapper.php
namespace App\WebsiteAudit;
final class AuditMapper
{
public function fromPayload(array $payload): WebsiteAudit
{
$rows = $payload['detections'] ?? [];
$rows = is_array($rows) ? $rows : [];
$detections = [];
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
$name = $row['name'] ?? null;
if (!is_string($name) || trim($name) === '') {
continue;
}
$confidence = $row['confidence'] ?? null;
$confidence = is_numeric($confidence)
? max(0.0, min(100.0, (float) $confidence))
: null;
$versions = $this->listValue($row['versions'] ?? []);
$evidence = $this->listValue($row['evidence'] ?? []);
$detections[] = new TechnologyDetection(
trim($name),
$confidence,
$versions,
$evidence,
);
}
$redirects = $payload['redirects'] ?? [];
return new WebsiteAudit(
$detections,
is_array($redirects) ? array_values($redirects) : [],
);
}
private function listValue(mixed $value): array
{
if (is_string($value) || is_numeric($value)) {
return [(string) $value];
}
return is_array($value) ? array_values($value) : [];
}
}
Задржувањето на претпоставките во еден мапер е практична одлука за одржување. Ако официјалната документација се развива, една граница се менува додека командата и работниот тек за цитирање остануваат стабилни.
Изградете отпорен HTTP клиент
Неуспесите имаат потреба од категории на кои операторот може да реагира. Неуспесите при автентикација бараат корекција на токенот, неуспесите при валидација бараат корекција на внесот, а привремените неуспеси нагоре може да оправдаат повторен обид.
<?php
// src/WebsiteAudit/AuditApiException.php
namespace App\WebsiteAudit;
final class AuditApiException extends \RuntimeException
{
public function __construct(
public readonly string $category,
public readonly ?int $status = null,
?\Throwable $previous = null,
) {
parent::__construct(
"Website audit failed: {$category}",
0,
$previous,
);
}
}
Клиентот користи кратки ограничувања за конекција и целосен одговор. Тој повторно се обидува само со транспортни неуспеси, ограничување на стапката и избрани привремени серверски неуспеси. Одговорите за валидација и автентикација се враќаат веднаш бидејќи повторувањето на истото невалидно барање не може да ги поправи.
<?php
// src/WebsiteAudit/WebsiteTechnologyClient.php
namespace App\WebsiteAudit;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class WebsiteTechnologyClient
{
public function __construct(
private readonly HttpClientInterface $http,
private readonly LoggerInterface $logger,
private readonly AuditMapper $mapper,
private readonly string $endpoint,
private readonly string $token,
) {}
public function inspect(string $url): WebsiteAudit
{
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new AuditApiException('invalid_url');
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->http->request('POST', $this->endpoint, [
'headers' => [
'Authorization' => 'Bearer '.$this->token,
'Accept' => 'application/json',
],
'json' => ['url' => $url],
'timeout' => 5.0,
'max_duration' => 20.0,
]);
$status = $response->getStatusCode();
$body = $response->getContent(false);
if ($status >= 200 && $status < 300) {
try {
$payload = json_decode(
$body,
true,
512,
JSON_THROW_ON_ERROR,
);
} catch (\JsonException $exception) {
throw new AuditApiException(
'invalid_json',
$status,
$exception,
);
}
if (!is_array($payload)) {
throw new AuditApiException('invalid_payload', $status);
}
return $this->mapper->fromPayload($payload);
}
if ($status === 401 || $status === 403) {
throw new AuditApiException('authentication', $status);
}
if ($status === 400 || $status === 422) {
throw new AuditApiException('validation', $status);
}
$retryable = in_array(
$status,
[429, 502, 503, 504],
true,
);
if (!$retryable || $attempt === 3) {
$category = $status === 429
? 'rate_limited'
: 'upstream_http';
throw new AuditApiException($category, $status);
}
} catch (TransportExceptionInterface $exception) {
if ($attempt === 3) {
throw new AuditApiException(
'transport',
null,
$exception,
);
}
}
$this->logger->warning('Website audit request will retry', [
'host' => parse_url($url, PHP_URL_HOST),
'attempt' => $attempt,
]);
usleep(200_000 * (2 ** ($attempt - 1)));
}
throw new AuditApiException('unexpected');
}
}
Намалувањето е намерно ограничено. Командата не треба да виси неодредено кога квотата е исцрпена или давателот е недостапен. За подолги одложувања, особено голема вредност Retry-After, престанете и закажете подоцнежно извршување наместо да држите PHP работник зафатен.
Изложете ја ревизијата како команда за цитирање
<?php
// src/Command/AuditWebsiteCommand.php
namespace App\Command;
use App\WebsiteAudit\AuditApiException;
use App\WebsiteAudit\WebsiteTechnologyClient;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'quote:audit-website',
description: 'Inspect a public website before preparing a redesign quote',
)]
final class AuditWebsiteCommand extends Command
{
public function __construct(
private readonly WebsiteTechnologyClient $client,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('url', InputArgument::REQUIRED, 'Public website URL');
}
protected function execute(
InputInterface $input,
OutputInterface $output,
): int {
$io = new SymfonyStyle($input, $output);
$url = (string) $input->getArgument('url');
try {
$audit = $this->client->inspect($url);
} catch (AuditApiException $exception) {
$io->error(sprintf(
'Audit unavailable (%s%s).',
$exception->category,
$exception->status ? ', HTTP '.$exception->status : '',
));
return Command::FAILURE;
}
$rows = [];
foreach ($audit->detections as $detection) {
$rows[] = [
$detection->name,
$detection->confidence === null
? 'unknown'
: $detection->confidence.'%',
$detection->versions === []
? 'not reported'
: implode(', ', $detection->versions),
count($detection->evidence),
];
}
$io->table(
['Technology', 'Confidence', 'Version', 'Evidence items'],
$rows,
);
$io->note(sprintf(
'%d redirect record(s) reported.',
count($audit->redirects),
));
return Command::SUCCESS;
}
}
Извршете го со php bin/console quote:audit-website https://example.com. Резултатите се внес за професионално расудување, а не автоматски калкулатор на цени. Откривањата со ниска доверба заслужуваат рачна потврда; пренасочувањата можат да сигнализираат работа за консолидација на домени; доказите за верзијата може да откријат ризик од миграција без да докажат дека компонентата е ранлива.
Тестирајте без повикување на живата услуга
MockHttpClient прави повторните обиди и мапирањата детерминистички. Овие тестови никогаш не трошат квота и никогаш не содржат вистински токен.
<?php
// tests/WebsiteAudit/WebsiteTechnologyClientTest.php
namespace App\Tests\WebsiteAudit;
use App\WebsiteAudit\AuditApiException;
use App\WebsiteAudit\AuditMapper;
use App\WebsiteAudit\WebsiteTechnologyClient;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class WebsiteTechnologyClientTest extends TestCase
{
public function testMapsSuccessfulDetection(): void
{
$http = new MockHttpClient(new MockResponse(json_encode([
'detections' => [[
'name' => 'Example CMS',
'confidence' => 92,
'versions' => ['1.2'],
'evidence' => ['public markup signature'],
]],
'redirects' => [],
], JSON_THROW_ON_ERROR), ['http_code' => 200]));
$client = new WebsiteTechnologyClient(
$http,
new NullLogger(),
new AuditMapper(),
'https://service.invalid/detect',
'test-token',
);
$audit = $client->inspect('https://example.com');
self::assertCount(1, $audit->detections);
self::assertSame('Example CMS', $audit->detections[0]->name);
self::assertSame(92.0, $audit->detections[0]->confidence);
}
public function testDoesNotRetryAuthenticationFailure(): void
{
$requests = 0;
$http = new MockHttpClient(function () use (&$requests) {
$requests++;
return new MockResponse('', ['http_code' => 401]);
});
$client = new WebsiteTechnologyClient(
$http,
new NullLogger(),
new AuditMapper(),
'https://service.invalid/detect',
'invalid-token',
);
try {
$client->inspect('https://example.com');
self::fail('Expected an authentication failure.');
} catch (AuditApiException $exception) {
self::assertSame('authentication', $exception->category);
self::assertSame(1, $requests);
}
}
}
Извршете php bin/phpunit. Понатамошен тест за повторен обид може да прикаже две 503 одговори проследени со успешен одговор, иако продукциските тест комплети треба да инјектираат апстракција за спиење за да тестовите за намалување на грбот всушност не паузираат.
Безбедност, опсервабилност и распоредување
Аудитирајте само јавни URL-адреси на клиенти што сте овластени да ги проверувате. Ако овој клиент подоцна се изложи преку веб-образец, измама на барања од страната на серверот станува сериозна грижа: отфрлете ги шемите што не се HTTP, localhost, приватни и адреси на врски, и домаќини што се решаваат во приватни мрежи. Повторно проверете по DNS резолуција и пренасочувања наместо да се потпирате само на поднесениот стринг.
Никогаш не го запишувајте заглавието за овластување, токенот, целосното тело на одговорот или ингеренциите на параметрите за барање. Примерокот ги запишува домаќинот на целта, обидот за повторен обид, категоријата и статусот — доволно за дијагноза без собирање непотребни докази од страницата. Додадете идентификатор за корелација на барања ако ревизиите поминуваат низ повеќе компоненти на апликацијата.
Распоредете со оптимизирани Composer зависности, променливи на продукциската околина и записни и кеш директориуми на Symfony што може да се запишат. По ротација на токен, рестартирајте долготрајни PHP процеси за да ги вчитаат конфигурациите поддржани од околината.
Вообичаени режими на неуспех
- 401 или 403: проверете ја активацијата на планот и токенот со опсег на услуги. Повторно генериран токен го поништува стариот.
- 400 или 422: проверете дали барањето е JSON и содржи валиден јавен
url. - 429: престанете по ограничени обиди и извршете подоцна или прегледајте го капацитетот на планот.
- Тајмаут или транспортен неуспех: разликувајте привремени проблеми со мрежата од клиентска страница што е постојано недостапна.
- Празни откривања: не преведувајте го отсуството на докази како „без технологија“. Запишете го ограничувањето и проверете рачно.
- Неочекуван JSON: откажете се на границата на маперот, споредете го обликот на товарот со официјалната документација и намерно ажурирајте го адаптерот.
Конечен список за проверка
- Командата испраќа
POSTдо точната крајна точка за откривање со JSON што содржиurl. - Bearer токенот доаѓа од конфигурација поддржана од околината и никогаш не се појавува во изворот или логовите.
- Времињата на конекција и целосниот одговор се ограничени.
- Не се повторуваат неуспеси при автентикација и валидација.
- Ограничувањата на стапката и привремените неуспеси нагоре добиваат само ограничено намалување.
- Откривањата, довербата, верзиите, доказите и пренасочувањата се мапираат одбранливо.
- Тестовите користат
MockHttpClientи не прават надворешни барања. - Тест за жива димна проба успева во продукциското опкружување пред да се потпрете на работниот тек.
Извештајот за технологија нема да напише цитат за редизајн за вас, и токму тоа е неговата вредност. Ги заменува претпоставките со инспективни докази, оставајќи ги одлуките за опсег и цени на развивачот. Кога откривањето е повторливо, ограничено и искрено за несигурноста, добиениот цитат станува полесен за одбрана — и многу помалку веројатно да сокрие скапо изненадување.