Vodiči

Symfony: Uncover Lead Website Tech Stacks for Smarter Agency Outreach

Symfony: Otkrijte tehnološke skupove vodećih web-stranica za pametnije agencijsko obraćanje

Web-stranica potencijalnog klijenta može otkriti odgovara li prirodno agenciji prije nego što itko napiše prvi e-mail za kontakt. Symfony stručnjaku potreban je drukčiji razgovor s potencijalnim klijentom koji koristi Symfony nego s onim koji koristi WordPress, Shopify ili neidentificirani prilagođeni skup tehnologija.

Ovaj vodič dodaje taj kontekst malom agencijskom CRM-u temeljenom na Symfonyju. Planirana naredba šalje javnu web-stranicu svakog potencijalnog klijenta API-ju Website Technology Detector, mapira rezultate s ocjenom pouzdanosti na obrambenoj granici aplikacije i pohranjuje sažet prikaz poput Symfony 7 (pouzdanost 98); PHP 8.3 (pouzdanost 92).

Implementacija daje prednost jednostavnoj, pouzdanoj skupnoj naredbi u odnosu na sinkroni zahtjev iz CRM sučelja. Otkrivanje može uključivati preusmjeravanja i udaljeni mrežni rad; ništa od toga ne bi smjelo usporiti stranicu na kojoj netko uređuje potencijalnog klijenta.

Pribavite pristup prije pisanja integracijskog koda

  1. Otvorite račun na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako ga već imate.
  2. Otvorite stranicu usluge Website Technology Detector.
  3. Odaberite dostupni plan Free, Plus ili Pro i dovršite njegovu aktivaciju.
  4. Otvorite službenu dokumentaciju usluge.
  5. Pronađite ploču Service token i kopirajte ondje prikazani token za tu uslugu.

Ova usluga zahtijeva autentikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Donja aplikacija koristi oblik Bearer jer vjerodajnice ne izlaže URL-ovima, povijesti preglednika, zapisnicima proxyja i uobičajenim zapisnicima pristupa.

Ponovnim generiranjem tokena usluge opoziva se prethodno aktivni token. Rotaciju tretirajte kao promjenu pri implementaciji: instalirajte novu vrijednost svugdje gdje se naredba izvršava, provjerite je i očekujte da procesi koji još koriste staru vrijednost prime pogrešku autentikacije.

Potvrdite točnu krajnju točku

Potrebni poziv je POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Njegov JSON zahtjev sadrži jedno polje, url. Prije izmjene CRM-a pošaljite minimalni zahtjev iz sigurnog terminala:

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"}'

Nikada ne lijepite stvarni token u povijest ljuske koju dijelite s drugim korisnicima. Za ponavljano testiranje učitajte ga iz zaštićene varijable okruženja i navedite tu varijablu u naredbi.

Pohranite token u Symfony konfiguraciju

Zadržite rezervirano mjesto u predanoj datoteci .env, a stvarnu tajnu tijekom lokalnog razvoja stavite u nepredanu datoteku .env.local. U produkciji je unesite putem konfiguracije tajni ili okruženja hosting platforme.

# .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)%'

Projekt treba Symfonyjev HTTP klijent prve strane. Ako već nije instaliran, dodajte ga naredbom composer require symfony/http-client. Testovi koriste MockHttpClient iz paketa pa ne upućuju vanjske zahtjeve.

Oblikujte značajku oko uske API granice

Integracija ima tri odgovornosti:

  • TechnologyDetector upravlja autentikacijom, vremenskim ograničenjima, ponovnim pokušajima, obradom statusa i dekodiranjem JSON-a.
  • TechnologyReport pretvara nesiguran vanjski JSON u stabilne podatke domene.
  • RefreshLeadTechnologiesCommand pregledava potencijalne klijente i sprema sažetke bez uključivanja web-zahtjeva.

Detektor može vratiti otkrivene tehnologije, verzije, dokaze, ocjene pouzdanosti i informacije o preusmjeravanju. Ti koncepti pripadaju modelu domene, ali vanjski JSON ne bi smio propuštati kroz cijeli CRM. Smještaj polja može varirati, stoga mapper pretražuje ugniježđene objekte i prihvaća samo vrijednosti odgovarajućeg tipa. Sačuvajte neobrađeni uspješni teret za dijagnostiku, uz razumno ograničenje veličine baze podataka i politiku zadržavanja.

Obrambeno mapirajte otkrivene tehnologije

Izradite src/Integration/TechnologyReport.php. Ovaj mapper ne pretpostavlja da se otkrivene tehnologije nalaze pod određenom omotnicom odgovora. Kandidat mora imati neprazan naziv i numeričku vrijednost pouzdanosti. Verzije i dokazi ostaju neobavezni.

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

Primijetite da sažetak vrijednost naziva ocjenom pouzdanosti umjesto da dodaje znak postotka. Time se izbjegava prešutno navođenje ljestvice koja nije dio dostavljenog ugovora. Dokazi ostaju dostupni za reviziju ili buduću ploču s pojedinostima, bez zatrpavanja popisa potencijalnih klijenata.

Izgradite otporan HTTP klijent

Izradite malu iznimku koja sadrži strojno čitljivu vrstu neuspjeha:

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

Sada izradite src/Integration/TechnologyDetector.php. Ograničenja povezivanja i ukupnog trajanja sprječavaju da spora ovisnost neograničeno zauzima naredbu. Ponovno se pokušavaju samo transportni neuspjesi, HTTP 429 i pogreške poslužitelja. Pogreške autentikacije i provjere valjanosti konačne su jer ih drugi identičan poziv neće ispraviti.

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

Zapisnik treba evidentirati identifikator potencijalnog klijenta, vrstu neuspjeha, statusni kod, ishod pokušaja i trajanje. Ne bi smio bilježiti zaglavlje Authorization, token, cijelo tijelo odgovora ni niz upita URL-a. Primjer zadržava bilježenje na razini naredbe kako bi imao koristan kontekst potencijalnog klijenta.

Spremite stanje vidljivo CRM-u

Dodajte ova polja postojećem Doctrine entitetu Lead, zatim generirajte i pregledajte migraciju naredbom php bin/console make:migration te je primijenite naredbom 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);
}

Zadržavanje prethodnog sažetka pri neuspjehu namjerno je. Privremeni prekid rada ne bi smio izbrisati koristan prodajni kontekst. Stanje i vremenska oznaka govore sučelju da prikazani rezultat možda nije ažuran.

Osvježavajte potencijalne klijente planiranom naredbom

Izradite src/Command/RefreshLeadTechnologiesCommand.php. Za mali CRM sekvencijalna obrada omogućuje lako razumljive kvote i ponašanje pri neuspjehu. Povremeno izvršavajte flush kako ORM ne bi zadržavao svakog potencijalnog klijenta tijekom cijelog izvršavanja.

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

Planirajte php bin/console app:refresh-lead-technologies --env=prod u intervalu primjerenom planu i učestalosti promjena web-stranica potencijalnih klijenata. Konfigurirajte raspoređivač tako da spriječi preklapanje izvršavanja. Za veću bazu podataka upitom dohvaćajte samo zastarjele zapise ili zapise na čekanju te uvedite ograničenje skupine temeljeno na kursoru prije razmatranja Messenger radnika.

Testirajte bez trošenja kvote

MockHttpClient provjerava odlazni zahtjev i isporučuje deterministički odgovor. Donji test također potvrđuje da sažetak domene uključuje podatke o verziji i pouzdanosti.

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

Fiksni odgovor vježba dokumentirane semantičke ulaze mappera; nije zamjena za provjeru aktualnih službenih primjera odgovora. Dodajte snimljene, sanitizirane ugovorne fiksne odgovore tek nakon uklanjanja korisničkih URL-ova, tokena i nepotrebnih dokaza.

Implementirajte i provjerite cijeli put

Unesite produkcijski token, pokrenite pregledanu Doctrine migraciju, zagrijte Symfony predmemoriju i izvršite jedno ručno osvježavanje prije uključivanja rasporeda. Postavite upozorenja za ponovljene neuspjehe autentikacije, trajno ograničavanje brzine, rastuće transportne neuspjehe i neuobičajeno dugo trajanje naredbe. Status 429 obično zahtijeva manju učestalost rasporeda ili pregled plana, a ne agresivne ponovne pokušaje.

Uobičajeni neuspjesi najčešće su jednostavni: 401 ili 403 znači da token nedostaje, opozvan je ili je pogrešno priložen; 400 ili 422 upućuje na nepravilno oblikovan ili nepodržan URL; ponovljeni odgovori 429 ukazuju na iscrpljenu kvotu ili prekomjernu istodobnost; transportne pogreške sugeriraju probleme s DNS-om, TLS-om, vatrozidom ili dostupnošću uzvodne usluge; a uspješan odgovor bez mapiranih otkrivenih tehnologija zahtijeva usporedbu sanitiziranog tereta sa službenom dokumentacijom i ažuriranje samo mappera na granici.

Završni kontrolni popis provjere

  • Token postoji samo u konfiguraciji temeljenoj na okruženju i nikada se ne pojavljuje u zapisnicima ni fiksnim odgovorima.
  • Aplikacija šalje JSON url na točnu HTTPS krajnju točku uz Bearer autentikaciju.
  • Vrijeme povezivanja i ukupno vrijeme odgovora ograničeni su.
  • Ponovno se pokušavaju samo prolazni transportni neuspjesi, ograničenja brzine i pogreške poslužitelja.
  • Otkrivene tehnologije, pouzdanost, verzije, dokazi i informacije o preusmjeravanju obrambeno se mapiraju ili zadržavaju.
  • Neuspješno osvježavanje čuva posljednji korisni sažetak i bilježi strukturirano stanje.
  • Automatizirani testovi ne upućuju pozive stvarnoj usluzi.
  • Produkcijski raspoređivač sprječava preklapanje izvršavanja i poštuje ograničenja plana.
  • Svaki pregledani potencijalni klijent prikazuje čitljiv sažetak sa stanjem i vremenskom oznakom pregleda.

Vrijedan rezultat nije gomila JSON-a za otkrivanje tehnologija. To je jedan miran, čitljiv redak uz svakog potencijalnog klijenta: dovoljno tehničkog konteksta za oblikovanje relevantnog razgovora, potkrijepljen dokazima kad ih netko treba pregledati i održavan integracijom koja predvidljivo ne uspijeva umjesto da iznenađuje korisnike CRM-a.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.