Symfony CRM: Obogatite potencijalne klijente automatskim uvidima u tehnologije web-stranica
Web-stranica potencijalnog klijenta često otkriva više nego obrazac za otkrivanje. Sažeta bilješka poput “WordPress, WooCommerce, Cloudflare; otkrivena analitika” agenciji odmah daje kontekst za kvalifikaciju, tehničko otkrivanje i izradu ponude.
Ovaj vodič dodaje tu bilješku malom Symfony CRM-u. Inspekcija web-stranice izvršava se asinkrono, udaljeni se odgovor normalizira na granici aplikacije, a kvarovi ostaju vidljivi bez blokiranja izrade potencijalnog klijenta. Rezultat je korisno obogaćivanje, a ne neprozirni ispis API podataka.
Dobijte pristup detektoru
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 njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte token ograničen na uslugu.
Usluga zahtijeva autentikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Ova implementacija koristi oblik Bearer kako bi vjerodajnica ostala izvan URL-ova, zapisnika pristupa, povijesti preglednika i oznaka nadzora.
Ponovno generiranje servisnog tokena opoziva prethodno aktivni token. Rotaciju tretirajte kao postupak implementacije: ažurirajte tajnu u svakom okruženju prije povlačenja workera koji još uvijek drže staru vrijednost.
Potvrdite krajnju točku
Točan zahtjev je POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Njegovo JSON tijelo sadrži jednu vrijednost url. Prije pisanja aplikacijskog koda pošaljite jedan minimalni zahtjev iz pouzdane ljuske:
read -s WTD_TOKEN
curl --request POST \
--url https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies \
--header "Authorization: Bearer ${WTD_TOKEN}" \
--header "Content-Type: application/json" \
--data '{"url":"https://example.com"}'
unset WTD_TOKEN
Nemojte lijepiti odgovor u javni issue ili fixture: dokazi i informacije o preusmjeravanju mogu otkriti pojedinosti o stranici potencijalnog klijenta. Razvojnu vjerodajnicu pohranite u neverzioniranu datoteku .env.local:
WEBSITE_TECHNOLOGY_TOKEN=YOUR_SERVICE_TOKEN
U produkciji ubrizgajte istu varijablu putem upravitelja tajnama hosting platforme ili Symfonyjevog sustava tajni. Nikada ne predajte stvarnu vrijednost u .env.
Arhitektura: obogatite podatke izvan web-zahtjeva
CRM već ima Doctrine entitet Lead koji sadrži ID i URL web-stranice. Dodat ćemo normalizirane podatke uvida, čitljiv sažetak i eksplicitno stanje skeniranja. Konzolna naredba šalje ID-jeve potencijalnih klijenata putem Symfony Messengera; handler ponovno učitava svakog potencijalnog klijenta, poziva detektor, mapira njegov odgovor i sprema rezultat.
Ta je granica važna. Izrada potencijalnog klijenta ostaje brza čak i kada je javna web-stranica spora, API ima ograničenje stope ili worker privremeno nije dostupan. Prosljeđivanje samo ID-ja baze podataka kroz Messenger također izbjegava serijaliziranje zastarjelih Doctrine entiteta.
Relevantna struktura namjerno je mala:
src/
Command/EnrichLeadsCommand.php
Entity/Lead.php
Message/DetectLeadTechnologies.php
MessageHandler/DetectLeadTechnologiesHandler.php
WebsiteTechnology/DetectorClient.php
WebsiteTechnology/DetectorException.php
WebsiteTechnology/TechnologyInsight.php
tests/
WebsiteTechnology/DetectorClientTest.php
config/
packages/messenger.yaml
services.yaml
Spremite normalizirane rezultate i vidljivo stanje
Dodajte ova polja i metode u Lead. JSON stupac zadržava normalizirane dokaze i preusmjeravanja za kasniji pregled, dok tekstualni stupac podržava brzo prikazivanje u popisima i izvozima.
<?php
namespace App\Entity;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
class Lead
{
#[ORM\Column(length: 20)]
private string $technologyScanStatus = 'pending';
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $technologySummary = null;
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $technologyInsights = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $technologyScanError = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $technologyScannedAt = null;
public function markTechnologyScanRunning(): void
{
$this->technologyScanStatus = 'running';
$this->technologyScanError = null;
}
public function applyTechnologyInsight(
string $summary,
array $insights,
\DateTimeImmutable $scannedAt,
): void {
$this->technologySummary = $summary;
$this->technologyInsights = $insights;
$this->technologyScannedAt = $scannedAt;
$this->technologyScanStatus = 'complete';
$this->technologyScanError = null;
}
public function markTechnologyScanFailed(string $safeMessage): void
{
$this->technologyScanStatus = 'failed';
$this->technologyScanError = mb_substr($safeMessage, 0, 1000);
}
}
Generirajte i pregledajte migraciju umjesto ručnog pisanja SQL-a koji možda neće odgovarati vašoj platformi baze podataka:
php bin/console make:migration
php bin/console doctrine:migrations:migrate --no-interaction
Normalizirajte API ugovor na jednoj granici
Detektor vraća determinističke nalaze s pouzdanošću, dokazima, informacijama o verziji i informacijama o preusmjeravanju. Udaljeni JSON ipak treba smatrati nepouzdanim. Nedostajući ključevi, neočekivane skalarne vrijednosti i neispravni unosi ne smiju se proširiti kroz domenu.
<?php
namespace App\WebsiteTechnology;
final readonly class TechnologyInsight
{
public function __construct(
public array $technologies,
public array $redirects,
) {}
public static function fromPayload(array $payload): self
{
$items = is_array($payload['technologies'] ?? null)
? $payload['technologies']
: [];
$technologies = [];
foreach ($items as $item) {
if (!is_array($item) || !is_string($item['name'] ?? null)) {
continue;
}
$name = trim($item['name']);
if ($name === '') {
continue;
}
$confidence = is_numeric($item['confidence'] ?? null)
? (float) $item['confidence']
: null;
$evidence = is_array($item['evidence'] ?? null)
? array_values(array_filter(
$item['evidence'],
static fn (mixed $value): bool => is_string($value)
))
: [];
$versions = is_array($item['versions'] ?? null)
? array_values(array_filter(
$item['versions'],
static fn (mixed $value): bool => is_string($value)
))
: [];
$technologies[] = [
'name' => $name,
'confidence' => $confidence,
'versions' => $versions,
'evidence' => $evidence,
];
}
$redirects = is_array($payload['redirects'] ?? null)
? array_values($payload['redirects'])
: [];
return new self($technologies, $redirects);
}
public function summary(): string
{
if ($this->technologies === []) {
return 'Nijedna tehnologija nije pouzdano identificirana.';
}
return implode('; ', array_map(
static function (array $technology): string {
$version = $technology['versions'][0] ?? null;
return $version === null
? $technology['name']
: $technology['name'].' '.$version;
},
$this->technologies,
));
}
public function toArray(): array
{
return [
'technologies' => $this->technologies,
'redirects' => $this->redirects,
];
}
}
Sažetak namjerno izbjegava pretvaranje pouzdanosti u postotak jer klijent ne bi trebao nagađati ljestvicu usluge. Izvorni numerički rezultat ostaje dostupan u normaliziranom JSON-u. Dokazi se zadržavaju radi objašnjenja i otklanjanja poteškoća, a ne spajaju u bučan CRM popis.
Izgradite ograničen HTTP klijent svjestan kvarova
Symfonyjev HttpClientInterface pruža ubrizgavanje ovisnosti, transporte koji se mogu testirati i ograničene opcije zahtjeva. Klijent u nastavku ponavlja samo kvarove transporta, HTTP 429 odgovore i pogreške poslužitelja. Pogreške provjere valjanosti i autentikacije trajne su dok se ne promijeni unos ili konfiguracija.
<?php
namespace App\WebsiteTechnology;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class DetectorException extends \RuntimeException
{
public function __construct(
string $message,
public readonly bool $retryable,
) {
parent::__construct($message);
}
}
final readonly class DetectorClient
{
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 $websiteTechnologyToken,
) {
if (trim($websiteTechnologyToken) === '') {
throw new \LogicException('Token za tehnologiju web-stranice nije konfiguriran.');
}
}
public function detect(string $url): TechnologyInsight
{
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->httpClient->request('POST', self::ENDPOINT, [
'headers' => [
'Authorization' => 'Bearer '.$this->websiteTechnologyToken,
'Accept' => 'application/json',
],
'json' => ['url' => $url],
'timeout' => 5.0,
'max_duration' => 15.0,
]);
$status = $response->getStatusCode();
if ($status >= 200 && $status < 300) {
try {
$payload = json_decode(
$response->getContent(false),
true,
512,
JSON_THROW_ON_ERROR,
);
} catch (\JsonException $exception) {
throw new DetectorException(
'Detektor je vratio neispravan JSON.',
false,
);
}
if (!is_array($payload)) {
throw new DetectorException(
'Detektor je vratio neočekivani payload.',
false,
);
}
return TechnologyInsight::fromPayload($payload);
}
$retryable = $status === 429 || $status >= 500;
$this->logger->warning('Zahtjev za otkrivanje tehnologije nije uspio.', [
'status' => $status,
'attempt' => $attempt,
'retryable' => $retryable,
]);
if (!$retryable) {
throw new DetectorException(
'Detektor je odbio zahtjev s HTTP '.$status.'.',
false,
);
}
if ($attempt === 3) {
throw new DetectorException(
'Detektor je ostao nedostupan nakon ograničenih ponovnih pokušaja.',
true,
);
}
$retryAfter = $response->getHeaders(false)['retry-after'][0] ?? null;
$delay = ctype_digit((string) $retryAfter)
? min(5, (int) $retryAfter)
: min(4, 2 ** ($attempt - 1));
usleep($delay * 1_000_000);
} catch (TransportExceptionInterface $exception) {
$this->logger->warning('Kvar transporta detektora tehnologije.', [
'attempt' => $attempt,
'exception' => $exception::class,
]);
if ($attempt === 3) {
throw new DetectorException(
'Detektor nije bilo moguće dosegnuti nakon ograničenih ponovnih pokušaja.',
true,
);
}
usleep((2 ** ($attempt - 1)) * 1_000_000);
}
}
throw new DetectorException('Otkrivanje tehnologije nije uspjelo.', true);
}
}
Zapisnici sadrže informacije o statusu i pokušajima, ali nikada token, tijelo odgovora, dokaze ni URL potencijalnog klijenta. To je dovoljno za operativnu dijagnostiku bez pretvaranja zapisnika u drugu CRM bazu podataka.
Povežite konfiguraciju okruženja
Vežite argument konstruktora u config/services.yaml:
services:
_defaults:
autowire: true
autoconfigure: true
bind:
string $websiteTechnologyToken: '%env(WEBSITE_TECHNOLOGY_TOKEN)%'
App\:
resource: '../src/'
Obradite potencijalne klijente putem Messengera
Poruka sadrži samo ID potencijalnog klijenta. Handler provjerava postoji li potencijalni klijent još uvijek i ima li još uvijek HTTP ili HTTPS web-stranicu prije slanja zahtjeva koji se naplaćuje ili troši kvotu.
<?php
namespace App\Message;
final readonly class DetectLeadTechnologies
{
public function __construct(public int $leadId) {}
}
// src/MessageHandler/DetectLeadTechnologiesHandler.php
namespace App\MessageHandler;
use App\Entity\Lead;
use App\Message\DetectLeadTechnologies;
use App\WebsiteTechnology\DetectorClient;
use App\WebsiteTechnology\DetectorException;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final readonly class DetectLeadTechnologiesHandler
{
public function __construct(
private EntityManagerInterface $entityManager,
private DetectorClient $detector,
) {}
public function __invoke(DetectLeadTechnologies $message): void
{
$lead = $this->entityManager->find(Lead::class, $message->leadId);
if (!$lead instanceof Lead) {
return;
}
$url = $lead->getWebsiteUrl();
$scheme = is_string($url) ? parse_url($url, PHP_URL_SCHEME) : null;
if (!in_array($scheme, ['http', 'https'], true)) {
$lead->markTechnologyScanFailed('Web-stranica mora koristiti HTTP ili HTTPS.');
$this->entityManager->flush();
return;
}
$lead->markTechnologyScanRunning();
$this->entityManager->flush();
try {
$insight = $this->detector->detect($url);
$lead->applyTechnologyInsight(
$insight->summary(),
$insight->toArray(),
new \DateTimeImmutable(),
);
} catch (DetectorException $exception) {
$category = $exception->retryable
? 'Privremeni kvar detektora; pokušajte ponovno kasnije.'
: 'Zahtjev za otkrivanje tehnologije je odbijen.';
$lead->markTechnologyScanFailed($category);
}
$this->entityManager->flush();
}
}
Konfigurirajte asinkroni transport u config/packages/messenger.yaml. DSN transporta ostaje vezan uz okruženje; Doctrine transport praktičan je zadani izbor za mali CRM, pod uvjetom da je instaliran symfony/doctrine-messenger.
framework:
messenger:
failure_transport: failed
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
failed: 'doctrine://default?queue_name=failed'
routing:
App\Message\DetectLeadTechnologies: async
Naredba app:enrich-leads može upitati potencijalne klijente čiji je status pending, ili neuspjele potencijalne klijente čiji je prozor za ponovni pokušaj istekao, te poslati jednu poruku DetectLeadTechnologies po ID-ju. Taj upit repozitorija držite paginiranim kako veliki uvoz ne bi učitao svaki entitet odjednom.
php bin/console app:enrich-leads
php bin/console messenger:consume async \
--time-limit=3600 \
--memory-limit=128M \
--limit=500
Pokrenite worker pod systemd-om, Supervisorom ili upraviteljem procesa koji pruža hosting platforma. Ponovno pokrenite workere tijekom implementacija jer dugotrajni PHP procesi automatski ne učitavaju novi kod ni rotirane vrijednosti okruženja.
Testirajte bez pozivanja žive usluge
MockHttpClient čini testove determinističkima i sprječava trošenje kvote. Ovaj test također dokazuje da se neispravni unosi tehnologija odbacuju, dok valjani dokazi i preusmjeravanja preživljavaju normalizaciju.
<?php
namespace App\Tests\WebsiteTechnology;
use App\WebsiteTechnology\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 testItMapsAValidResponseDefensively(): void
{
$responses = [
new MockResponse(json_encode([
'technologies' => [
[
'name' => 'Example CMS',
'confidence' => 0.97,
'versions' => ['4.2'],
'evidence' => ['generator metadata'],
],
['confidence' => 0.4],
],
'redirects' => [
['from' => 'http://example.com', 'to' => 'https://example.com'],
],
], JSON_THROW_ON_ERROR), [
'http_code' => 200,
'response_headers' => ['content-type: application/json'],
]),
];
$client = new MockHttpClient($responses);
$detector = new DetectorClient($client, new NullLogger(), 'test-token');
$insight = $detector->detect('https://example.com');
self::assertSame('Example CMS 4.2', $insight->summary());
self::assertCount(1, $insight->technologies);
self::assertSame(0.97, $insight->technologies[0]['confidence']);
self::assertCount(1, $insight->redirects);
}
}
Dodajte popratne slučajeve za HTTP 401, iscrpljivanje HTTP 429 ponovnih pokušaja, neispravan JSON, iznimke transporta, nepostojećeg potencijalnog klijenta i neispravnu URL shemu. Nikada ne koristite produkcijski token u testnom okruženju.
Sigurnost, operacije i uobičajeni kvarovi
Šaljite samo web-stranice koje usluga smije pregledavati. Validirajte URL-ove prije slanja, ograničite sheme na HTTP i HTTPS te ne dopuštajte interne sheme poput file. Ako korisnici mogu unijeti proizvoljne URL-ove, procijenite rizike zahtjeva sa strane poslužitelja na granici proizvoda, iako udaljena usluga izvršava dohvat.
- 401 ili 403: potvrdite token ograničen na uslugu, aktivaciju plana i tajnu implementacije. Nemojte automatski ponavljati.
- Pogreška validacije serije 400: pregledajte poslani javni URL i ugovor. Ponovno slanje istog unosa troši kvotu.
- 429: poštujte ograničeni povratni odmak, smanjite konkurentnost workera i kasnije ponovno rasporedite neuspjele potencijalne klijente.
- Pogreška serije 500 ili transporta: zadržite potencijalnog klijenta, zabilježite sigurno privremeno stanje i pokušajte ponovno kroz kontroliranu grupu.
- Prazne detekcije: valjani prazan rezultat tretirajte kao uspješno skeniranje, a ne kao iznimku.
- Zaglavljeno stanje running: pratite starost skeniranja i ponovno stavite u red zapise čiji je worker prekinut usred zadatka.
Pratite brojke i latenciju prema ishodu, a ne prema web-stranici. Upozorite na trajne kvarove autentikacije, neuobičajeno ograničavanje stope, rastuću dubinu reda i stare zapise running. ID korelacije priložen poruci i kontekstu zapisa koristan je, pod uvjetom da ne sadrži vjerodajnicu ni podatke potencijalnog klijenta.
Završni kontrolni popis za provjeru
- Stvarni token postoji samo u pohrani tajni koja je vezana uz okruženje.
- Aplikacija poziva točnu POST krajnju točku s JSON-om
url. - Izrada potencijalnog klijenta uspijeva čak i kada detektor nije dostupan.
- Pouzdanost, dokazi, verzije i preusmjeravanja validiraju se prije spremanja.
- Kvarovi autentikacije i validacije ne ponavljaju se naslijepo.
- Vremenska ograničenja, broj ponovnih pokušaja i povratni odmak ograničeni su.
- Zapisnici isključuju tokene, neobrađene odgovore, dokaze i URL-ove potencijalnih klijenata.
- Workeri se ponovno pokreću nakon implementacije koda ili rotacije tokena.
- Testovi temeljeni na mockovima izvode se bez vanjskog mrežnog pristupa.
- Obrađeni potencijalni klijent prikazuje čitljiv sažetak tehnologija i jasno stanje skeniranja.
Najvrjedniji dio ove integracije nije HTTP zahtjev. To je disciplinirani put od nepouzdanog vanjskog opažanja do pouzdane CRM činjenice: stavljen u red, validiran, objašnjiv, promatran i siguran za ponovni pregled. Dobro izgradite tu granicu i jednostavan URL web-stranice postaje koristan kontekst za svaki razgovor koji slijedi.