Native PHP 8.3: Збогатете ги потенцијалните клиенти со увид во технологиите на веб-страниците со користење на Detector API
Веб-страницата на еден потенцијален клиент често открива повеќе отколку оскуден контакт-формулар. Технолошкото резиме може да ѝ каже на мала агенција дали потенцијалниот клиент користи WordPress, одредена аналитичка платформа или апликациски стек што одговара на експертизата на тимот. Корисната верзија на оваа функционалност не е екстензија за прелистувач или рачна проверка: туку збогатување што се појавува директно покрај секој потенцијален клиент.
Овој водич го гради тој работен тек во Native PHP 8.3. Команда чита незбогатени потенцијални клиенти од постоечки CRM поддржан од SQLite, го повикува API-то Website Technology Detector, ги претвора резултатите со оценки за доверба во концизно резиме и запишува структурирани состојби на успех или неуспех. Границата кон API-то користи изворен cURL, ограничени тајмаути, селективни повторни обиди, дефанзивно мапирање на JSON и транспортна апстракција што ги прави PHPUnit тестовите детерминистички.
Добијте пристап и создајте сервисен токен
- Отворете ја страницата за регистрација и создајте сметка, или користете ја страницата за најава ако веќе имате сметка.
- Посетете ја страницата на услугата Website Technology Detector. Изберете достапен Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата, пронајдете го панелот Service token и копирајте го токенот ограничен на услугата.
- Зачувајте ја таа вредност во конфигурацијата на околината на проектот. Никогаш не ја зачувувајте во контрола на изворниот код.
Повторното генерирање на сервисниот токен го поништува претходно активниот токен. Третирајте ја ротацијата како промена при распоредување: ажурирајте ја секоја околина што ја користи старата вредност, рестартирајте ги релевантните PHP worker-и или процеси на команди и потврдете ја автентикацијата пред да ја отстраните можноста за враќање назад.
Оваа услуга бара автентикација. Прифаќа Bearer токен, заглавие X-API-Token или параметар за пребарување token. Имплементацијата подолу ја користи Bearer формата бидејќи ги држи ингеренциите надвор од URL-адресите, логовите за пристап, историјата на прелистувачот и аналитиката на проксијата.
Прво потврдете ја точната крајна точка
Повикот на детекторот е POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Неговото JSON тело на барањето содржи url. Пред да напишете апликациски код, направете едно минимално барање од доверлив терминал:
export WEBSITE_DETECTOR_TOKEN='YOUR_SERVICE_TOKEN'
curl --fail-with-body \
--connect-timeout 3 \
--max-time 20 \
-X POST \
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies' \
-H "Authorization: Bearer ${WEBSITE_DETECTOR_TOKEN}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{"url":"https://example.com"}'
За локален развој, создајте непратена .env датотека и предадена датотека со место за вредност именувана .env.example:
# .env.example
WEBSITE_DETECTOR_TOKEN=YOUR_SERVICE_TOKEN
CRM_DATABASE_PATH=var/crm.sqlite
cp .env.example .env
# Replace only the placeholder in .env, then load it into the process:
set -a
. ./.env
set +a
Додадете ги .env и var/ во .gitignore. Native PHP не чита автоматски dotenv датотеки, па школката експлицитно ја извезува датотеката пред извршување на командата. Во продукција, внесете ги истите променливи преку управувачот со процеси или складиштето за тајни наместо да вчитувате датотека од складиштето.
Архитектура што одговара на мал CRM
Збогатувањето на потенцијални клиенти е надворешен I/O, па не треба да го одложува екранот каде што корисникот создава потенцијален клиент. За мал CRM доволна е закажана команда и се избегнува воведување систем за редици само за една функционалност. Таа избира ограничена серија, ја валидира секоја веб-страница, го повикува детекторот и запишува читливо резиме.
API клиентот го поседува HTTP однесувањето и автентикацијата. Маперот ја поседува границата кон недоверливиот одговор. Командата ја поседува CRM политиката: кои потенцијални клиенти се квалификуваат, што се случува по привремен неуспех и дали грешка при автентикација треба да ја запре серијата.
bin/enrich-leads
src/Detector/CurlTransport.php
src/Detector/DetectorClient.php
src/Detector/DetectionReport.php
src/Detector/HttpResponse.php
src/Detector/HttpTransport.php
src/Detector/ServiceFailure.php
tests/DetectorClientTest.php
composer.json
.env.example
Инсталирајте ја само зависноста за тестирање; продукцијата ги користи PHP екстензиите cURL и PDO:
{
"require": {
"php": "^8.3",
"ext-curl": "*",
"ext-json": "*",
"ext-pdo": "*",
"ext-pdo_sqlite": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"scripts": {
"test": "phpunit tests"
}
}
Изградете заменлив изворен cURL транспорт
Задржувањето на cURL зад мал интерфејс спречува мрежните повици да протечат во тестовите. Исто така, им дава различни значења на тајмаутите за поврзување и за целосен одговор: првиот го ограничува воспоставувањето врска, додека вториот ја ограничува целата размена.
<?php
// src/Detector/HttpTransport.php
namespace App\Detector;
interface HttpTransport
{
public function post(
string $url,
array $headers,
string $body,
int $connectTimeoutMs,
int $timeoutMs
): HttpResponse;
}
final readonly class HttpResponse
{
public function __construct(
public int $status,
public array $headers,
public string $body
) {}
}
final class TransportFailure extends \RuntimeException {}
// src/Detector/CurlTransport.php
final class CurlTransport implements HttpTransport
{
public function post(
string $url,
array $headers,
string $body,
int $connectTimeoutMs,
int $timeoutMs
): HttpResponse {
$responseHeaders = [];
$handle = curl_init($url);
if ($handle === false) {
throw new TransportFailure('Unable to initialize cURL');
}
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT_MS => $connectTimeoutMs,
CURLOPT_TIMEOUT_MS => $timeoutMs,
CURLOPT_HEADERFUNCTION => static function (
$curl,
string $line
) use (&$responseHeaders): int {
$separator = strpos($line, ':');
if ($separator !== false) {
$name = strtolower(trim(substr($line, 0, $separator)));
$responseHeaders[$name] = trim(substr($line, $separator + 1));
}
return strlen($line);
},
]);
$bodyText = curl_exec($handle);
if ($bodyText === false) {
$message = curl_error($handle);
curl_close($handle);
throw new TransportFailure($message);
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
return new HttpResponse($status, $responseHeaders, $bodyText);
}
}
Мапирајте го API одговорот на една дефанзивна граница
Далечинскиот JSON е недоверлив дури и по успешен HTTP статус. Записите за технологии може да содржат информации за доверба, доказ и верзија, додека податоците за пренасочување опишуваат како се разрешила бараната адреса. Незадолжителните или неправилно форматирани вредности не смеат да создаваат известувања или да го оштетат CRM-от.
Маперот подолу ги прифаќа документираните поими за технологии само кога нивните типови при извршување се употребливи. Тој задржува нормализиран доказ за идни дијагностики, но резимето наменето за потенцијалниот клиент останува компактно.
<?php
// src/Detector/DetectionReport.php
namespace App\Detector;
final readonly class DetectionReport
{
public function __construct(
public array $technologies,
public array $redirects
) {}
public static function fromJson(array $document): self
{
$payload = is_array($document['data'] ?? null)
? $document['data']
: $document;
$rawTechnologies = $payload['technologies'] ?? [];
$technologies = [];
if (is_array($rawTechnologies)) {
foreach ($rawTechnologies as $item) {
if (!is_array($item) || !is_string($item['name'] ?? null)) {
continue;
}
$confidence = $item['confidence'] ?? null;
$technologies[] = [
'name' => trim($item['name']),
'version' => is_string($item['version'] ?? null)
? trim($item['version'])
: null,
'confidence' => is_numeric($confidence)
? max(0.0, min(100.0, (float) $confidence))
: null,
'evidence' => is_array($item['evidence'] ?? null)
? $item['evidence']
: [],
];
}
}
usort(
$technologies,
static fn (array $a, array $b): int =>
($b['confidence'] ?? -1) <=> ($a['confidence'] ?? -1)
);
$redirects = is_array($payload['redirects'] ?? null)
? $payload['redirects']
: [];
return new self($technologies, $redirects);
}
public function summary(int $limit = 6): string
{
if ($this->technologies === []) {
return 'No technologies detected';
}
$parts = array_map(
static function (array $technology): string {
$label = $technology['name'];
if ($technology['version'] !== null &&
$technology['version'] !== '') {
$label .= ' ' . $technology['version'];
}
if ($technology['confidence'] !== null) {
$label .= sprintf(
' (%.0f%% confidence)',
$technology['confidence']
);
}
return $label;
},
array_slice($this->technologies, 0, $limit)
);
$suffix = count($this->technologies) > $limit
? sprintf(' and %d more', count($this->technologies) - $limit)
: '';
return implode(', ', $parts) . $suffix;
}
}
Чувајте го суровиот доказ во меморија или во наменско складиште за ревизија само кога CRM-от има потреба од него. Доказот може да биде обемен, а неограниченото зачувување на цел одговор од давателот создава непотребни обврски за складирање и приватност.
Додадете селективни повторни обиди и структурирани неуспеси
Повторен обид е соодветен при неуспех на врска, HTTP 429 или серверски одговор 5xx. Несоодветен е за невалиден влез или одбиени ингеренции. Повторното обидување за тие одговори само троши квота и ги прикрива дефектите во конфигурацијата.
<?php
// src/Detector/DetectorClient.php
namespace App\Detector;
final class ServiceFailure extends \RuntimeException
{
public function __construct(
public readonly string $reason,
public readonly bool $retryable,
public readonly ?int $status = null
) {
parent::__construct($reason);
}
}
final readonly class DetectorClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies';
public function __construct(
private HttpTransport $transport,
private string $token,
private \Closure $sleep
) {
if ($token === '') {
throw new \InvalidArgumentException('Detector token is missing');
}
}
public function detect(string $url): DetectionReport
{
$body = json_encode(['url' => $url], JSON_THROW_ON_ERROR);
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->transport->post(
self::ENDPOINT,
[
'Authorization: Bearer ' . $this->token,
'Accept: application/json',
'Content-Type: application/json',
],
$body,
3000,
20000
);
} catch (TransportFailure $exception) {
if ($attempt === 3) {
throw new ServiceFailure('transport_error', true);
}
($this->sleep)($attempt === 1 ? 250 : 750);
continue;
}
if ($response->status >= 200 && $response->status < 300) {
try {
$json = json_decode(
$response->body,
true,
512,
JSON_THROW_ON_ERROR
);
} catch (\JsonException) {
throw new ServiceFailure(
'invalid_response',
false,
$response->status
);
}
if (!is_array($json)) {
throw new ServiceFailure(
'invalid_response',
false,
$response->status
);
}
return DetectionReport::fromJson($json);
}
if (in_array($response->status, [401, 403], true)) {
throw new ServiceFailure(
'authentication_failed',
false,
$response->status
);
}
if (in_array($response->status, [400, 422], true)) {
throw new ServiceFailure(
'invalid_request',
false,
$response->status
);
}
$retryable = $response->status === 429
|| $response->status >= 500;
if (!$retryable || $attempt === 3) {
throw new ServiceFailure(
$response->status === 429
? 'rate_limited'
: 'upstream_error',
$retryable,
$response->status
);
}
$retryAfter = $response->headers['retry-after'] ?? null;
$delayMs = ctype_digit((string) $retryAfter)
? min(10000, (int) $retryAfter * 1000)
: ($attempt === 1 ? 250 : 750);
($this->sleep)($delayMs);
}
throw new ServiceFailure('unexpected_state', false);
}
}
Збогатете ги потенцијалните клиенти без губење добри податоци
Додадете четири nullable колони во постоечката табела leads на CRM-от. Приспособете ја синтаксата за миграција ако вашата апликација користи друга база на податоци:
ALTER TABLE leads ADD COLUMN tech_summary TEXT NULL;
ALTER TABLE leads ADD COLUMN tech_status TEXT NULL;
ALTER TABLE leads ADD COLUMN tech_checked_at TEXT NULL;
ALTER TABLE leads ADD COLUMN tech_error TEXT NULL;
Командата обработува ограничена серија. Привремен неуспех запишува состојба што може повторно да се обиде, но зачувува какво било претходно резиме. Неуспех при автентикација запира веднаш бидејќи секое наредно барање би пропаднало од истата причина.
<?php
// bin/enrich-leads
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use App\Detector\CurlTransport;
use App\Detector\DetectorClient;
use App\Detector\ServiceFailure;
$token = getenv('WEBSITE_DETECTOR_TOKEN');
$databasePath = getenv('CRM_DATABASE_PATH') ?: 'var/crm.sqlite';
$client = new DetectorClient(
new CurlTransport(),
is_string($token) ? $token : '',
static fn (int $milliseconds) => usleep($milliseconds * 1000)
);
$pdo = new PDO('sqlite:' . $databasePath, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$leads = $pdo->query(
"SELECT id, website FROM leads
WHERE website IS NOT NULL
AND website != ''
AND (tech_status IS NULL OR tech_status = 'retry')
ORDER BY id
LIMIT 50"
)->fetchAll(PDO::FETCH_ASSOC);
$success = $pdo->prepare(
"UPDATE leads
SET tech_summary = :summary,
tech_status = 'complete',
tech_checked_at = :checked,
tech_error = NULL
WHERE id = :id"
);
$failure = $pdo->prepare(
"UPDATE leads
SET tech_status = :status,
tech_checked_at = :checked,
tech_error = :error
WHERE id = :id"
);
foreach ($leads as $lead) {
$url = filter_var($lead['website'], FILTER_VALIDATE_URL);
$scheme = is_string($url) ? parse_url($url, PHP_URL_SCHEME) : null;
if ($url === false || !in_array($scheme, ['http', 'https'], true)) {
$failure->execute([
'status' => 'invalid_url',
'checked' => gmdate(DATE_ATOM),
'error' => 'Website must be an HTTP or HTTPS URL',
'id' => $lead['id'],
]);
continue;
}
try {
$report = $client->detect($url);
$success->execute([
'summary' => $report->summary(),
'checked' => gmdate(DATE_ATOM),
'id' => $lead['id'],
]);
error_log(json_encode([
'event' => 'lead_technology_enriched',
'lead_id' => $lead['id'],
'technology_count' => count($report->technologies),
'redirect_count' => count($report->redirects),
], JSON_THROW_ON_ERROR));
} catch (ServiceFailure $exception) {
if ($exception->reason === 'authentication_failed') {
throw $exception;
}
$failure->execute([
'status' => $exception->retryable ? 'retry' : 'failed',
'checked' => gmdate(DATE_ATOM),
'error' => $exception->reason,
'id' => $lead['id'],
]);
error_log(json_encode([
'event' => 'lead_technology_enrichment_failed',
'lead_id' => $lead['id'],
'reason' => $exception->reason,
'http_status' => $exception->status,
'retryable' => $exception->retryable,
], JSON_THROW_ON_ERROR));
}
}
Тестирајте без да ја контактирате услугата
Лажниот транспорт враќа одговори ставени во ред, додека вметнатиот sleeper го бележи backoff-от наместо да го паузира тест-пакетот. Фикстурите не содржат ингеренции.
<?php
// tests/DetectorClientTest.php
declare(strict_types=1);
use App\Detector\DetectorClient;
use App\Detector\HttpResponse;
use App\Detector\HttpTransport;
use PHPUnit\Framework\TestCase;
final class FakeTransport implements HttpTransport
{
public int $calls = 0;
public function __construct(private array $responses) {}
public function post(
string $url,
array $headers,
string $body,
int $connectTimeoutMs,
int $timeoutMs
): HttpResponse {
$response = $this->responses[$this->calls] ?? null;
$this->calls++;
if (!$response instanceof HttpResponse) {
throw new RuntimeException('Fake response queue exhausted');
}
return $response;
}
}
final class DetectorClientTest extends TestCase
{
public function testMapsAndSummarizesDetections(): void
{
$transport = new FakeTransport([
new HttpResponse(200, [], json_encode([
'data' => [
'technologies' => [
[
'name' => 'Example CMS',
'version' => '8',
'confidence' => 96,
'evidence' => ['header marker'],
],
],
'redirects' => [['status' => 301]],
],
], JSON_THROW_ON_ERROR)),
]);
$client = new DetectorClient(
$transport,
'test-token',
static fn (int $milliseconds) => null
);
$report = $client->detect('https://example.com');
self::assertSame(
'Example CMS 8 (96% confidence)',
$report->summary()
);
self::assertCount(1, $report->redirects);
}
public function testRetriesRateLimitThenSucceeds(): void
{
$transport = new FakeTransport([
new HttpResponse(429, ['retry-after' => '1'], '{}'),
new HttpResponse(200, [], '{"technologies":[]}'),
]);
$delays = [];
$client = new DetectorClient(
$transport,
'test-token',
static function (int $milliseconds) use (&$delays): void {
$delays[] = $milliseconds;
}
);
self::assertSame(
'No technologies detected',
$client->detect('https://example.com')->summary()
);
self::assertSame(2, $transport->calls);
self::assertSame([1000], $delays);
}
}
Извршете composer install, потоа composer test. Додадете посебни тестови за неправилен JSON, недостасувачки полиња за технологија, 401 без повторен обид, три последователни одговори 5xx и транспортен исклучок. Тие случаи ги штитат политиките што најверојатно ќе назадуваат при одржување.
Безбедност, операции и распоредување
Бидејќи оваа функционалност презема URL-адреса доставена од потенцијален клиент преку надворешна услуга, валидирајте ја шемата пред да ја испратите. Избегнувајте да запишувате целосни URL-адреси во логовите: тие може да содржат параметри за пребарување, идентификатори на клиенти или вградени ингеренции. Логовите треба да содржат ID на потенцијалниот клиент, исход, HTTP статус, можност за повторен обид, број на детекции и број на пренасочувања — но не токенот, заглавијата на барањето, телото на одговорот или суровиот доказ.
Распоредете ја промената на шемата пред командата. Внесете ги WEBSITE_DETECTOR_TOKEN и CRM_DATABASE_PATH, потврдете дека екстензиите cURL и PDO SQLite се овозможени, извршете тестови и извршете canary со еден потенцијален клиент. Закажете ја командата само откако тој canary ќе произведе читливо резиме.
Користете заклучување на распоредувачот за две инстанци на командата да не можат истовремено да ја збогатуваат истата серија. Почнете со конзервативна фреквенција и големина на серија соодветни на активираниот план. Третирајте ги повторените одговори 429 како сигнал за капацитет: намалете го притисокот од распоредувањето или разгледајте го планот наместо да го зголемувате бројот на повторни обиди.
Вообичаени обрасци на неуспех
- 401 или 403: токенот недостасува, е поништен, е неправилно копиран или не важи за услугата. Заменете го и рестартирајте го процесот; не обидувајте се повторно автоматски.
- 400 или 422: проверете ја валидацијата на URL-адресата и создавањето JSON. Командата намерно ги означува овие неуспеси како такви за кои не може повторно да се обиде.
- 429: почитувајте нумеричка вредност
Retry-Afterво рамките на конфигурираното ограничување од десет секунди, потоа оставете го потенцијалниот клиент подобен за подоцнежно закажано извршување ако обидите се исцрпени. - 5xx или неуспех на врска: обидете се повторно накратко, запишете
retryпо исцрпувањето и зачувајте постоечко резиме. - Успешен статус со неправилен JSON: класифицирајте го како невалиден одговор. Не погодувајте податоци и не заменувајте претходно корисно резиме.
- Празни детекции: зачувајте „No technologies detected.“ Празен резултат е валиден деловен резултат, а не автоматски транспортен неуспех.
Конечна контролна листа за проверка
- Планот на услугата е активен, а токенот ограничен на услугата доаѓа од панелот Service token на страницата со документација.
- Токенот е поддржан од околината и отсуствува од контрола на изворниот код, фикстури, логови и URL-адреси.
- Клиентот испраќа
POSTдо точната крајна точка на детекторот со JSONurlи Bearer автентикација. - Тајмаутите за поврзување и за вкупно време се ограничени, а повторно се обидуваат само привремените неуспеси.
- Информациите за доверба, верзија, доказ и пренасочување минуваат низ дефанзивна граница за мапирање.
- Успешен потенцијален клиент прикажува концизно технолошко резиме; привремените неуспеси ги зачувуваат претходните добри податоци.
- Автоматизираните тестови користат детерминистички лажен транспорт и никогаш не го повикуваат живото API.
- Продукциското распоредување спречува преклопување на извршувања и изложува структурирани настани за успех, неуспех и ограничување на стапката.
Исполираниот дел од оваа интеграција не е HTTP барањето. Тоа е дисциплината околу него: ингеренциите остануваат задржани, неизвесниот JSON се нормализира еднаш, привремените неуспеси не уништуваат корисни податоци, а CRM-от добива јазик што човек може брзо да го прегледа. Тоа е она што ја претвора детекцијата на веб-страници од интересен API одговор во сигурна функционалност за збогатување потенцијални клиенти.