Туториали

Laravel: Automate Client Website Audits for Redesign Quotes with Tech Detector API

Laravel: Автоматизирајте ревизии на веб-страници на клиенти за понуди за редизајн со Tech Detector API

Понудата за редизајн станува ризична кога се заснова само на она што го открива прелистувачот. Изгланцаната почетна страница може да крие застарен CMS, неколку аналитички производи, излог со многу JavaScript или инфраструктура што ќе ја усложни миграцијата. Откривањето на тој стек пред проценката на работата му дава на фриленсер или мал развоен тим подобра основа за опсег, прашања и цени.

Овој туторијал гради Laravel апликација ориентирана кон продукциска употреба, која испраќа јавна URL-адреса на клиент до API-то Website Technology Detector, го претвора одговорот во доменски објекти и враќа наоди за технологии поткрепени со докази за проценка на редизајн. Интеграцијата го користи вградениот HTTP клиент на Laravel, ограничени тајмаути, селективни повторни обиди, структурирани грешки, безбедно логирање и детерминистички тестови.

Добијте пристап и копирајте го сервисниот токен

Регистрирајте се на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате сметка.

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

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

Регенерирањето на сервисниот токен го поништува претходно активниот токен. Третирајте го регенерирањето како ротација на ингеренции: веднаш ажурирајте ја секоја распоредена околина, исчистете ја кешираната Laravel конфигурација и потврдете ја интеграцијата пред да отстраните какво било оперативно предупредување.

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

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

curl --request POST \
  --url https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies \
  --header "Authorization: Bearer YOUR_SERVICE_TOKEN" \
  --header "Accept: application/json" \
  --header "Content-Type: application/json" \
  --data '{"url":"https://example.com"}'

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

Создадете ја границата на Laravel проектот

Имплементацијата намерно останува мала: контролерот ја валидира јавната URL-адреса, сервисот ја поседува далечинската HTTP размена, а доменските објекти го нормализираат одговорот. Барањето останува синхроно бидејќи корисник што ревидира една веб-страница има корист од непосреден резултат. Ако производот подоцна прифаќа листи на сајтови, преместете го истиот повик до сервисот во задачи во редица наместо да ги држите веб-барањата отворени.

Започнете со тековна Laravel апликација што работи со PHP 8.3 или понова верзија:

composer create-project laravel/laravel redesign-auditor
cd redesign-auditor
php artisan make:controller WebsiteAuditController
php artisan make:test TechnologyDetectorTest

Релевантната структура на проектот ќе биде:

app/
  Domain/WebsiteAudit/TechnologyAudit.php
  Exceptions/TechnologyDetectorFailure.php
  Http/Controllers/WebsiteAuditController.php
  Services/TechnologyDetector.php
config/
  services.php
routes/
  api.php
tests/
  Feature/TechnologyDetectorTest.php

Ставете ја ингеренцијата во конфигурација поддржана од околината

Додајте резервирани места во .env.example, а потоа ставете го вистинскиот токен само во непредадената .env:

'technology_detector' => [
    'url' => env(
        'TECHNOLOGY_DETECTOR_URL',
        'https://ai.mihajlo.mk/api/website-technology-detector'
    ),
    'token' => env('TECHNOLOGY_DETECTOR_TOKEN'),
],

Апликацискиот код мора да чита config(), а не директно да повикува env(). Таа разлика е важна откако php artisan config:cache ќе ја компајлира конфигурацијата за продукција.

Мапирајте го одговорот на границата на апликацијата

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

<?php

namespace App\Domain\WebsiteAudit;

final readonly class TechnologyDetection
{
    public function __construct(
        public string $name,
        public ?float $confidence,
        public array $versions,
        public array $evidence,
    ) {}

    public static function fromApi(array $item): ?self
    {
        $name = $item['name'] ?? null;

        if (! is_string($name) || trim($name) === '') {
            return null;
        }

        $confidence = $item['confidence'] ?? null;

        return new self(
            name: trim($name),
            confidence: is_numeric($confidence) ? (float) $confidence : null,
            versions: self::stringList($item['versions'] ?? []),
            evidence: is_array($item['evidence'] ?? null)
                ? $item['evidence']
                : [],
        );
    }

    private static function stringList(mixed $value): array
    {
        if (! is_array($value)) {
            return [];
        }

        return array_values(array_filter(
            $value,
            static fn (mixed $item): bool => is_string($item)
                && trim($item) !== ''
        ));
    }
}

final readonly class TechnologyAudit
{
    public function __construct(
        public array $technologies,
        public array $redirects,
    ) {}

    public static function fromApi(array $payload): self
    {
        $body = is_array($payload['data'] ?? null)
            ? $payload['data']
            : $payload;

        $items = is_array($body['technologies'] ?? null)
            ? $body['technologies']
            : [];

        $technologies = array_values(array_filter(array_map(
            static fn (mixed $item): ?TechnologyDetection =>
                is_array($item)
                    ? TechnologyDetection::fromApi($item)
                    : null,
            $items,
        )));

        return new self(
            technologies: $technologies,
            redirects: is_array($body['redirects'] ?? null)
                ? array_values($body['redirects'])
                : [],
        );
    }
}

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

Изградете отпорен сервис за детекција

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

<?php

namespace App\Exceptions;

use RuntimeException;

final class TechnologyDetectorFailure extends RuntimeException
{
    public function __construct(
        public readonly string $kind,
        string $message,
        public readonly ?int $upstreamStatus = null,
        public readonly bool $retryable = false,
    ) {
        parent::__construct($message);
    }
}

Сега создајте app/Services/TechnologyDetector.php. Сервисот поставува одделни ограничувања за поврзување и вкупно време за одговор. Повторно се обидува при неуспеси на поврзувањето, ограничувања на стапката и минливи одговори од серверот со кратко, ограничено одложување. Неуспесите при автентикација и валидација намерно се исклучени од повторните обиди бидејќи повторувањето на непроменето барање не може да ги поправи.

<?php

namespace App\Services;

use App\Domain\WebsiteAudit\TechnologyAudit;
use App\Exceptions\TechnologyDetectorFailure;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;

final class TechnologyDetector
{
    public function detect(string $url): TechnologyAudit
    {
        $baseUrl = rtrim((string) config('services.technology_detector.url'), '/');
        $token = (string) config('services.technology_detector.token');

        if ($token === '') {
            throw new TechnologyDetectorFailure(
                'configuration',
                'The technology detector token is not configured.'
            );
        }

        $started = hrtime(true);
        $host = parse_url($url, PHP_URL_HOST) ?: 'unknown';

        try {
            $response = Http::baseUrl($baseUrl)
                ->acceptJson()
                ->asJson()
                ->withToken($token)
                ->connectTimeout(3)
                ->timeout(15)
                ->retry(
                    [200, 500],
                    0,
                    static function (
                        Throwable $exception,
                        PendingRequest $request
                    ): bool {
                        if ($exception instanceof ConnectionException) {
                            return true;
                        }

                        return $exception instanceof RequestException
                            && in_array(
                                $exception->response->status(),
                                [429, 500, 502, 503, 504],
                                true
                            );
                    },
                    false
                )
                ->post('/v1/detect-technologies', ['url' => $url]);
        } catch (ConnectionException $exception) {
            Log::warning('technology_detector.network_failure', [
                'host' => $host,
                'message' => $exception->getMessage(),
            ]);

            throw new TechnologyDetectorFailure(
                'network',
                'The detector could not be reached.',
                retryable: true
            );
        }

        Log::info('technology_detector.completed', [
            'host' => $host,
            'status' => $response->status(),
            'duration_ms' => (int) ((hrtime(true) - $started) / 1_000_000),
        ]);

        if ($response->successful()) {
            $payload = $response->json();

            if (! is_array($payload)) {
                throw new TechnologyDetectorFailure(
                    'invalid_response',
                    'The detector returned invalid JSON.',
                    $response->status(),
                    true
                );
            }

            return TechnologyAudit::fromApi($payload);
        }

        $status = $response->status();

        throw match (true) {
            in_array($status, [401, 403], true) =>
                new TechnologyDetectorFailure(
                    'authentication',
                    'The service token was rejected.',
                    $status
                ),
            $status === 422 =>
                new TechnologyDetectorFailure(
                    'validation',
                    'The detector rejected the submitted URL.',
                    $status
                ),
            $status === 429 =>
                new TechnologyDetectorFailure(
                    'rate_limit',
                    'The detector quota or rate limit was reached.',
                    $status,
                    true
                ),
            $status >= 500 =>
                new TechnologyDetectorFailure(
                    'upstream',
                    'The detector is temporarily unavailable.',
                    $status,
                    true
                ),
            default =>
                new TechnologyDetectorFailure(
                    'request',
                    'The detector request failed.',
                    $status
                ),
        };
    }
}

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

Изложете ја крајната точка за ревизија

Создадете app/Http/Controllers/WebsiteAuditController.php. Покрај URL-валидацијата на Laravel, контролерот ги одбива localhost и приватните или резервираните IP литерали. Тоа ја одржува функцијата усогласена со нејзината намена: проверка на јавни веб-страници.

<?php

namespace App\Http\Controllers;

use App\Exceptions\TechnologyDetectorFailure;
use App\Services\TechnologyDetector;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

final class WebsiteAuditController extends Controller
{
    public function __invoke(
        Request $request,
        TechnologyDetector $detector
    ): JsonResponse {
        $validated = $request->validate([
            'url' => ['required', 'string', 'url:http,https', 'max:2048'],
        ]);

        $host = parse_url($validated['url'], PHP_URL_HOST);

        if (
            ! is_string($host)
            || strtolower($host) === 'localhost'
            || (
                filter_var($host, FILTER_VALIDATE_IP)
                && ! filter_var(
                    $host,
                    FILTER_VALIDATE_IP,
                    FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
                )
            )
        ) {
            throw ValidationException::withMessages([
                'url' => 'Enter a publicly reachable website URL.',
            ]);
        }

        try {
            $audit = $detector->detect($validated['url']);
        } catch (TechnologyDetectorFailure $failure) {
            $status = match ($failure->kind) {
                'validation' => 422,
                'rate_limit' => 429,
                'configuration', 'authentication' => 503,
                default => 502,
            };

            return response()->json([
                'error' => [
                    'type' => $failure->kind,
                    'message' => $failure->getMessage(),
                    'retryable' => $failure->retryable,
                ],
            ], $status);
        }

        return response()->json([
            'technologies' => array_map(
                static fn ($technology): array => [
                    'name' => $technology->name,
                    'confidence' => $technology->confidence,
                    'versions' => $technology->versions,
                    'evidence' => $technology->evidence,
                ],
                $audit->technologies
            ),
            'redirects' => $audit->redirects,
        ]);
    }
}

Регистрирајте ја крајната точка во routes/api.php. Ограничувањето на ниво на рута го штити вашиот план од случајни јамки и повремена злоупотреба; автентицираните апликации можат да го заменат со политика по корисник.

<?php

use App\Http\Controllers\WebsiteAuditController;
use Illuminate\Support\Facades\Route;

Route::post('/website-audits', WebsiteAuditController::class)
    ->middleware('throttle:10,1');

Тестирајте успех и неуспех што не се повторува

Http::fake() на Laravel ги одржува тестовите детерминистички и осигурува дека ниедно платено барање или барање ограничено со квота не излегува од пакетот. Тестот за успех ја докажува автентикацијата на барањето и мапирањето; тестот за автентикација докажува дека одбиен токен не се повторува.

<?php

namespace Tests\Feature;

use App\Exceptions\TechnologyDetectorFailure;
use App\Services\TechnologyDetector;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

final class TechnologyDetectorTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        config()->set(
            'services.technology_detector.url',
            'https://ai.mihajlo.mk/api/website-technology-detector'
        );
        config()->set(
            'services.technology_detector.token',
            'test-service-token'
        );
    }

    public function test_it_maps_a_technology_audit(): void
    {
        Http::fake([
            '*/v1/detect-technologies' => Http::response([
                'data' => [
                    'technologies' => [[
                        'name' => 'Example CMS',
                        'confidence' => 92,
                        'versions' => ['6.x'],
                        'evidence' => ['generator metadata'],
                    ]],
                    'redirects' => [
                        ['from' => 'http://example.com',
                         'to' => 'https://example.com'],
                    ],
                ],
            ], 200),
        ]);

        $audit = app(TechnologyDetector::class)
            ->detect('https://example.com');

        $this->assertCount(1, $audit->technologies);
        $this->assertSame(
            'Example CMS',
            $audit->technologies[0]->name
        );
        $this->assertSame(
            92.0,
            $audit->technologies[0]->confidence
        );

        Http::assertSent(fn ($request): bool =>
            $request->url()
                === 'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies'
            && $request->hasHeader(
                'Authorization',
                'Bearer test-service-token'
            )
            && $request['url'] === 'https://example.com'
        );
    }

    public function test_authentication_failure_is_not_retried(): void
    {
        Http::fake([
            '*/v1/detect-technologies' =>
                Http::response(['message' => 'Unauthorized'], 401),
        ]);

        try {
            app(TechnologyDetector::class)
                ->detect('https://example.com');

            $this->fail('Expected detector failure was not thrown.');
        } catch (TechnologyDetectorFailure $failure) {
            $this->assertSame('authentication', $failure->kind);
            $this->assertFalse($failure->retryable);
        }

        Http::assertSentCount(1);
    }
}

Извршете го пакетот и испробајте ја границата на апликацијата:

php artisan test
php artisan serve

curl --request POST \
  --url http://127.0.0.1:8000/api/website-audits \
  --header "Accept: application/json" \
  --header "Content-Type: application/json" \
  --data '{"url":"https://example.com"}'

Безбедност, набљудливост и распоредување во продукција

Држете ја крајната точка зад автентикацијата на вашата апликација ако резултатите од ревизијата се дел од приватен работен процес за понуди. Ограничувањето на стапката ја штити API-дозволата, но не е авторизација. Не ги прикажувајте вратените докази како доверлив HTML; чувајте ги како структурирани податоци и ексапирајте ги во кој било интерфејс за понуди.

Следете ги бројот и латентноста на успешните повици, мрежните неуспеси, одговорите 429 и одговорите 5xx од надворешниот сервис. Постојан неуспех при автентикација обично значи истечен, регенериран или неправилно распореден токен. Одговор со ограничување на стапката треба да ја паузира масовната обработка наместо да поттикне агресивни непосредни повторни обиди.

Распоредете ја тајната на апликацијата преку хостинг-платформата, потоа повторно изградете ја кешираната конфигурација на Laravel:

php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan test --testsuite=Feature

Кога го ротирате токенот, ажурирајте го TECHNOLOGY_DETECTOR_TOKEN, повторно извршете config:cache и направете една контролирана ревизија. Запомнете дека регенерирањето го поништува стариот активен токен, па застарената инстанца веднаш ќе почне да добива грешки при автентикација.

Вообичаени неуспеси и нивното значење

  • 401 или 403: токенот недостасува, е поништен, е неправилно копиран или не е достапен во кешираната конфигурација. Не обидувајте се повторно автоматски.
  • 422: испратената URL-адреса е одбиена. Вратете порака за валидација насочена кон исправка наместо да ја третирате како прекин.
  • 429: достигната е квотата на планот или ограничувањето на стапката. Прикажете состојба што може повторно да се проба, почитувајте ги упатствата на услугата и одложете ја сериската работа.
  • Тајмаут на поврзување: DNS, мрежата или надворешната услуга може да не се достапни. Ограничениот повторен обид се справува со кратки прекини без да држи работник зафатен на неодредено време.
  • Празни детекции: ова може да биде валиден резултат. Не докажува дека веб-страницата не користи технологија; значи дека јавните докази не создале мапирани детекции.
  • Неочекуван JSON: третирајте го како неуспех на договорот со надворешниот сервис, зачувајте безбедни метаподатоци за статусот и ажурирајте го граничниот мапер според официјалната документација.

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

  • Сметката и Free, Plus или Pro планот се активни.
  • Токенот ограничен на услугата се чува надвор од изворната контрола.
  • Апликацијата ја повикува точната крајна точка за детекторот POST со JSON url.
  • Тајмаутите за поврзување и вкупниот одговор се ограничени.
  • Се повторуваат само мрежни неуспеси, ограничувања на стапката и минливи неуспеси на серверот.
  • Детекциите, доверливоста, верзиите, доказите и пренасочувањата преминуваат низ одбранбена доменска граница.
  • Логовите ги изоставуваат токените, телата на одговорите и целосните URL-адреси на клиентите.
  • Тестовите со Http::fake() го покриваат мапирањето и неуспехот при автентикација што не се повторува.
  • Кешираната продукциска конфигурација го содржи тековниот токен.

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

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

Mihajlo

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