Native PHP: Откријте го технолошкиот стек на веб-страницата на клиентот за попаметни понуди за редизајн
Понудата за редизајн станува ризична кога „прилично едноставна веб-страница“ ќе се покаже дека содржи градител на страници, застарен додаток за е-трговија, неколку ознаки за аналитика и синџир на пренасочувања што никој не го документирал. Визуелниот опсег е само половина од проценката. Постоечкиот технолошки стек го одредува напорот за миграција, длабочината на тестирањето, можностите за извлекување содржина и веројатноста за непријатни изненадувања.
Ова упатство изработува команднолиниска алатка во Native PHP 8.3 што ја испраќа јавната URL-адреса на клиентот до API-то Website Technology Detector, ги мапира неговите наоди оценети според доверливост во доменски објекти и произведува структурирани докази за подготовка на понуда. Интеграцијата користи нативен cURL, ограничени повторни обиди, дефанзивно обработување одговори, PHPUnit тестови и тајни поддржани од околински променливи.
Добијте пристап и копирајте го сервисниот токен
Создадете сметка на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате сметка.
- Отворете ја страницата на услугата Website Technology Detector.
- Изберете го достапниот Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата.
- Најдете го панелот Service token и копирајте го токенот ограничен на услугата.
- Зачувајте го во конфигурацијата на околината на проектот, никогаш во PHP изворниот код.
Оваа услуга бара автентикација. Прифаќа Bearer токен, заглавие X-API-Token или параметар за пребарување token. Ќе ја користиме Bearer формата бидејќи параметрите за пребарување најчесто се појавуваат во дневниците за пристап и записите за следење. Испраќајте само еден механизам за автентикација по барање.
Повторното генерирање на сервисниот токен го поништува претходно активниот токен. Третирајте го повторното генерирање како ротација на ингеренции: ажурирајте ја секоја распоредена околина, рестартирајте ги засегнатите процеси, потврдете го новиот токен и дури тогаш сметајте дека ротацијата е завршена.
Потврдете го точниот API повик
Интеграцијата користи POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies. Нејзиното JSON тело на барањето содржи една задолжителна вредност за интеграцијата: url.
Извршете го ова минимално барање откако ќе го замените местодржачот во вашата shell околина:
export WEBSITE_TECH_DETECTOR_TOKEN='YOUR_SERVICE_TOKEN'
curl --fail-with-body \
--connect-timeout 5 \
--max-time 20 \
-X POST \
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies' \
-H "Authorization: Bearer ${WEBSITE_TECH_DETECTOR_TOKEN}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{"url":"https://www.example.com"}'
Успешниот одговор содржи детекции на технологии со доверливост, поткрепувачки докази, информации за верзијата каде што се достапни и информации за пренасочувања. Не претпоставувајте дека секоја технологија открива верзија, дека доказите секогаш се скаларни или дека идните одговори ќе содржат само полиња што вашата апликација веќе ги разбира.
Обликувајте го проектот околу една граница
Алатката има три слоја: транспорт одговорен за HTTP механиката, клиент одговорен за автентикација и политика за повторни обиди и доменски објекти одговорни за нормализирање на одговорот. Работниот тек за понудата користи доменски објекти наместо да зависи од суров JSON.
stack-inspector/
├── bin/inspect.php
├── src/
│ ├── Contract.php
│ ├── CurlTransport.php
│ ├── DetectorClient.php
│ └── DetectionReport.php
├── tests/DetectorClientTest.php
├── composer.json
└── .env.local
Создадете composer.json со PHP 8.3, cURL екстензијата, PSR-4 автоматско вчитување и PHPUnit 11:
{
"require": {
"php": "^8.3",
"ext-curl": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"QuoteStack\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"QuoteStack\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit tests"
}
}
Извршете composer install. За локален развој, внесете го следново во непотврдена датотека .env.local:
export WEBSITE_TECH_DETECTOR_TOKEN='YOUR_SERVICE_TOKEN'
export WEBSITE_TECH_CONNECT_TIMEOUT_MS='5000'
export WEBSITE_TECH_RESPONSE_TIMEOUT_MS='20000'
Вчитајте ја со set -a; . ./.env.local; set +a. Додајте .env.local во .gitignore. Нативниот PHP не ги анализира автоматски датотеките со околински променливи; продукцијата треба да ги внесе истите променливи преку управувачот на процеси, платформата за контејнери или управувачот со тајни.
Имплементирајте ограничен нативен cURL транспорт
Транспортот не знае што значи детекција на технологија. Тој враќа статус, заглавија и тело, додека наметнува TLS верификација, временски ограничувања за поврзување и вкупно време, како и ограничување на одговор од два мегабајти.
<?php
// src/Contract.php
declare(strict_types=1);
namespace QuoteStack;
final readonly class HttpResponse
{
public function __construct(
public int $status,
public array $headers,
public string $body,
) {}
}
interface Transport
{
public function post(
string $url,
array $headers,
string $body,
int $connectTimeoutMs,
int $responseTimeoutMs,
): HttpResponse;
}
final class TransportException extends \RuntimeException {}
final class ApiFailure extends \RuntimeException
{
public function __construct(
public readonly string $kind,
public readonly ?int $status = null,
string $message = 'Technology detection failed',
?\Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
}
<?php
// src/CurlTransport.php
declare(strict_types=1);
namespace QuoteStack;
final class CurlTransport implements Transport
{
public function __construct(private int $maxBytes = 2_097_152) {}
public function post(
string $url,
array $headers,
string $body,
int $connectTimeoutMs,
int $responseTimeoutMs,
): HttpResponse {
$curl = curl_init($url);
if ($curl === false) {
throw new TransportException('Could not initialize cURL');
}
$responseHeaders = [];
$responseBody = '';
$tooLarge = false;
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_CONNECTTIMEOUT_MS => $connectTimeoutMs,
CURLOPT_TIMEOUT_MS => $responseTimeoutMs,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
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;
},
CURLOPT_WRITEFUNCTION => function ($handle, string $chunk)
use (&$responseBody, &$tooLarge): int {
if (strlen($responseBody) + strlen($chunk) > $this->maxBytes) {
$tooLarge = true;
return 0;
}
$responseBody .= $chunk;
return strlen($chunk);
},
]);
$result = curl_exec($curl);
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$error = curl_error($curl);
curl_close($curl);
if ($tooLarge) {
throw new TransportException('API response exceeded the size limit');
}
if ($result === false) {
throw new TransportException('cURL request failed: ' . $error);
}
return new HttpResponse($status, $responseHeaders, $responseBody);
}
}
Нормализирајте ги неизвесните податоци од одговорот на границата
Договорот на API-то опишува детекции, доверливост, докази, верзии и пренасочувања. Маперот подолу намерно избегнува да претпостави една обвивка на највисоко ниво. Тој поминува низ декодираниот товар, препознава објекти со облик на детекција, ги зачувува вредностите на доказите и верзиите без да го принудува нивното значење, собира структури поврзани со пренасочување и го задржува суровиот одговор за контролирана дијагностика.
<?php
// src/DetectionReport.php
declare(strict_types=1);
namespace QuoteStack;
final readonly class TechnologyDetection implements \JsonSerializable
{
public function __construct(
public string $technology,
public float $confidence,
public array $evidence,
public array $versions,
) {}
public function jsonSerialize(): array
{
return get_object_vars($this);
}
}
final readonly class DetectionReport implements \JsonSerializable
{
public function __construct(
public array $detections,
public array $redirectInformation,
public array $raw,
) {}
public static function fromApi(array $payload): self
{
$detections = [];
$redirects = [];
self::walk($payload, $detections, $redirects);
return new self($detections, $redirects, $payload);
}
private static function walk(
array $node,
array &$detections,
array &$redirects,
): void {
$name = $node['technology'] ?? $node['name'] ?? null;
$confidence = $node['confidence'] ?? null;
if (is_string($name) && $name !== '' && is_numeric($confidence)) {
$detections[] = new TechnologyDetection(
$name,
(float) $confidence,
self::values($node['evidence'] ?? null),
self::values($node['versions'] ?? $node['version'] ?? null),
);
}
foreach ($node as $key => $value) {
if (is_string($key)
&& str_contains(strtolower($key), 'redirect')
&& is_array($value)
) {
$redirects[$key] = $value;
}
if (is_array($value)) {
self::walk($value, $detections, $redirects);
}
}
}
private static function values(mixed $value): array
{
if ($value === null || $value === '') {
return [];
}
return is_array($value) ? array_values($value) : [$value];
}
public function jsonSerialize(): array
{
return [
'detections' => $this->detections,
'redirect_information' => $this->redirectInformation,
];
}
}
Доверливоста останува нумеричката вредност вратена од услугата. Кодот не ја претолкува тивко како процент или веројатност. Политиката за понуди треба да користи прагови само по потврдување на документираната скала и тестирање на репрезентативни одговори.
Додајте автентикација, повторни обиди и категории на неуспех
Привремените неуспеси во транспортот, 429 и избрани неуспеси на порталот или достапноста може повторно да се обидат. Невалидните барања и неуспесите во автентикацијата треба веднаш да се вратат: повторувањето на истите ингеренции или товар само троши време и квота.
<?php
// src/DetectorClient.php
declare(strict_types=1);
namespace QuoteStack;
final class DetectorClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies';
private \Closure $sleep;
public function __construct(
private Transport $transport,
private string $token,
private int $connectTimeoutMs = 5000,
private int $responseTimeoutMs = 20000,
private int $maxAttempts = 3,
?\Closure $sleep = null,
) {
if ($token === '') {
throw new \InvalidArgumentException('Service token is missing');
}
$this->sleep = $sleep ?? static fn (int $ms) => usleep($ms * 1000);
}
public function detect(string $targetUrl): DetectionReport
{
$this->assertAllowedUrl($targetUrl);
$body = json_encode(['url' => $targetUrl], JSON_THROW_ON_ERROR);
for ($attempt = 1; $attempt <= $this->maxAttempts; $attempt++) {
try {
$response = $this->transport->post(
self::ENDPOINT,
[
'Authorization: Bearer ' . $this->token,
'Accept: application/json',
'Content-Type: application/json',
],
$body,
$this->connectTimeoutMs,
$this->responseTimeoutMs,
);
} catch (TransportException $exception) {
if ($attempt === $this->maxAttempts) {
throw new ApiFailure(
'transport',
null,
'Transport attempts exhausted',
$exception,
);
}
($this->sleep)($this->backoffMs($attempt, null));
continue;
}
if ($response->status >= 200 && $response->status < 300) {
try {
$payload = json_decode(
$response->body,
true,
512,
JSON_THROW_ON_ERROR,
);
} catch (\JsonException $exception) {
throw new ApiFailure(
'invalid_response',
$response->status,
'API returned malformed JSON',
$exception,
);
}
if (!is_array($payload)) {
throw new ApiFailure(
'invalid_response',
$response->status,
'API response was not a JSON object or array',
);
}
return DetectionReport::fromApi($payload);
}
if ($response->status === 401 || $response->status === 403) {
throw new ApiFailure('authentication', $response->status);
}
$retryable = $response->status === 429
|| in_array($response->status, [502, 503, 504], true);
if (!$retryable) {
throw new ApiFailure('request', $response->status);
}
if ($attempt === $this->maxAttempts) {
$kind = $response->status === 429 ? 'rate_limit' : 'upstream';
throw new ApiFailure($kind, $response->status);
}
$retryAfter = $response->headers['retry-after'] ?? null;
($this->sleep)($this->backoffMs($attempt, $retryAfter));
}
throw new ApiFailure('internal');
}
private function backoffMs(int $attempt, mixed $retryAfter): int
{
if (is_string($retryAfter) && ctype_digit($retryAfter)) {
return min(5000, (int) $retryAfter * 1000);
}
return min(2000, 200 * (2 ** ($attempt - 1)) + random_int(0, 100));
}
private function assertAllowedUrl(string $url): void
{
$parts = parse_url($url);
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
$host = strtolower((string) ($parts['host'] ?? ''));
if (!filter_var($url, FILTER_VALIDATE_URL)
|| !in_array($scheme, ['http', 'https'], true)
|| $host === ''
|| isset($parts['user'])
|| isset($parts['pass'])
|| $host === 'localhost'
|| str_ends_with($host, '.local')
) {
throw new \InvalidArgumentException('A public HTTP(S) URL is required');
}
if (filter_var($host, FILTER_VALIDATE_IP)
&& !filter_var(
$host,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE,
)
) {
throw new \InvalidArgumentException('Private IP targets are not allowed');
}
}
}
Поврзете го детекторот со работниот тек за понудата
Командата печати нормализиран JSON што може да се прикачи на запис за понуда или да го прегледа проценувачот. Таа бележи само оперативни метаподатоци: идентификатор за корелација, име на домаќин, траење, број и категорија на неуспех. Токените, телата на одговорите, доказите и целосните URL-адреси не влегуваат во дневниците.
<?php
// bin/inspect.php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use QuoteStack\ApiFailure;
use QuoteStack\CurlTransport;
use QuoteStack\DetectorClient;
$url = $argv[1] ?? '';
$token = getenv('WEBSITE_TECH_DETECTOR_TOKEN') ?: '';
$connectMs = (int) (getenv('WEBSITE_TECH_CONNECT_TIMEOUT_MS') ?: 5000);
$responseMs = (int) (getenv('WEBSITE_TECH_RESPONSE_TIMEOUT_MS') ?: 20000);
$requestId = bin2hex(random_bytes(8));
$started = hrtime(true);
try {
$client = new DetectorClient(
new CurlTransport(),
$token,
$connectMs,
$responseMs,
);
$report = $client->detect($url);
error_log(json_encode([
'event' => 'technology_detection_completed',
'request_id' => $requestId,
'host' => parse_url($url, PHP_URL_HOST),
'detections' => count($report->detections),
'duration_ms' => (int) ((hrtime(true) - $started) / 1_000_000),
], JSON_THROW_ON_ERROR));
echo json_encode(
$report,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
) . PHP_EOL;
} catch (ApiFailure | \InvalidArgumentException $exception) {
error_log(json_encode([
'event' => 'technology_detection_failed',
'request_id' => $requestId,
'kind' => $exception instanceof ApiFailure
? $exception->kind
: 'validation',
'status' => $exception instanceof ApiFailure
? $exception->status
: null,
], JSON_THROW_ON_ERROR));
fwrite(STDERR, "Inspection failed; reference {$requestId}." . PHP_EOL);
exit(1);
}
Извршете php bin/inspect.php https://www.example.com. Извештајот може да информира прашања за CMS миграција, серверско рендерирање, интеграции за е-трговија, замена на аналитика, зачувување пренасочувања и технологии чии недостапни верзии бараат рачна истрага. Детекцијата е доказ за дефинирање на опсег, а не дозвола за заклучување за приватна инфраструктура или замена за откривање со засегнатите страни.
Тестирајте без да ја повикувате платената услуга
Лажен транспорт го прави однесувањето на повторниот обид и мапирањето детерминистичко. Тестовите никогаш не смеат да содржат вистински токен или да контактираат со продукциската крајна точка.
<?php
// tests/DetectorClientTest.php
declare(strict_types=1);
namespace QuoteStack\Tests;
use PHPUnit\Framework\TestCase;
use QuoteStack\ApiFailure;
use QuoteStack\DetectorClient;
use QuoteStack\HttpResponse;
use QuoteStack\Transport;
final class FakeTransport implements Transport
{
public int $calls = 0;
public function __construct(private array $responses) {}
public function post(
string $url,
array $headers,
string $body,
int $connectTimeoutMs,
int $responseTimeoutMs,
): HttpResponse {
$this->calls++;
return array_shift($this->responses);
}
}
final class DetectorClientTest extends TestCase
{
public function testRetriesAndMapsContractData(): void
{
$transport = new FakeTransport([
new HttpResponse(503, [], '{}'),
new HttpResponse(200, [], json_encode([
'result' => [
'detections' => [[
'technology' => 'Example CMS',
'confidence' => 0.94,
'evidence' => ['generator metadata'],
'versions' => ['8'],
]],
'redirects' => [
['from' => 'http://example.test',
'to' => 'https://example.test'],
],
],
], JSON_THROW_ON_ERROR)),
]);
$client = new DetectorClient(
$transport,
'test-token',
maxAttempts: 2,
sleep: static fn (int $ms) => null,
);
$report = $client->detect('https://example.test');
self::assertSame(2, $transport->calls);
self::assertCount(1, $report->detections);
self::assertSame(
'Example CMS',
$report->detections[0]->technology,
);
self::assertNotEmpty($report->redirectInformation);
}
public function testDoesNotRetryAuthenticationFailure(): void
{
$transport = new FakeTransport([
new HttpResponse(401, [], '{}'),
]);
$client = new DetectorClient($transport, 'bad-token');
try {
$client->detect('https://example.test');
self::fail('Expected ApiFailure');
} catch (ApiFailure $failure) {
self::assertSame('authentication', $failure->kind);
self::assertSame(1, $transport->calls);
}
}
}
Извршете composer test. Додавајте фикстури копирани од одобрени, санирани API одговори додека договорот се развива, особено за празни детекции, верзии што недостигаат, повеќекратни пренасочувања, необични структури на докази, невалиден JSON, истекување на времето и ограничување на стапката.
Продукциски одлуки што го штитат процесот на понудување
Безбедност и приватност
Прифаќајте само јавни HTTP или HTTPS цели, овластете кој може да поднесува инспекции и ограничете ја стапката на сопствената рута или извршувач на команди. Локалната валидација ги блокира очигледните приватни буквални адреси, но DNS имињата можат да ја сменат резолуцијата; оддалечената услуга мора да остане одговорна за безбедно преземање само на јавни веб-страници. Никогаш не го изложувајте детекторот како неограничен јавен прокси.
Ограничете ја видливоста на тајните на апликацискиот процес, ротирајте намерно и редактирајте ги заглавијата за авторизација насекаде. Задржувајте сурови одговори само кога е неопходно, со контроли за пристап и политика за бришење. URL-адресата на клиентот може и самата да биде комерцијално чувствителна пред да биде објавен редизајнот.
Набљудливост и квоти
Следете успех, траење, категорија на неуспех, HTTP статус, број на повторни обиди и број на детекции. Алармирајте за трајни неуспеси во автентикацијата бидејќи тие често укажуваат на истечен или повторно генериран токен. Третирајте ги повторените одговори 429 како повратна информација за капацитетот, а не како дозвола да се обидувате засекогаш. Ако подготовката на понуда стане интерактивна, прикажете состојба „привремено недостапно“ и овозможете намерен подоцнежен повторен обид.
Распоредување
Потврдете PHP 8.3 или понов, ext-curl, CA сертификати, излезен HTTPS пристап, оптимизираниот автоматски вчитувач на Composer и внесени околински променливи. Одржувајте ги вредностите за поврзување и вкупно истекување на времето под сопственото ограничување за извршување на повикувачкиот worker. Ако инспекциите подоцна преминат во позадински задачи, зачувајте идемпотентни идентификатори на задачи и ограничете ги обидите за повторна испорака; додавањето редица е непотребно за овој фокусиран команднолиниски работен тек.
Вообичаени неуспеси и нивното значење
- 401 или 403: токенот недостига, е невалиден, има погрешен опсег или бил отповикан со повторно генерирање. Не обидувајте се автоматски повторно.
- Неуспех на барање од класата 400: потврдете ја јавната URL-адреса и JSON барањето. Повторувањето на непроменето невалидно барање не може да го поправи.
- 429: почитувајте нумеричка вредност
Retry-Afterво рамките на локално ограничување, а потоа запрете по ограничениот број обиди. - 502, 503 или 504: користете кратко експоненцијално повлекување со случајна варијација и зачувајте структуриран неуспех нагоре по текот ако обидите се исцрпени.
- Невалиден или преголем JSON: откажете безбедно. Не претворајте неизвесен одговор во празен, навидум успешен извештај.
- Нема детектирани технологии: забележете дека откривањето било неубедливо и закажете рачна инспекција наместо да претпоставите дека веб-страницата нема значаен стек.
Контролна листа за конечна верификација
- Се користи точната POST крајна точка со JSON вредност
url. - Токенот ограничен на услугата доаѓа од околината и никогаш не се појавува во дневници или фикстури.
- Ограничувањата за поврзување, одговор, големина на телото, повторни обиди и повлекување се ограничени.
- Неуспесите во автентикацијата и валидацијата не се повторуваат.
- Детекциите ги зачувуваат доверливоста, доказите и верзиите без измислени стандардни вредности.
- Информациите за пренасочување се задржуваат за планирање на миграцијата.
- PHPUnit целосно поминува преку лажниот транспорт.
- Вистински smoke тест произведува извештај за овластена јавна URL-адреса на клиент.
- Проценувачот ги прегледува наодите со малку докази и со верзии што недостигаат пред да го одреди ценовникот за редизајнот.
Силната понуда за редизајн не се преправа дека неизвесноста исчезнала. Таа ја прави неизвесноста видлива доволно рано за чесно да се проценат откривањето, миграцијата и тестирањето. Со одржување на строгата API граница и читливоста на добиените докази, оваа мала PHP алатка претвора нејасна веб-страница во подобар технички разговор — и понуда што е многу помалку веројатно да се расплетка откако ќе започне работата.