Nativni PHP 8.3: Dodajte sažetke tehnologija web-mjesta CRM potencijalnim klijentima pomoću AI detektora
Web-stranica potencijalnog klijenta često otkriva više nego oskudan obrazac za kontakt. Sažeta bilješka poput „WordPress 6.5, WooCommerce, Cloudflare; dokazi visoke pouzdanosti; jedno preusmjeravanje” agenciji odmah daje kontekst za uvodne pozive, procjene i tehničke revizije.
Ovaj vodič izrađuje tu značajku za mali CRM pomoću PHP-a 8.3, izvornog cURL-a, SQLitea i naredbe prikladne za cron. Integracija provjerava API granicu, ponovno pokušava samo prikladne neuspjehe, bilježi strukturirana stanja i ostaje testabilna bez mrežnih zahtjeva.
Dobijte pristup detektoru
Najprije registrirajte račun ili se prijavite ako ga već imate.
Otvorite stranicu usluge Website Technology Detector, odaberite dostupan paket Free, Plus ili Pro i dovršite aktivaciju. Zatim otvorite službenu dokumentaciju usluge. Pronađite ploču Service token i kopirajte token ograničen na tu uslugu.
Ova usluga zahtijeva autentifikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Upotrijebit ćemo Bearer zaglavlje jer parametri upita mogu procuriti u zapisnike pristupa i povijest preglednika.
Ponovno generiranje tokena usluge opoziva prethodno aktivni token. Rotaciju tretirajte kao operaciju implementacije: instalirajte zamjenu svugdje gdje se izvršava radnik, provjerite je, a zatim ponovno generirajte ili zamijenite staru vjerodajnicu u skladu sa svojim planom uvođenja.
Potvrdite točnu krajnju točku
Integracija koristi POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Njezin JSON zahtjev sadrži jednu obaveznu vrijednost, url. Testirajte je bez spremanja tokena u povijest ljuske:
read -rsp "Service token: " WEBSITE_DETECTOR_TOKEN
curl --fail-with-body \
--connect-timeout 3 \
--max-time 15 \
-X POST \
-H "Authorization: Bearer ${WEBSITE_DETECTOR_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"url":"https://example.com"}' \
https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies
unset WEBSITE_DETECTOR_TOKEN
Uspješan odgovor sadrži detekcije s ocjenom pouzdanosti, verzije, dokaze i informacije o preusmjeravanju. Odgovor iz ovog testa zadržite samo onoliko dugo koliko je potrebno da ga usporedite s trenutačnom dokumentacijom; javne stranice i dalje mogu otkriti operativne pojedinosti koje možda ne želite u zapisnicima.
Sada pohranite vjerodajnicu u lokalnu konfiguraciju okruženja. Nikada nemojte predati ovu datoteku u repozitorij:
# .env
WEBSITE_DETECTOR_TOKEN=YOUR_SERVICE_TOKEN
CRM_DSN=sqlite:/srv/agency-crm/var/crm.sqlite
# .gitignore
.env
.phpunit.cache/
vendor/
Arhitektura za mali CRM
Dizajn je namjerno skroman. Postojeća tablica leads daje ID potencijalnog klijenta i URL javne web-stranice. Naredba odabire rad na čekanju, poziva detektor, mapira odgovor u objekte domene, zapisuje čitljiv sažetak i bilježi je li neuspjeh moguće ponoviti.
- API adapter: upravlja autentifikacijom, vremenskim ograničenjima, JSON dekodiranjem i pravilima ponovnih pokušaja.
- Maper domene: provjerava detekcije, verzije, dokaze, pouzdanost i preusmjeravanja prije nego što ih CRM kod vidi.
- Skupna naredba: obogaćuje ograničen broj potencijalnih klijenata i može se izvršavati pod cron-om.
- Stanje baze podataka: čini uspjehe idempotentnima i razlikuje privremene neuspjehe od trajnih.
Izvršavanje u pozadini bolje je od obogaćivanja unutar slanja obrasca. Detekcija zahtijeva vanjski zahtjev, pa bi čekanje prodajnog osoblja povezalo odzivnost CRM-a s mrežnom latencijom. Red bi bio opravdan u većem opsegu, ali zaključana cron naredba s ograničenim opsegom lakša je za upravljanje maloj agenciji.
Izradite PHP 8.3 projekt
Potrebni su vam PHP 8.3 ili noviji, Composer, izvorni cURL, JSON, PDO i PDO SQLite. Jedini paket za vrijeme izvođenja učitava lokalne datoteke okruženja; produkcija može iste varijable izravno ubrizgati.
{
"require": {
"php": "^8.3",
"ext-curl": "*",
"ext-json": "*",
"ext-pdo": "*",
"ext-pdo_sqlite": "*",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"AgencyCrm\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"AgencyCrm\\Tests\\": "tests/"
}
}
}
composer install
composer dump-autoload
mkdir -p src/Detector src/Http bin database tests var
Pretpostavlja se da CRM ima leads(id, website_url). Primijenite ovu migraciju jednom putem svog uobičajenog postupka migracije:
ALTER TABLE leads ADD COLUMN technology_summary TEXT;
ALTER TABLE leads ADD COLUMN technology_status TEXT;
ALTER TABLE leads ADD COLUMN technology_error TEXT;
ALTER TABLE leads ADD COLUMN technology_checked_at TEXT;
CREATE INDEX leads_technology_status_idx
ON leads (technology_status);
Izradite ograničeni cURL prijenos
Prijenos utvrđuje krajnju točku u aplikacijskom kodu, koristi kratka vremenska ograničenja povezivanja i ukupnog trajanja te vraća samo podatke koji su klijentu potrebni. Držanje proizvoljnih krajnjih točaka izvan konfiguracije okruženja sprječava da kompromitirana konfiguracijska vrijednost preusmjeri token usluge drugamo.
<?php
// src/Http/HttpResponse.php
declare(strict_types=1);
namespace AgencyCrm\Http;
final readonly class HttpResponse
{
public function __construct(
public int $status,
public string $body,
public array $headers
) {}
}
<?php
// src/Http/CurlTransport.php
declare(strict_types=1);
namespace AgencyCrm\Http;
use RuntimeException;
final class CurlTransport
{
public function __invoke(
string $url,
array $headers,
string $json
): HttpResponse {
$handle = curl_init($url);
if ($handle === false) {
throw new RuntimeException('Could not initialize cURL');
}
$responseHeaders = [];
$headerLines = [];
foreach ($headers as $name => $value) {
$headerLines[] = $name . ': ' . $value;
}
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => 3000,
CURLOPT_TIMEOUT_MS => 12000,
CURLOPT_HTTPHEADER => $headerLines,
CURLOPT_POSTFIELDS => $json,
CURLOPT_HEADERFUNCTION => static function (
$handle,
string $line
) use (&$responseHeaders): int {
$length = strlen($line);
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$responseHeaders[strtolower(trim($parts[0]))]
= trim($parts[1]);
}
return $length;
},
]);
$body = curl_exec($handle);
if ($body === false) {
$message = curl_error($handle);
curl_close($handle);
throw new RuntimeException('Detector transport failed: ' . $message);
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
return new HttpResponse($status, $body, $responseHeaders);
}
}
Mapirajte odgovor usluge na jednoj granici
API odgovoru nikada se ne smije vjerovati samo zato što je JSON dekodiranje uspjelo. Maper u nastavku zahtijeva popis detections, preskače neispravne unose, provjerava skalarna polja, zadržava informacije o dokazima i preusmjeravanjima te zanemaruje nepoznate dodatke. Ako službena dokumentacija promijeni ugovor odgovora, ovo je jedina klasa koju treba ažurirati.
<?php
// src/Detector/TechnologyReport.php
declare(strict_types=1);
namespace AgencyCrm\Detector;
use UnexpectedValueException;
final readonly class Detection
{
public function __construct(
public string $name,
public ?float $confidence,
public array $versions,
public array $evidence
) {}
}
final readonly class TechnologyReport
{
public function __construct(
public array $detections,
public array $redirects
) {}
public static function fromPayload(array $payload): self
{
$items = $payload['detections'] ?? null;
if (!is_array($items) || !array_is_list($items)) {
throw new UnexpectedValueException(
'Detector response has no valid detections list'
);
}
$detections = [];
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
$name = $item['name'] ?? null;
if (!is_string($name) || trim($name) === '') {
continue;
}
$confidence = $item['confidence'] ?? null;
$confidence = is_int($confidence) || is_float($confidence)
? (float) $confidence
: null;
$versions = is_array($item['versions'] ?? null)
? array_values(array_filter(
$item['versions'],
static fn (mixed $value): bool =>
is_string($value) && $value !== ''
))
: [];
$evidence = is_array($item['evidence'] ?? null)
? $item['evidence']
: [];
$detections[] = new Detection(
trim($name),
$confidence,
$versions,
$evidence
);
}
$redirects = is_array($payload['redirects'] ?? null)
? $payload['redirects']
: [];
return new self($detections, $redirects);
}
public function summary(): string
{
if ($this->detections === []) {
return 'No technologies detected';
}
$parts = array_map(
static function (Detection $detection): string {
$text = $detection->name;
if ($detection->versions !== []) {
$text .= ' ' . implode(', ', $detection->versions);
}
$details = [];
if ($detection->confidence !== null) {
$details[] = 'confidence '
. rtrim(rtrim(
number_format($detection->confidence, 2, '.', ''),
'0'
), '.');
}
$details[] = count($detection->evidence)
. ' evidence item(s)';
return $text . ' (' . implode('; ', $details) . ')';
},
$this->detections
);
if ($this->redirects !== []) {
$parts[] = count($this->redirects) . ' redirect(s) observed';
}
return implode('; ', $parts);
}
}
Pouzdanost se namjerno prikazuje bez pretpostavke da je njezina ljestvica postotak. Verzije i dokazi nisu obavezni na lokalnoj granici jer neispravni djelomični podaci ne bi trebali srušiti cijelu skupnu obradu.
Dodajte ponovne pokušaje bez stvaranja oluje ponovnih pokušaja
Detekcija je samo za čitanje, stoga je mali budžet ponovnih pokušaja razuman za pogreške prijenosa, ograničavanje brzine i odabrane pogreške poslužitelja. Pogreške provjere i autentifikacije ne pokušavaju se ponovno. Vrijednost Retry-After poštuje se samo kada je cijeli broj i ograničena je na pet sekundi.
<?php
// src/Detector/WebsiteTechnologyClient.php
declare(strict_types=1);
namespace AgencyCrm\Detector;
use AgencyCrm\Http\HttpResponse;
use Closure;
use JsonException;
use RuntimeException;
use Throwable;
final class DetectorException extends RuntimeException
{
public function __construct(
string $message,
public readonly bool $retryable,
public readonly ?int $httpStatus = null
) {
parent::__construct($message);
}
}
final class WebsiteTechnologyClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies';
public function __construct(
private readonly string $token,
private readonly Closure $transport,
private readonly Closure $sleep
) {}
public function detect(string $url): TechnologyReport
{
if (!filter_var($url, FILTER_VALIDATE_URL)) {
throw new DetectorException('Lead URL is invalid', false);
}
$scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
if (!in_array($scheme, ['http', 'https'], true)) {
throw new DetectorException('Lead URL must use HTTP or HTTPS', false);
}
try {
$json = json_encode(['url' => $url], JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
throw new DetectorException('Could not encode request', false);
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = ($this->transport)(
self::ENDPOINT,
[
'Authorization' => 'Bearer ' . $this->token,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
$json
);
} catch (Throwable $exception) {
if ($attempt === 3) {
throw new DetectorException(
'Detector transport unavailable',
true
);
}
($this->sleep)(250 * (2 ** ($attempt - 1)));
continue;
}
if ($response->status >= 200 && $response->status < 300) {
return $this->mapResponse($response);
}
$retryable = $response->status === 429
|| in_array($response->status, [500, 502, 503, 504], true);
if (!$retryable || $attempt === 3) {
throw new DetectorException(
'Detector returned HTTP ' . $response->status,
$retryable,
$response->status
);
}
$retryAfter = $response->headers['retry-after'] ?? null;
$delay = is_string($retryAfter) && ctype_digit($retryAfter)
? min(5000, max(250, (int) $retryAfter * 1000))
: 250 * (2 ** ($attempt - 1));
($this->sleep)($delay);
}
throw new DetectorException('Retry loop exhausted', true);
}
private function mapResponse(HttpResponse $response): TechnologyReport
{
try {
$payload = json_decode(
$response->body,
true,
512,
JSON_THROW_ON_ERROR
);
} catch (JsonException $exception) {
throw new DetectorException(
'Detector returned invalid JSON',
false,
$response->status
);
}
if (!is_array($payload)) {
throw new DetectorException(
'Detector returned an invalid document',
false,
$response->status
);
}
try {
return TechnologyReport::fromPayload($payload);
} catch (Throwable $exception) {
throw new DetectorException(
'Detector response did not match its contract',
false,
$response->status
);
}
}
}
Obogatite CRM potencijalne klijente na čekanju
Naredba ograničava svako pokretanje, ažurira jednog potencijalnog klijenta odjednom i bilježi identifikatore i stanja umjesto vjerodajnica, tijela odgovora, dokaza ili potpunih URL-ova. Trajni neuspjesi ostaju vidljivi radi ispravka; privremeni neuspjesi mogu se ponovno odabrati.
<?php
// bin/enrich-leads.php
declare(strict_types=1);
use AgencyCrm\Detector\DetectorException;
use AgencyCrm\Detector\WebsiteTechnologyClient;
use AgencyCrm\Http\CurlTransport;
use Dotenv\Dotenv;
require dirname(__DIR__) . '/vendor/autoload.php';
Dotenv::createImmutable(dirname(__DIR__))->safeLoad();
$token = $_ENV['WEBSITE_DETECTOR_TOKEN']
?? getenv('WEBSITE_DETECTOR_TOKEN')
?: null;
$dsn = $_ENV['CRM_DSN'] ?? getenv('CRM_DSN') ?: null;
if (!is_string($token) || $token === '' || !is_string($dsn) || $dsn === '') {
fwrite(STDERR, "Missing WEBSITE_DETECTOR_TOKEN or CRM_DSN\n");
exit(1);
}
$transport = new CurlTransport();
$sleep = static fn (int $milliseconds): int =>
usleep($milliseconds * 1000);
$client = new WebsiteTechnologyClient(
$token,
Closure::fromCallable($transport),
$sleep(...)
);
$pdo = new PDO($dsn, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$select = $pdo->prepare(
"SELECT id, website_url
FROM leads
WHERE website_url IS NOT NULL
AND (
technology_status IS NULL
OR technology_status IN ('pending', 'retryable')
)
ORDER BY id
LIMIT :batch_size"
);
$select->bindValue(':batch_size', 25, PDO::PARAM_INT);
$select->execute();
$update = $pdo->prepare(
"UPDATE leads
SET technology_summary = :summary,
technology_status = :status,
technology_error = :error,
technology_checked_at = :checked_at
WHERE id = :id"
);
foreach ($select as $lead) {
try {
$report = $client->detect((string) $lead['website_url']);
$update->execute([
'summary' => $report->summary(),
'status' => 'complete',
'error' => null,
'checked_at' => gmdate('c'),
'id' => $lead['id'],
]);
error_log(json_encode([
'event' => 'technology_detection_complete',
'lead_id' => $lead['id'],
'detection_count' => count($report->detections),
], JSON_THROW_ON_ERROR));
} catch (DetectorException $exception) {
$status = $exception->retryable ? 'retryable' : 'failed';
$update->execute([
'summary' => null,
'status' => $status,
'error' => $exception->getMessage(),
'checked_at' => gmdate('c'),
'id' => $lead['id'],
]);
error_log(json_encode([
'event' => 'technology_detection_failed',
'lead_id' => $lead['id'],
'status' => $status,
'http_status' => $exception->httpStatus,
], JSON_THROW_ON_ERROR));
}
}
Testirajte bez pozivanja vanjske usluge
Deterministički lažni prijenos omogućuje testu da provjeri mapiranje, vrijeme ponovnih pokušaja i ponašanje autentifikacije bez trošenja kvote ili ovisnosti o mreži.
<?php
// tests/WebsiteTechnologyClientTest.php
declare(strict_types=1);
namespace AgencyCrm\Tests;
use AgencyCrm\Detector\DetectorException;
use AgencyCrm\Detector\WebsiteTechnologyClient;
use AgencyCrm\Http\HttpResponse;
use PHPUnit\Framework\TestCase;
final class WebsiteTechnologyClientTest extends TestCase
{
public function testItBuildsReadableSummary(): void
{
$transport = static fn (): HttpResponse => new HttpResponse(
200,
json_encode([
'detections' => [[
'name' => 'Example CMS',
'confidence' => 92,
'versions' => ['6.5'],
'evidence' => ['generator metadata', 'asset path'],
]],
'redirects' => [['from' => 'http', 'to' => 'https']],
], JSON_THROW_ON_ERROR),
[]
);
$client = new WebsiteTechnologyClient(
'test-token',
$transport(...),
static fn (int $milliseconds): null => null
);
$summary = $client->detect('https://example.com')->summary();
self::assertSame(
'Example CMS 6.5 (confidence 92; 2 evidence item(s)); '
. '1 redirect(s) observed',
$summary
);
}
public function testItRetriesRateLimitOnce(): void
{
$responses = [
new HttpResponse(429, '{}', ['retry-after' => '1']),
new HttpResponse(200, '{"detections":[],"redirects":[]}', []),
];
$delays = [];
$transport = static function () use (&$responses): HttpResponse {
return array_shift($responses);
};
$sleep = static function (int $milliseconds) use (&$delays): void {
$delays[] = $milliseconds;
};
$client = new WebsiteTechnologyClient(
'test-token',
$transport(...),
$sleep(...)
);
$client->detect('https://example.com');
self::assertSame([1000], $delays);
}
public function testItDoesNotRetryAuthenticationFailure(): void
{
$calls = 0;
$transport = static function () use (&$calls): HttpResponse {
$calls++;
return new HttpResponse(401, '{}', []);
};
$client = new WebsiteTechnologyClient(
'test-token',
$transport(...),
static fn (int $milliseconds): null => null
);
try {
$client->detect('https://example.com');
self::fail('Expected DetectorException');
} catch (DetectorException $exception) {
self::assertFalse($exception->retryable);
self::assertSame(401, $exception->httpStatus);
self::assertSame(1, $calls);
}
}
}
vendor/bin/phpunit --testdox tests
Sigurnost, vidljivost i implementacija
Šaljite samo javne web-stranice za čiju obradu vaš CRM ima ovlaštenje. Ponovno provjerite URL-ove kada se unesu u CRM, ograničite sheme na HTTP i HTTPS te ne izlažite ovu naredbu kao proxy kojim upravlja korisnik. Dokaze i sirove odgovore držite izvan općih zapisnika aplikacije.
Radniku dodijelite samo dozvole baze podataka koje su mu potrebne. Lokalno zaštitite .env restriktivnim dozvolama datotečnog sustava; u produkciji radije koristite spremište tajni platforme za implementaciju ili ubrizgane varijable okruženja. Vrijednosti tokena nikada se ne smiju pojaviti u testnim podacima, porukama iznimki, snimkama zaslona ili oznakama za nadzor.
Pratite brojeve dovršenih, ponovljivih i trajnih neuspjeha, kao i trajanje zahtjeva i klasu HTTP statusa. Upozorite na dugotrajne neuspjehe autentifikacije, ponavljano ograničavanje brzine ili nagli porast pogrešaka mapiranja ugovora. Ti signali razlikuju istekli token od pritiska kvote ili promjene odgovora uzvodne usluge.
Implementirajte ovisnosti pomoću composer install --no-dev --classmap-authoritative, primijenite migraciju, ubrizgajte token i DSN te ručno pokrenite jednog potencijalnog klijenta. Zatim rasporedite radnika s blokadom operacijskog sustava kako se preklapajuća cron pokretanja ne bi bavila istom skupinom:
flock -n /var/lock/agency-crm-enrichment.lock \
php /srv/agency-crm/bin/enrich-leads.php
Uobičajeni neuspjesi za koje vrijedi projektirati
- HTTP 401 ili 403: potvrdite aktivaciju paketa i token ograničen na uslugu. Ako je ponovno generiran, prethodni token je opozvan. Nemojte automatski pokušavati ponovno.
- HTTP 400 ili 422: pregledajte pohranjeni URL potencijalnog klijenta i trenutačnu dokumentaciju. Ispravite ulaz umjesto da ga ponovno pokušavate.
- HTTP 429: poštujte ograničeni povratni odmak, smanjite učestalost skupne obrade i provjerite aktivni paket ili kvotu.
- Istek vremena ili odabrani odgovori 5xx: dopustite budžet od tri pokušaja, a zatim zadržite
retryableza kasnije zakazano pokretanje. - Pogreške mapiranja ugovora: usporedite sanitizirani odgovor sa službenom dokumentacijom i ažurirajte samo granični maper.
- Prazne detekcije: tretirajte ovo kao valjan rezultat, a ne kao operativni neuspjeh. Javna stranica može pružiti nedovoljno determinističkih dokaza.
Završni kontrolni popis za provjeru
- Račun i paket usluge Free, Plus ili Pro aktivni su.
- Trenutačni token usluge pohranjen je samo u konfiguraciji koja se oslanja na okruženje.
- Minimalni POST zahtjev uspijeva s JSON tijelom koje sadrži
url. - PHPUnit prolazi bez stvarnog mrežnog zahtjeva.
- Potencijalni klijent na čekanju postaje
completes čitljivim sažetkom tehnologija. - 401 postaje
failednakon jednog pokušaja, dok privremeni prekid postajeretryable. - Zapisnici sadrže ID-jeve potencijalnih klijenata, stanja ishoda i statusne kodove, ali ne token, sirove dokaze, tijelo odgovora ni puni URL.
- Produkcijska naredba ima ograničene skupine, vremenska ograničenja, ponovne pokušaje i blokadu preklapanja.
Korisni dio ove značajke nije samo to što otkriva CMS ili CDN. Ona pretvara vanjske tehničke dokaze u pouzdan CRM kontekst: dovoljno sažet za zapis potencijalnog klijenta, dovoljno strukturiran za siguran rad i dovoljno izoliran da se razvija kada se promijeni ugovor usluge.