Symfony: Откријте ги технолошките стекови на веб-страниците на потенцијалните клиенти за попаметен пристап на агенциите
Веб-страницата на потенцијален клиент може да открие дали агенцијата природно одговара уште пред некој да ја напише првата порака за контакт. На специјалист за Symfony му треба поинаков разговор со потенцијален клиент што работи со Symfony отколку со клиент што користи WordPress, Shopify или неидентификуван сопствен технолошки стек.
Ова упатство го додава тој контекст во мал CRM за агенција базиран на Symfony. Закажана команда ја испраќа јавната веб-страница на секој потенцијален клиент до API-то Website Technology Detector, ги мапира резултатите со оценета доверливост на заштитна граница на апликацијата и зачувува концизен преглед како Symfony 7 (доверливост 98); PHP 8.3 (доверливост 92).
Имплементацијата претпочита едноставна, сигурна сериска команда наместо синхроно барање од CRM-интерфејсот. Детекцијата може да вклучува пренасочувања и далечинска мрежна работа; ништо од тоа не треба да ја одложува страницата каде што некој уредува потенцијален клиент.
Добијте пристап пред да напишете код за интеграција
- Создадете сметка на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате.
- Отворете ја страницата на услугата Website Technology Detector.
- Изберете го достапниот план Free, Plus или Pro и завршете ја неговата активација.
- Отворете ја официјалната документација на услугата.
- Најдете го панелот Service token и копирајте го таму прикажаниот токен ограничен на услугата.
Оваа услуга бара автентикација. Таа прифаќа Bearer токен, заглавие X-API-Token или параметар за барање token. Апликацијата подолу ја користи Bearer-формата затоа што ги задржува ингеренциите надвор од URL-адресите, историјата на прелистувачот, дневниците на прокси-серверите и вообичаените дневници за пристап.
Повторното генерирање на токенот за услугата го поништува претходно активниот токен. Третирајте ја ротацијата како промена при распоредување: инсталирајте ја новата вредност насекаде каде што се извршува командата, потврдете ја и очекувајте процесите што сè уште ја користат старата вредност да добијат неуспех при автентикација.
Потврдете ја точната крајна точка
Потребниот повик е POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Неговото JSON-барање содржи едно поле, url. Пред да го менувате CRM-от, направете минимално барање од безбеден терминал:
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
Чувајте го местодржачот во предадената .env датотека и ставете ја вистинската тајна во непредадена .env.local при локален развој. Во продукција, внесете ја преку тајната или конфигурацијата на околината на хостинг-платформата.
# .env
WEBSITE_TECHNOLOGY_DETECTOR_TOKEN=YOUR_SERVICE_TOKEN
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\Integration\TechnologyDetector:
arguments:
$serviceToken: '%env(WEBSITE_TECHNOLOGY_DETECTOR_TOKEN)%'
На проектот му е потребен првостраниот HTTP-клиент на Symfony. Ако веќе не е инсталиран, додајте го со composer require symfony/http-client. Тестовите го користат MockHttpClient од пакетот, па не прават надворешни барања.
Обликувајте ја функционалноста околу тесна API-граница
Интеграцијата има три одговорности:
TechnologyDetectorуправува со автентикација, временски ограничувања, повторни обиди, обработка на статус и декодирање на JSON.TechnologyReportпретвора неизвесен надворешен JSON во стабилни доменски податоци.RefreshLeadTechnologiesCommandги скенира потенцијалните клиенти и зачувува прегледи без да вклучува веб-барање.
Детекторот може да врати детекции, верзии, докази, оценки на доверливост и информации за пренасочување. Овие поими припаѓаат во доменскиот модел, но надворешниот JSON не треба да протекува низ целиот CRM. Поставеноста на полињата може да варира, па маперот пребарува вгнездени објекти и прифаќа само вредности со соодветен тип. Зачувајте го суровиот успешен payload за дијагностика, согласно разумно ограничување на големината на базата и политика за задржување.
Мапирајте ги детекциите дефанзивно
Создадете src/Integration/TechnologyReport.php. Овој мапер не претпоставува дека детекциите се наоѓаат под одредена обвивка на одговорот. Кандидатот мора да има непразно име и нумеричка вредност на доверливост. Верзиите и доказите остануваат опционални.
<?php
namespace App\Integration;
final readonly class TechnologyReport
{
public function __construct(
public array $detections,
public array $redirectInformation,
public array $raw,
) {}
public static function fromPayload(array $payload): self
{
$detections = [];
$redirects = [];
$walk = function (mixed $node, ?string $key = null) use (&$walk, &$detections, &$redirects): void {
if (!is_array($node)) {
return;
}
if ($key !== null && str_contains(strtolower($key), 'redirect')) {
$redirects[] = $node;
}
$name = $node['name'] ?? $node['technology'] ?? null;
$confidence = $node['confidence'] ?? null;
if (is_string($name) && trim($name) !== '' && is_numeric($confidence)) {
$versions = $node['versions'] ?? $node['version'] ?? [];
$evidence = $node['evidence'] ?? [];
$detections[] = [
'name' => trim($name),
'confidence' => (float) $confidence,
'versions' => self::scalarList($versions),
'evidence' => is_array($evidence) ? $evidence : [$evidence],
];
}
foreach ($node as $childKey => $child) {
$walk($child, is_string($childKey) ? $childKey : null);
}
};
$walk($payload);
$unique = [];
foreach ($detections as $detection) {
$identity = strtolower($detection['name'].'|'.implode(',', $detection['versions']));
$unique[$identity] = $detection;
}
usort(
$unique,
static fn (array $a, array $b): int => $b['confidence'] <=> $a['confidence'],
);
return new self(array_values($unique), $redirects, $payload);
}
public function summary(): string
{
if ($this->detections === []) {
return 'No technology detections returned';
}
return implode('; ', array_map(static function (array $detection): string {
$version = $detection['versions'][0] ?? null;
return sprintf(
'%s%s (confidence %s)',
$detection['name'],
$version !== null ? ' '.$version : '',
rtrim(rtrim(number_format($detection['confidence'], 2, '.', ''), '0'), '.'),
);
}, $this->detections));
}
private static function scalarList(mixed $value): array
{
$values = is_array($value) ? $value : [$value];
return array_values(array_map(
static fn (string|int|float $item): string => (string) $item,
array_filter($values, static fn (mixed $item): bool => is_scalar($item)),
));
}
}
Забележете дека прегледот ја нарекува вредноста оценка на доверливост, наместо да додава знак за процент. Така се избегнува тивко тврдење за размер што не е дел од доставениот договор. Доказите остануваат достапни за ревизија или иден панел со детали, без да ја натрупуваат листата на потенцијални клиенти.
Изградете отпорен HTTP-клиент
Создадете мал исклучок што носи машински читлив вид на неуспех:
<?php
namespace App\Integration;
final class DetectionException extends \RuntimeException
{
public function __construct(
public readonly string $kind,
string $message,
public readonly ?int $statusCode = null,
) {
parent::__construct($message);
}
}
Сега создадете src/Integration/TechnologyDetector.php. Ограничувањата за поврзување и вкупно времетраење спречуваат бавна зависност да ја задржи командата неограничено. Само транспортни неуспеси, HTTP 429 и неуспеси на серверот се повторуваат. Грешките при автентикација и валидација се конечни, бидејќи друг идентичен повик нема да ги поправи.
<?php
namespace App\Integration;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final readonly class TechnologyDetector
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies';
public function __construct(
private HttpClientInterface $httpClient,
private LoggerInterface $logger,
private string $serviceToken,
) {}
public function detect(string $url): TechnologyReport
{
$this->assertPublicHttpUrl($url);
$delays = [250_000, 750_000, 1_500_000];
for ($attempt = 0; $attempt < 3; ++$attempt) {
try {
$response = $this->httpClient->request('POST', self::ENDPOINT, [
'headers' => [
'Authorization' => 'Bearer '.$this->serviceToken,
'Accept' => 'application/json',
],
'json' => ['url' => $url],
'timeout' => 5.0,
'max_duration' => 20.0,
]);
$status = $response->getStatusCode();
if (($status === 429 || $status >= 500) && $attempt < 2) {
$response->cancel();
usleep($delays[$attempt]);
continue;
}
if ($status === 401 || $status === 403) {
throw new DetectionException('authentication', 'Service authentication failed.', $status);
}
if ($status === 400 || $status === 422) {
throw new DetectionException('validation', 'The service rejected the website URL.', $status);
}
if ($status === 429) {
throw new DetectionException('rate_limited', 'Service quota or rate limit reached.', $status);
}
if ($status < 200 || $status >= 300) {
throw new DetectionException('upstream', 'Technology detection failed upstream.', $status);
}
try {
$payload = json_decode(
$response->getContent(false),
true,
512,
JSON_THROW_ON_ERROR,
);
} catch (\JsonException $exception) {
throw new DetectionException('invalid_response', 'Service returned invalid JSON.');
}
if (!is_array($payload)) {
throw new DetectionException('invalid_response', 'Service returned an unexpected JSON value.');
}
return TechnologyReport::fromPayload($payload);
} catch (TransportExceptionInterface $exception) {
if ($attempt < 2) {
usleep($delays[$attempt]);
continue;
}
throw new DetectionException('transport', 'Technology service was unreachable.');
}
}
throw new DetectionException('transport', 'Technology service was unreachable.');
}
private function assertPublicHttpUrl(string $url): void
{
$parts = parse_url($url);
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
$host = $parts['host'] ?? null;
if (!in_array($scheme, ['http', 'https'], true) || !is_string($host) || $host === '') {
throw new DetectionException('validation', 'A complete HTTP or HTTPS website URL is required.');
}
if (isset($parts['user']) || isset($parts['pass'])) {
throw new DetectionException('validation', 'Website URLs containing credentials are not accepted.');
}
}
}
Дневникот треба да ги бележи идентификаторот на потенцијалниот клиент, видот на неуспех, статусниот код, исходот од обидот и времетраењето. Не треба да ги бележи заглавието Authorization, токенот, целосното тело на одговорот или низата за барање во URL-адресата. Примерот го задржува бележењето на ниво на командата за да го има корисниот контекст за потенцијалниот клиент.
Зачувајте ја состојбата насочена кон CRM
Додајте ги овие полиња во постојниот Doctrine ентитет Lead, потоа генерирајте и прегледајте миграција со php bin/console make:migration и применете ја со php bin/console doctrine:migrations:migrate:
#[ORM\Column(length: 2000, nullable: true)]
private ?string $technologySummary = null;
#[ORM\Column(length: 24, options: ['default' => 'pending'])]
private string $technologyScanState = 'pending';
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $technologyScannedAt = null;
#[ORM\Column(length: 500, nullable: true)]
private ?string $technologyScanError = null;
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $technologyScanPayload = null;
public function recordTechnologyReport(TechnologyReport $report): void
{
$this->technologySummary = mb_substr($report->summary(), 0, 2000);
$this->technologyScanState = 'complete';
$this->technologyScannedAt = new \DateTimeImmutable();
$this->technologyScanError = null;
$this->technologyScanPayload = $report->raw;
}
public function recordTechnologyFailure(DetectionException $exception): void
{
$this->technologyScanState = $exception->kind;
$this->technologyScannedAt = new \DateTimeImmutable();
$this->technologyScanError = mb_substr($exception->getMessage(), 0, 500);
}
Задржувањето на претходниот преглед при неуспех е намерно. Привремен прекин не треба да избрише корисен продажен контекст. Состојбата и временската ознака му кажуваат на интерфејсот дека прикажаниот резултат можеби е застарен.
Освежувајте ги потенцијалните клиенти со закажана команда
Создадете src/Command/RefreshLeadTechnologiesCommand.php. За мал CRM, последователната обработка ги прави квотите и однесувањето при неуспех лесни за разбирање. Периодично извршувајте flush за ORM да не го задржи секој потенцијален клиент во текот на целото извршување.
<?php
namespace App\Command;
use App\Entity\Lead;
use App\Integration\DetectionException;
use App\Integration\TechnologyDetector;
use App\Repository\LeadRepository;
use Doctrine\ORM\EntityManagerInterface;
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;
#[AsCommand('app:refresh-lead-technologies')]
final class RefreshLeadTechnologiesCommand extends Command
{
public function __construct(
private readonly LeadRepository $leads,
private readonly TechnologyDetector $detector,
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$query = $this->leads->createQueryBuilder('lead')
->andWhere('lead.websiteUrl IS NOT NULL')
->orderBy('lead.id', 'ASC')
->getQuery();
$processed = 0;
$failed = 0;
foreach ($query->toIterable() as $lead) {
assert($lead instanceof Lead);
try {
$lead->recordTechnologyReport(
$this->detector->detect($lead->getWebsiteUrl()),
);
} catch (DetectionException $exception) {
++$failed;
$lead->recordTechnologyFailure($exception);
$this->logger->warning('Lead technology detection failed.', [
'lead_id' => $lead->getId(),
'kind' => $exception->kind,
'status_code' => $exception->statusCode,
]);
}
if (++$processed % 20 === 0) {
$this->entityManager->flush();
$this->entityManager->clear();
}
}
$this->entityManager->flush();
$output->writeln(sprintf('Processed %d leads; %d failed.', $processed, $failed));
return $failed === 0 ? Command::SUCCESS : Command::FAILURE;
}
}
Закажете php bin/console app:refresh-lead-technologies --env=prod во интервал соодветен на планот и на тоа колку често се менуваат веб-страниците на потенцијалните клиенти. Конфигурирајте го распоредувачот да спречи преклопување на извршувањата. За поголема база на податоци, барајте само застарени или записи на чекање и воведете ограничување на серија базирано на курсор пред да размислите за Messenger workers.
Тестирајте без трошење квота
MockHttpClient го проверува излезното барање и доставува детерминистички одговор. Тестот подолу исто така потврдува дека доменскиот преглед вклучува податоци за верзијата и доверливоста.
<?php
namespace App\Tests\Integration;
use App\Integration\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 testItMapsAValidDetectionResponse(): void
{
$client = new MockHttpClient(function (string $method, string $url, array $options): MockResponse {
self::assertSame('POST', $method);
self::assertSame(
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies',
$url,
);
self::assertStringContainsString(
'Bearer test-token',
implode("\n", $options['headers']),
);
return new MockResponse(json_encode([
'detections' => [[
'name' => 'Symfony',
'confidence' => 98,
'versions' => ['7'],
'evidence' => ['deterministic test evidence'],
]],
'redirects' => [],
], JSON_THROW_ON_ERROR), ['http_code' => 200]);
});
$report = (new TechnologyDetector($client, new NullLogger(), 'test-token'))
->detect('https://example.com');
self::assertSame('Symfony 7 (confidence 98)', $report->summary());
self::assertCount(1, $report->detections);
}
public function testItDoesNotRetryAuthenticationFailure(): void
{
$requests = 0;
$client = new MockHttpClient(function () use (&$requests): MockResponse {
++$requests;
return new MockResponse('{"error":"unauthorized"}', ['http_code' => 401]);
});
try {
(new TechnologyDetector($client, new NullLogger(), 'invalid'))
->detect('https://example.com');
self::fail('Expected an authentication failure.');
} catch (\App\Integration\DetectionException $exception) {
self::assertSame('authentication', $exception->kind);
self::assertSame(1, $requests);
}
}
}
Фикстурата на одговорот ги испробува документираните семантички влезови на маперот; таа не е замена за проверка на тековните официјални примери за одговори. Додавајте снимени, санирани договорни фикстури само по отстранување на URL-адресите на клиентите, токените и непотребните докази.
Распоредете и потврдете ја целосната патека
Внесете го продукцискиот токен, извршете ја прегледаната Doctrine миграција, загрејте го кешот на Symfony и извршете едно рачно освежување пред да го овозможите распоредот. Поставете предупредувања за повторени неуспеси при автентикација, трајно ограничување на стапката, растечки транспортни неуспеси и невообичаено долго времетраење на командата. Кодот 429 вообичаено бара поретка фреквенција на распоредот или преглед на планот, а не агресивни повторни обиди.
Вообичаените неуспеси најчесто се едноставни: 401 или 403 значи дека токенот недостига, е поништен или е прикачен погрешно; 400 или 422 укажува на погрешно формирана или неподдржана URL-адреса; повторени одговори 429 укажуваат на исцрпена квота или прекумерна конкурентност; транспортните грешки укажуваат на проблем со DNS, TLS, заштитен ѕид или достапноста на надворешната услуга; а успешен одговор без мапирани детекции налага споредување на санираниот payload со официјалната документација и ажурирање само на граничниот мапер.
Конечна листа за проверка
- Токенот постои само во конфигурација поддржана од околината и никогаш не се појавува во дневници или фикстури.
- Апликацијата испраќа JSON
urlдо точната HTTPS крајна точка со Bearer автентикација. - Времето за поврзување и вкупното време на одговор се ограничени.
- Се повторуваат само минливи транспортни неуспеси, ограничувања на стапката и неуспеси на серверот.
- Детекциите, доверливоста, верзиите, доказите и информациите за пренасочување се мапираат или задржуваат дефанзивно.
- Неуспешното освежување го зачувува последниот корисен преглед и запишува структурирана состојба.
- Автоматизираните тестови не прават повици до жива услуга.
- Продукцискиот распоредувач спречува преклопување на извршувањата и ги почитува ограничувањата на планот.
- Секој скениран потенцијален клиент прикажува читлив преглед со состојба и временска ознака на скенирањето.
Вредниот резултат не е куп JSON за отпечатоци. Тоа е една смирена, читлива линија покрај секој потенцијален клиент: доволно технички контекст за да обликува релевантен разговор, поткрепен со докази кога некој треба да го прегледа, и одржуван од интеграција што откажува предвидливо наместо да ги изненадува корисниците на CRM-от.