Laravel: Претворете ги URL-адресите на веб-страниците во однапред пополнети CRM потенцијални клиенти со извлекување податоци за компании со ВИ
Продавачот не треба да мора да копира име на компанија, општа е-пошта, телефонски број и податоци за вработени од веб-страница во CRM, поле по поле. Подобар работен тек ја бара веб-страницата еднаш, презема структурирани податоци за компанијата и ги прикажува како нацрт за потенцијален клиент што може да се уредува.
Ова упатство го гради тој работен тек во Laravel со намерно тесна архитектура: синхрона крајна точка за збогатување, дефанзивен мапер на границата на апликацијата и мала CRM-форма што никогаш не зачувува податоци од трета страна без човечка проверка. Интеграцијата го користи вградениот HTTP-клиент на Laravel, ограничени временски ограничувања, селективни повторни обиди, структурирани грешки и детерминистички тестови.
Добијте пристап до услугата Website to Company data
Прво, регистрирајте сметка или најавете се во постоечка сметка. Отворете ја страницата на услугата Website to Company data, изберете го достапниот Free, Plus или Pro план и завршете ја неговата активација.
Потоа, отворете ја официјалната документација за услугата. Најдете го панелот Service token и копирајте го токенот ограничен на услугата. Повторното генерирање на овој токен го поништува претходно активниот токен, па ротацијата на токенот мора да ја ажурира секоја распоредена околина што го користи.
Оваа услуга не е анонимна: секое барање бара токен за услугата во параметарот за барање token. На некои API-и не им е потребна акредитива, но тоа не е случај тука.
Потврдете го точниот API-договор
Интеграцијата испраќа HTTP GET барање до https://ai.mihajlo.mk/api/website-to-company-data/v1/extract. Таа доставува два параметри за барање: token за автентикација и website за јавната веб-страница на компанијата.
Пред да напишете Laravel-код, направете едно минимално барање со токен-поставувач:
curl --get 'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract' \
--data-urlencode 'token=YOUR_SERVICE_TOKEN' \
--data-urlencode 'website=https://example.com'
Користете само поставувачи во документацијата и историјата на школката. Не внесувајте вистински токен во контрола на изворен код, слики од екран, дневници, тест-податоци или пораки за поддршка.
Чувајте ја вистинската акредитива во непредадената датотека .env на проектот:
WEBSITE_COMPANY_DATA_TOKEN=YOUR_SERVICE_TOKEN
WEBSITE_COMPANY_DATA_URL=https://ai.mihajlo.mk/api/website-to-company-data/v1/extract
Додадете неактивен поставувач, никогаш вистинската вредност, во .env.example. Потоа изложете ги двете поставки преку config/services.php:
'website_company_data' => [
'token' => env('WEBSITE_COMPANY_DATA_TOKEN'),
'url' => env(
'WEBSITE_COMPANY_DATA_URL',
'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract'
),
],
Изберете архитектура што одговара на интеракцијата
Ова е интерактивна операција за претпополнување: продавачот внесува веб-страница и веднаш очекува предлози. Редицата би додала анкетирање, управување со состојба и уште една граница за неуспех, без да ја подобри оваа кратка патека на барање. Затоа синхрон контролер е соодветен, под услов времето за мрежа да е ограничено.
Функционалноста има четири одговорности:
- Барањето на формата ја нормализира и валидира поднесената веб-страница.
- Наменски клиент се грижи за автентикацијата, временското ограничување, повторниот обид и однесувањето при грешки од надворешниот систем.
- Доменски DTO ги прифаќа само документираните полиња
company,contact,email,phoneиpeople. - Контролер враќа нацрт за потенцијален клиент што прелистувачот може да го постави во полиња што може да се уредуваат.
Резултатот од API-то не ажурира директно база на податоци. Содржината на веб-страницата може да биде нецелосна, застарена или двосмислена, па продавачот го прегледува нацртот пред вообичаената операција за зачувување на CRM-системот.
Создадете ја Laravel-функционалноста
Примерот е наменет за PHP 8.3 или понов и тековна Laravel-апликација. Не е потребен HTTP-пакет од трета страна.
composer create-project laravel/laravel crm-prefill
cd crm-prefill
php artisan make:request PrefillLeadRequest
php artisan make:controller LeadPrefillController
php artisan make:class Data/CompanyData
php artisan make:class Exceptions/CompanyDataException
php artisan make:class Services/WebsiteCompanyDataClient
Валидирајте и нормализирајте ја веб-страницата
Продавачите често внесуваат example.com наместо целосен URL. Барањето подолу додава HTTPS кога шемата недостига, прифаќа само HTTP или HTTPS, отфрла вградени акредитиви и блокира буквални приватни или резервирани IP-адреси. Мрежната политика заснована на DNS останува одговорност на надворешната услуга.
<?php
// app/Http/Requests/PrefillLeadRequest.php
namespace App\Http\Requests;
use Closure;
use Illuminate\Foundation\Http\FormRequest;
final class PrefillLeadRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$website = trim((string) $this->input('website'));
if ($website !== '' && ! preg_match('~^https?://~i', $website)) {
$website = 'https://'.$website;
}
$this->merge(['website' => $website]);
}
public function rules(): array
{
return [
'website' => [
'required',
'string',
'max:2048',
'url:http,https',
function (string $attribute, mixed $value, Closure $fail): void {
$parts = parse_url((string) $value);
if (! is_array($parts) || ! isset($parts['host'])) {
return;
}
if (isset($parts['user']) || isset($parts['pass'])) {
$fail('The website must not contain credentials.');
return;
}
$host = strtolower($parts['host']);
if ($host === 'localhost') {
$fail('The website must be publicly reachable.');
return;
}
$isIp = filter_var($host, FILTER_VALIDATE_IP) !== false;
$isPublicIp = filter_var(
$host,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) !== false;
if ($isIp && ! $isPublicIp) {
$fail('The website must use a public address.');
}
},
],
];
}
}
Мапирајте го одговорот на границата на апликацијата
Договорот за услугата именува пет вратени вредности, но интеграцијата не треба да им верува на нивните типови при извршување. Маперот прифаќа низи за четирите полиња за потенцијалниот клиент и низа за луѓето. Недостасувачките, празните или неочекуваните вредности стануваат безбедни стандардни вредности, наместо несигурни податоци да протекуваат низ апликацијата.
<?php
// app/Data/CompanyData.php
namespace App\Data;
final readonly class CompanyData
{
public function __construct(
public ?string $company,
public ?string $contact,
public ?string $email,
public ?string $phone,
public array $people,
) {}
public static function fromPayload(array $payload): self
{
$people = $payload['people'] ?? [];
return new self(
company: self::text($payload['company'] ?? null),
contact: self::text($payload['contact'] ?? null),
email: self::text($payload['email'] ?? null),
phone: self::text($payload['phone'] ?? null),
people: is_array($people) ? $people : [],
);
}
public function toArray(): array
{
return [
'company' => $this->company,
'contact' => $this->contact,
'email' => $this->email,
'phone' => $this->phone,
'people' => $this->people,
];
}
private static function text(mixed $value): ?string
{
if (! is_string($value)) {
return null;
}
$value = trim($value);
return $value === '' ? null : $value;
}
}
Ако официјалниот договор за одговор подоцна дефинира вгнездени структури за некое поле, ажурирајте го овој единствен мапер наместо да нагаѓате произволни клучеви на објекти низ контролерите и погледите.
Изградете ограничен, селективен HTTP-клиент
Клиентот дозволува два обида за ова идемпотентно GET-барање. Тој повторува при неуспеси на поврзувањето и избрани привремени одговори од серверот. Одговор за ограничување на стапката се повторува само кога нумеричко заглавие Retry-After бара не повеќе од една секунда. Неуспесите при автентикација и валидација никогаш не се повторуваат.
<?php
// app/Exceptions/CompanyDataException.php
namespace App\Exceptions;
use RuntimeException;
final class CompanyDataException extends RuntimeException
{
public function __construct(
public readonly string $category,
public readonly int $httpStatus,
string $message,
) {
parent::__construct($message);
}
}
<?php
// app/Services/WebsiteCompanyDataClient.php
namespace App\Services;
use App\Data\CompanyData;
use App\Exceptions\CompanyDataException;
use Exception;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
final class WebsiteCompanyDataClient
{
public function extract(string $website): CompanyData
{
$token = config('services.website_company_data.token');
$endpoint = config('services.website_company_data.url');
if (! is_string($token) || $token === '') {
throw new CompanyDataException(
'configuration',
503,
'Company enrichment is not configured.'
);
}
$started = microtime(true);
try {
$response = Http::acceptJson()
->connectTimeout(2)
->timeout(8)
->retry(
2,
fn (int $attempt, Exception $e): int =>
$this->retryDelay($attempt, $e),
fn (Exception $e): bool => $this->isRetryable($e),
throw: false,
)
->get((string) $endpoint, [
'token' => $token,
'website' => $website,
]);
} catch (ConnectionException) {
throw new CompanyDataException(
'network',
503,
'Company enrichment is temporarily unavailable.'
);
}
Log::info('website_company_data.completed', [
'host' => parse_url($website, PHP_URL_HOST),
'status' => $response->status(),
'latency_ms' => (int) ((microtime(true) - $started) * 1000),
]);
if (in_array($response->status(), [401, 403], true)) {
throw new CompanyDataException(
'authentication',
502,
'Company enrichment credentials were rejected.'
);
}
if ($response->status() === 429) {
throw new CompanyDataException(
'rate_limited',
429,
'Company enrichment is busy. Please try again shortly.'
);
}
if ($response->status() >= 500) {
throw new CompanyDataException(
'upstream',
503,
'Company enrichment is temporarily unavailable.'
);
}
if (! $response->successful()) {
throw new CompanyDataException(
'upstream_request',
502,
'The website could not be enriched.'
);
}
$payload = $response->json();
if (! is_array($payload)) {
throw new CompanyDataException(
'invalid_response',
502,
'Company enrichment returned an invalid response.'
);
}
return CompanyData::fromPayload($payload);
}
private function isRetryable(Exception $exception): bool
{
if ($exception instanceof ConnectionException) {
return true;
}
if (! $exception instanceof RequestException) {
return false;
}
$status = $exception->response->status();
if ($status === 429) {
$retryAfter = $exception->response->header('Retry-After');
return is_string($retryAfter)
&& ctype_digit($retryAfter)
&& (int) $retryAfter <= 1;
}
return in_array($status, [500, 502, 503, 504], true);
}
private function retryDelay(int $attempt, Exception $exception): int
{
if ($exception instanceof RequestException
&& $exception->response->status() === 429) {
return min(
1000,
((int) $exception->response->header('Retry-After')) * 1000
);
}
return 250 * $attempt;
}
}
Изложете ја крајната точка за претпополнување
Контролерот претвора исчистен неуспех на интеграцијата во стабилен одговор на апликацијата. Тој ги евидентира само категоријата на неуспех, статусот и името на домаќинот — не токенот, целата низа на барање, телото од надворешниот систем или патеката на поднесената страница.
<?php
// app/Http/Controllers/LeadPrefillController.php
namespace App\Http\Controllers;
use App\Exceptions\CompanyDataException;
use App\Http\Requests\PrefillLeadRequest;
use App\Services\WebsiteCompanyDataClient;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
final class LeadPrefillController extends Controller
{
public function __invoke(
PrefillLeadRequest $request,
WebsiteCompanyDataClient $client,
): JsonResponse {
$website = $request->validated('website');
try {
return response()->json([
'data' => $client->extract($website)->toArray(),
]);
} catch (CompanyDataException $exception) {
Log::warning('website_company_data.failed', [
'category' => $exception->category,
'status' => $exception->httpStatus,
'host' => parse_url($website, PHP_URL_HOST),
]);
return response()->json([
'error' => [
'code' => $exception->category,
'message' => $exception->getMessage(),
],
], $exception->httpStatus);
}
}
}
<?php
// routes/web.php
use App\Http\Controllers\LeadPrefillController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->group(function (): void {
Route::view('/crm/leads/create', 'leads.create')
->name('leads.create');
Route::post('/crm/leads/prefill', LeadPrefillController::class)
->middleware('throttle:20,1')
->name('leads.prefill');
});
Поврзете го со формата за потенцијален клиент
Поставете го следниов фрагмент во постојната автентицирана форма за потенцијален клиент на CRM-системот. Копчето за претпополнување го повикува Laravel, ги копира прифатените скаларни вредности во влезови што може да се уредуваат и ја прикажува колекцијата на луѓе за преглед. Постојната акција за зачувување на CRM-системот останува авторитетна.
<input id="website" name="website" placeholder="example.com">
<button id="prefill" type="button">Prefill company data</button>
<input id="company" name="company">
<input id="contact" name="contact">
<input id="email" name="email" type="email">
<input id="phone" name="phone">
<pre id="people"></pre>
<p id="prefill-status"></p>
<script>
document.getElementById('prefill').addEventListener('click', async () => {
const status = document.getElementById('prefill-status');
status.textContent = 'Looking up company data…';
try {
const response = await fetch('/crm/leads/prefill', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({
website: document.getElementById('website').value
})
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error?.message ?? 'Prefill failed.');
}
for (const field of ['company', 'contact', 'email', 'phone']) {
document.getElementById(field).value = payload.data[field] ?? '';
}
document.getElementById('people').textContent =
JSON.stringify(payload.data.people, null, 2);
status.textContent = 'Company data is ready for review.';
} catch (error) {
status.textContent = error.message;
}
});
</script>
Тестирајте успех, валидација и неуспех при автентикација
Http::fake() го одржува пакетот тестови детерминистички и спречува тестовите да трошат квота од планот. Овие тестови ги потврдуваат точниот метод, крајната точка, автентикацијата преку параметар за барање, мапирањето на одговорот, локалната валидација и правилото дека неуспесите при автентикација не се повторуваат.
<?php
// tests/Feature/LeadPrefillTest.php
namespace Tests\Feature;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class LeadPrefillTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware();
config([
'services.website_company_data.token' => 'YOUR_SERVICE_TOKEN',
'services.website_company_data.url' =>
'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract',
]);
}
public function test_it_returns_a_lead_draft(): void
{
Http::fake([
'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract*' =>
Http::response([
'company' => 'Example Company',
'contact' => 'General enquiries',
'email' => '[email protected]',
'phone' => '+1 555 0100',
'people' => [],
]),
]);
$this->postJson('/crm/leads/prefill', [
'website' => 'example.com',
])->assertOk()->assertJsonPath(
'data.company',
'Example Company'
);
Http::assertSent(fn (Request $request): bool =>
$request->method() === 'GET'
&& $request['token'] === 'YOUR_SERVICE_TOKEN'
&& $request['website'] === 'https://example.com'
);
}
public function test_it_rejects_an_invalid_scheme_without_an_api_call(): void
{
$this->postJson('/crm/leads/prefill', [
'website' => 'ftp://example.com',
])->assertUnprocessable();
Http::assertNothingSent();
}
public function test_it_does_not_retry_rejected_credentials(): void
{
Http::fake([
'*' => Http::response([], 401),
]);
$this->postJson('/crm/leads/prefill', [
'website' => 'https://example.com',
])->assertStatus(502)
->assertJsonPath('error.code', 'authentication');
Http::assertSentCount(1);
}
}
Безбедност, набљудливост и распоредување
Бидејќи автентикацијата е задолжителна како параметар за барање, инфраструктурата заслужува посебно внимание. Одржувајте HTTPS овозможен од крај до крај и конфигурирајте обратни проксија, системи за следење, известувачи за исклучоци и алатки за перформанси на апликацијата да ги редигираат низите на барањето. Кодот на Laravel никогаш не ги евидентира крајната точка или токенот, но инфраструктурата надвор од Laravel стандардно може да снима целосни URL-адреси.
Заштитете ја рутата со автентикација, авторизација соодветна за CRM-корисниците, CSRF-заштита и ограничување на барања на ниво на апликација. Третирајте ги сите извлечени податоци како недоверлив влез при нивното прикажување; екранираниот излез на Blade и DOM textContent се побезбедни од суров HTML. Не зачувувајте тивко податоци за луѓе и не ги користете за да поттикнете контактирање без правила за преглед и усогласеност релевантни за апликацијата.
Следете броеви и латентност за успешни барања, неуспеси при валидација, ограничувања на стапката, неуспеси од надворешниот систем и неуспеси при автентикација. Ненадеен пораст на неуспеси при автентикација обично укажува на истечен или повторно генериран токен; растечка латентност или 5xx-одговори укажуваат на проблем со надворешна зависност. Предупредувањата треба да ги агрегираат категориите наместо да прикачуваат чувствителни детали за барањата.
Поставете го продукцискиот токен пред кеширање на конфигурацијата, а потоа распоредете со вообичаените Laravel-проверки:
php artisan test
php artisan config:cache
php artisan route:cache
По ротирање на токенот за услугата, ажурирајте ја тајната во секоја околина и повторно изградете го кешот на конфигурацијата на Laravel. Рестартирање на worker не е потребно за овој синхрон дизајн, но долготрајните сервери за апликации треба повторно да се вчитаат според вообичаената постапка на платформата за распоредување.
Вообичаени начини на неуспех
- Секое барање враќа грешка при автентикација: потврдете дека токенот доаѓа од панелот Service token на оваа услуга, а потоа освежете ја кешираната конфигурација. Повторно генериран токен веднаш го заменува претходниот активен токен.
- Влез што изгледа валидно враќа 422: проверете го нормализираниот URL и потврдете дека користи HTTP или HTTPS, не содржи вградени акредитиви и не упатува на буквална приватна адреса.
- Одговорот е успешен, но полињата се празни: проверете ги договорот со надворешниот систем и маперот. Границата намерно отфрла неочекувани типови наместо да нагаѓа недокументирани облици.
- Барањата повремено враќаат 429: не ги зголемувајте повторните обиди. Почитувајте го капацитетот на планот, одржувајте ја пораката во корисничкиот интерфејс применлива и обидете се повторно подоцна, наместо едно ограничено барање да го претворите во налет.
- Продукцијата гледа промени што локалниот развој не ги гледа: исчистете го и повторно изградете го кешот на конфигурацијата на Laravel по промена на поставки поткрепени со околината.
Завршна листа за проверка
- Продавачот може да внесе гол домен или целосен јавен HTTP/HTTPS URL.
- Серверот испраќа GET-барања само до точно конфигурираната крајна точка за извлекување.
- И
tokenиwebsiteсе параметри за барање. - Податоците за компанијата, контактот, е-поштата, телефонот и луѓето минуваат низ еден дефанзивен мапер.
- Временските ограничувања и бројот на повторни обиди се ограничени, а 401, 403 и неуспесите при валидација не се повторуваат.
- Ниту една акредитива или целосен автентициран URL не се појавува во код, дневници, тестови или мониторинг.
- Вратениот потенцијален клиент останува уредлив и се прегледува пред да биде зачуван.
Највредниот дел од оваа функционалност не е HTTP-повикот. Тоа е границата околу него: една веб-страница влегува, предвидлив нацрт за потенцијален клиент излегува, привремените неуспеси остануваат ограничени и човекот ја задржува конечната одлука. Тоа е она што го претвора практичното збогатување во доверлив CRM-работен тек.