Туториали

Symfony CRM: Enrich Leads with Automatic Website Technology Insights

Symfony CRM: Збогатете ги потенцијалните клиенти со автоматски увид во технологиите на веб-страниците

Веб-страницата на потенцијален клиент често открива повеќе отколку што открива формулар за откривање. Кратка белешка како „WordPress, WooCommerce, Cloudflare; откриена аналитика“ ѝ дава на агенцијата непосреден контекст за квалификација, техничко откривање и пишување понуди.

Овој туторијал ја додава таа белешка во мал Symfony CRM. Инспекцијата на веб-страницата се извршува асинхроно, оддалечениот одговор се нормализира на границата на апликацијата, а неуспесите остануваат видливи без да го блокираат создавањето на потенцијален клиент. Резултатот е корисно збогатување, наместо непрегледен исфрлен API-податок.

Добијте пристап до детекторот

Регистрирајте се преку страницата за регистрација, или користете ја страницата за најава ако веќе имате сметка.

  1. Отворете ја страницата на услугата Website Technology Detector.
  2. Изберете достапен Free, Plus или Pro план и завршете ја неговата активација.
  3. Отворете ја официјалната документација за услугата.
  4. Најдете го панелот Service token и копирајте го токенот со опсег на услугата.

Услугата бара автентикација. Таа прифаќа Bearer токен, заглавие X-API-Token или параметар за барање token. Оваа имплементација ја користи Bearer-формата, за акредитивот да не се појавува во URL-адреси, дневници за пристап, историја на прелистувачот и ознаки за мониторинг.

Повторното генерирање на сервисниот токен го поништува претходно активниот токен. Третирајте ја ротацијата како операција за распоредување: ажурирајте ја тајната во секоја околина пред да ги повлечете работниците што сè уште ја чуваат старата вредност.

Потврдете ја крајната точка

Точното барање е POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Неговото JSON-тело содржи единствена вредност url. Пред да пишувате апликациски код, направете едно минимално барање од доверлива школка:

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

Не го вметнувајте одговорот во јавно прашање или фикстура: доказите и информациите за пренасочување може да откријат детали за страницата на потенцијален клиент. Зачувајте го развојниот акредитив во непотврдената датотека .env.local:

WEBSITE_TECHNOLOGY_TOKEN=YOUR_SERVICE_TOKEN

Во продукција, внесете ја истата променлива преку менаџерот за тајни на хостинг-платформата или системот за тајни на Symfony. Никогаш не ја потврдувајте вистинската вредност во .env.

Архитектура: збогатување надвор од веб-барањето

CRM веќе има Doctrine ентитет Lead што содржи ID и URL-адреса на веб-страница. Ќе додадеме нормализирани податоци за увид, читливо резиме и експлицитна состојба на скенирање. Конзолна команда испраќа ID-ја на потенцијални клиенти преку Symfony Messenger; обработувач повторно го вчитува секој потенцијален клиент, го повикува детекторот, го мапира неговиот одговор и го зачувува резултатот.

Оваа граница е важна. Создавањето потенцијални клиенти останува брзо дури и кога јавна веб-страница е бавна, API-то е ограничено по стапка или работникот е привремено недостапен. Пренесувањето само на ID-то од базата на податоци преку Messenger исто така избегнува сериски претворање на застарени Doctrine ентитети.

Релевантната структура намерно е мала:

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

Зачувајте нормализирани резултати и видлива состојба

Додадете ги овие полиња и методи во Lead. JSON-колоната ги задржува нормализираните докази и пренасочувања за подоцнежна проверка, додека текстуалната колона поддржува брзо прикажување во листи и извози.

<?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);
    }
}

Генерирајте и прегледајте ја миграцијата, наместо рачно да пишувате SQL што можеби нема да одговара на вашата платформа за база на податоци:

php bin/console make:migration
php bin/console doctrine:migrations:migrate --no-interaction

Нормализирајте го API-договорот на една граница

Детекторот враќа детерминистички наоди со доверба, докази, информации за верзија и информации за пренасочување. Оддалечениот JSON и понатаму треба да се смета за недоверлив. Недостасувачките клучеви, неочекуваните скаларни вредности и неправилно форматираните записи не смеат да протекуваат низ доменот.

<?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 'No technologies were confidently identified.';
        }

        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,
        ];
    }
}

Резимето намерно избегнува претворање на довербата во процент, бидејќи клиентот не треба да ја претпоставува скалата на услугата. Оригиналниот нумерички резултат останува достапен во нормализираниот JSON. Доказите се задржуваат за објаснување и отстранување проблеми, а не се спојуваат во бучна CRM-листа.

Изградете ограничен HTTP-клиент свесен за неуспеси

HttpClientInterface на Symfony обезбедува dependency injection, преноси што може да се тестираат и ограничени опции за барања. Клиентот подолу повторува само неуспеси при транспорт, HTTP 429 одговори и грешки на серверот. Неуспесите при валидација и автентикација се трајни сè додека не се промени влезот или конфигурацијата.

<?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('Website technology token is not configured.');
        }
    }

    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(
                            'Detector returned invalid JSON.',
                            false,
                        );
                    }

                    if (!is_array($payload)) {
                        throw new DetectorException(
                            'Detector returned an unexpected payload.',
                            false,
                        );
                    }

                    return TechnologyInsight::fromPayload($payload);
                }

                $retryable = $status === 429 || $status >= 500;

                $this->logger->warning('Technology detection request failed.', [
                    'status' => $status,
                    'attempt' => $attempt,
                    'retryable' => $retryable,
                ]);

                if (!$retryable) {
                    throw new DetectorException(
                        'Detector rejected the request with HTTP '.$status.'.',
                        false,
                    );
                }

                if ($attempt === 3) {
                    throw new DetectorException(
                        'Detector remained unavailable after bounded retries.',
                        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('Technology detector transport failure.', [
                    'attempt' => $attempt,
                    'exception' => $exception::class,
                ]);

                if ($attempt === 3) {
                    throw new DetectorException(
                        'Detector could not be reached after bounded retries.',
                        true,
                    );
                }

                usleep((2 ** ($attempt - 1)) * 1_000_000);
            }
        }

        throw new DetectorException('Technology detection failed.', true);
    }
}

Дневниците содржат информации за статусот и обидот, но никогаш токенот, телото на одговорот, доказите или URL-адресата на потенцијалниот клиент. Тоа е доволно за оперативна дијагноза без претворање на дневниците во втора CRM-база на податоци.

Поврзете ја конфигурацијата на околината

Поврзете го аргументот на конструкторот во config/services.yaml:

services:
  _defaults:
    autowire: true
    autoconfigure: true
    bind:
      string $websiteTechnologyToken: '%env(WEBSITE_TECHNOLOGY_TOKEN)%'

  App\:
    resource: '../src/'

Обработувајте потенцијални клиенти преку Messenger

Пораката содржи само ID на потенцијалниот клиент. Обработувачот проверува дали потенцијалниот клиент сè уште постои и сè уште има HTTP или HTTPS веб-страница пред да направи барање што се наплаќа или троши квота.

<?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('Website must use HTTP or 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
                ? 'Temporary detector failure; retry later.'
                : 'Technology detection request was rejected.';

            $lead->markTechnologyScanFailed($category);
        }

        $this->entityManager->flush();
    }
}

Конфигурирајте асинхрон транспорт во config/packages/messenger.yaml. DSN на транспортот останува поддржан од околината; Doctrine transport е практична стандардна опција за мал CRM, под услов да е инсталиран 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

Командата app:enrich-leads може да пребарува потенцијални клиенти чиј статус е pending, или неуспешни потенцијални клиенти чиј прозорец за повторен обид е истечен, и да испрати една порака DetectLeadTechnologies по ID. Одржувајте го тоа пребарување во репозиториумот страницирано, за голем увоз да не го вчита секој ентитет одеднаш.

php bin/console app:enrich-leads
php bin/console messenger:consume async \
  --time-limit=3600 \
  --memory-limit=128M \
  --limit=500

Извршувајте го работникот под systemd, Supervisor или управувачот со процеси што го обезбедува хостинг-платформата. Рестартирајте ги работниците при распоредувања, бидејќи долготрајните PHP-процеси не вчитуваат автоматски нов код или ротирани вредности на околината.

Тестирајте без повикување на живата услуга

MockHttpClient ги одржува тестовите детерминистички и спречува трошење квота. Овој тест исто така докажува дека неправилно форматираните записи за технологии се отфрлаат, додека валидните докази и пренасочувања ја преживуваат нормализацијата.

<?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);
    }
}

Додадете придружни случаи за HTTP 401, исцрпување при HTTP 429, неправилен JSON, исклучоци при транспорт, отсутен потенцијален клиент и невалидна URL-шема. Никогаш не користете продукциски токен во тест-околината.

Безбедност, операции и вообичаени неуспеси

Поднесувајте само веб-страници што услугата има дозвола да ги провери. Валидирајте ги URL-адресите пред испраќање, ограничете ги шемите на HTTP и HTTPS и не дозволувајте внатрешни шеми како file. Ако корисниците можат да внесуваат произволни URL-адреси, проценете ги ризиците од серверски барања на границата на производот, иако далечинската услуга го врши преземањето.

  • 401 или 403: потврдете ги токенот со опсег на услугата, активацијата на планот и тајната при распоредување. Не обидувајте се повторно автоматски.
  • Грешка за валидација од серијата 400: проверете ги поднесената јавна URL-адреса и договорот. Повторувањето идентичен влез троши квота.
  • 429: почитувајте ограничен backoff, намалете ја конкурентноста на работниците и презакажете ги неуспешните потенцијални клиенти за подоцна.
  • Грешка од серијата 500 или грешка при транспорт: задржете го потенцијалниот клиент, забележете безбедна привремена состојба и обидете се повторно преку контролирана серија.
  • Празни детекции: третирајте валиден празен резултат како успешно скенирање, а не како исклучок.
  • Заглавена состојба на извршување: следете ја староста на скенирањето и повторно ставете во ред записите чиј работник бил прекинат среде задача.

Следете ги бројките и латентноста според исход, а не според веб-страница. Алармирајте при трајни неуспеси на автентикација, невообичаено ограничување на стапката, растечка длабочина на редицата и стари записи running. ID за корелација прикачен на пораката и контекстот на дневникот е корисен, под услов да не содржи акредитив или податоци за потенцијален клиент.

Конечна контролна листа за верификација

  • Вистинскиот токен постои само во складиште за тајни поддржано од околината.
  • Апликацијата ја повикува точната POST-крајна точка со JSON url.
  • Создавањето потенцијални клиенти успева дури и кога детекторот е недостапен.
  • Довербата, доказите, верзиите и пренасочувањата се валидираат пред зачувување.
  • Неуспесите при автентикација и валидација не се повторуваат слепо.
  • Тајмаутите, бројот на повторни обиди и backoff се ограничени.
  • Дневниците не содржат токени, необработени одговори, докази и URL-адреси на потенцијални клиенти.
  • Работниците се рестартираат по распоредување код или ротација на токен.
  • Тестовите засновани на mock се извршуваат без надворешен пристап до мрежата.
  • Обработен потенцијален клиент прикажува читливо резиме на технологии и јасна состојба на скенирање.

Највредниот дел од оваа интеграција не е HTTP-барањето. Тоа е дисциплинираниот пат од ненадежно надворешно набљудување до доверлив CRM-факт: ставен во редица, валидиран, објаснив, набљудлив и безбеден за повторна проверка. Изградете ја таа граница добро и едноставна URL-адреса на веб-страница станува корисен контекст за секој разговор што следува.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.