Laravel форми за понуди: беспрекорно збогатете ги барањата со податоци за компанијата
Формуларот за понуда треба да делува непосредно, дури и кога бизнисот бара повеќе од име и е-пошта. Ако потенцијален клиент достави веб-страница на компанија, можеме да го претвориме тој URL во структурирани податоци за компанијата и контактот — но прелистувачот не треба да чека додека надворешна услуга ја испитува страницата.
Овој туторијал го гради тој работен тек во Laravel и PHP 8.3. Апликацијата прво го зачувува барањето за понуда, враќа 202 Accepted и го збогатува преку задача во редица. Интеграцијата го користи вградениот HTTP клиент на Laravel, мапира несигурни надворешни податоци во контролиран доменски објект и ги третира неуспесите при автентикација, ограничувањата на стапка, истекувањата на време, повторните обиди и распоредувањето како приоритетни грижи.
Добијте пристап пред да пишувате код за интеграција
Оваа услуга бара токен ограничен на услугата. Започнете со регистрирање сметка, или користете ја страницата за најавување ако веќе имате.
- Отворете ја страницата на услугата Website to Company data.
- Изберете го достапниот Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата.
- Пронајдете го панелот Service token и копирајте го токенот ограничен на услугата што е прикажан таму.
Повторното генерирање на токенот го поништува претходно активниот токен. Третирајте ја ротацијата како промена при распоредување: ажурирајте ја секоја околина што ја користи акредитацијата, рестартирајте ги долготрајните работници, потврдете го новиот токен и дури потоа сметајте дека ротацијата е завршена.
Потврдете го точниот HTTP договор
Интеграцијата прави GET барање до https://ai.mihajlo.mk/api/website-to-company-data/v1/extract. Ја испраќа јавната веб-страница во параметарот за барање website и се автентицира со параметарот за барање token={serviceToken}.
Направете едно минимално барање пред да ја изградите функционалноста во Laravel:
curl --fail-with-body --silent --show-error --get \
'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract' \
--data-urlencode 'website=https://example.com' \
--data-urlencode 'token=YOUR_SERVICE_TOKEN'
Успешниот одговор содржи податоци за компанија, контакт, е-пошта, телефон и лица. Ќе ги мапираме тие пет полиња на границата на апликацијата, наместо да дозволиме надворешен одговор да се прошири низ доменскиот модел.
Складирајте ја акредитацијата во Laravel конфигурацијата
Ставете го вистинскиот токен само во .env датотеката на распоредена околина. Зачувајте го само местодржачот, никогаш акредитацијата, во .env.example.
WEBSITE_COMPANY_TOKEN=YOUR_SERVICE_TOKEN
QUEUE_CONNECTION=database
Додајте ја услугата во config/services.php. Задржувањето на фиксна крајна точка спречува случајна промена на околината да го испрати токенот на друг хост.
'website_company' => [
'endpoint' => 'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract',
'token' => env('WEBSITE_COMPANY_TOKEN'),
],
Дизајнирајте ја патеката на барањето околу латентноста и неуспехот
Надворешниот повик не припаѓа во HTTP барањето на формуларот. DNS-резолуцијата, далечинската обработка, ограничувањето на стапка и привремените мрежни неуспеси имаат непредвидлива латентност. Наместо тоа, патеката на барањето е намерно кратка:
- Валидирајте го и зачувајте го барањето за понуда.
- Испратете задача за збогатување откако ќе се потврди трансакцијата со базата на податоци.
- Вратете одговор веднаш.
- Дозволете работник од редицата да ја повика услугата и да го зачува мапираниот резултат.
Ова создава евентуална конзистентност: понудата постои веднаш, додека збогатувањето поминува низ pending, processing, completed или failed. Тој компромис е соодветен бидејќи деталите за компанијата го подобруваат продажниот тек, но не се потребни за прифаќање на барањето за понуда.
Релевантната структура на проектот е компактна:
app/
Data/CompanyEnrichment.php
Exceptions/EnrichmentException.php
Http/Controllers/QuoteRequestController.php
Jobs/EnrichQuoteRequest.php
Models/QuoteRequest.php
Services/WebsiteCompanyClient.php
config/services.php
database/migrations/..._create_quote_requests_table.php
routes/api.php
tests/Feature/QuoteEnrichmentTest.php
Создадете траен запис за понуда
Нова Laravel апликација веќе го вклучува HTTP клиентот. Создадете ги компонентите на апликацијата и создадете табела за редица во базата на податоци ако проектот веќе нема таква:
composer create-project laravel/laravel quote-enrichment
cd quote-enrichment
php artisan make:model QuoteRequest -m
php artisan make:controller QuoteRequestController
php artisan make:job EnrichQuoteRequest
php artisan queue:table
php artisan migrate
Ако миграцијата jobs веќе постои, не генерирајте дупликат. Додајте ги следните колони во миграцијата за барања за понуда. JSON-колоните ги зачувуваат структурираните вредности на услугата без да претпоставуваат дека податоците за е-пошта или телефон секогаш мора да бидат една низа.
Schema::create('quote_requests', function (Blueprint $table) {
$table->id();
$table->uuid('public_id')->unique();
$table->string('name');
$table->string('email');
$table->text('project');
$table->string('website', 2048);
$table->string('enrichment_status')->default('pending');
$table->json('company_data')->nullable();
$table->json('contact_data')->nullable();
$table->json('email_data')->nullable();
$table->json('phone_data')->nullable();
$table->json('people_data')->nullable();
$table->string('enrichment_error')->nullable();
$table->timestamps();
});
Моделот генерира јавен идентификатор што не може да се погоди и доследно ги претвора полињата за збогатување:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
final class QuoteRequest extends Model
{
protected $fillable = [
'name', 'email', 'project', 'website',
'enrichment_status', 'company_data', 'contact_data',
'email_data', 'phone_data', 'people_data',
'enrichment_error',
];
protected function casts(): array
{
return [
'company_data' => 'array',
'contact_data' => 'array',
'email_data' => 'array',
'phone_data' => 'array',
'people_data' => 'array',
];
}
protected static function booted(): void
{
static::creating(function (self $quote): void {
$quote->public_id ??= (string) Str::orderedUuid();
});
}
public function getRouteKeyName(): string
{
return 'public_id';
}
}
Мапирајте го API-одговорот на една граница
Надворешниот JSON заслужува сомнеж. Одговорот може да биде валиден JSON, додека поле недостасува, е null или е претставено поинаку од обликот што го очекува вашиот UI. DTO подолу ги препознава само петте договорени полиња и секое го нормализира во низа безбедна за JSON. Непознатите полиња од одговорот никогаш не влегуваат во моделот на апликацијата.
namespace App\Data;
final readonly class CompanyEnrichment
{
public function __construct(
public array $company,
public array $contact,
public array $email,
public array $phone,
public array $people,
) {}
public static function fromApi(array $payload): self
{
return new self(
company: self::normalize($payload['company'] ?? null),
contact: self::normalize($payload['contact'] ?? null),
email: self::normalize($payload['email'] ?? null),
phone: self::normalize($payload['phone'] ?? null),
people: self::normalize($payload['people'] ?? null),
);
}
private static function normalize(mixed $value): array
{
if (is_array($value)) {
return $value;
}
if (is_string($value) && trim($value) !== '') {
return ['value' => trim($value)];
}
return [];
}
}
Создадете мал исклучок што носи стабилна категорија на неуспех, одлука за повторен обид, HTTP-статус и опционално доцнење. Кодот насочен кон корисникот треба да зачува категорија како authentication, а не сурово тело на одговор.
namespace App\Exceptions;
use RuntimeException;
final class EnrichmentException extends RuntimeException
{
public function __construct(
public readonly string $kind,
public readonly bool $retryable,
public readonly ?int $status = null,
public readonly ?int $retryAfter = null,
) {
parent::__construct("Company enrichment failed: {$kind}");
}
}
Изградете ограничен HTTP клиент
Клиентот користи временско ограничување од три секунди за поврзување и вкупно временско ограничување од десет секунди за одговор. Намерно изведува еден мрежен обид. Повторните обиди во редицата го обезбедуваат одложувањето, спречувајќи вгнездени јамки за повторни обиди да ги умножат повиците и неочекувано да ја трошат квотата.
namespace App\Services;
use App\Data\CompanyEnrichment;
use App\Exceptions\EnrichmentException;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
final class WebsiteCompanyClient
{
public function extract(string $website): CompanyEnrichment
{
$token = config('services.website_company.token');
if (! is_string($token) || $token === '') {
throw new EnrichmentException('configuration', false);
}
try {
$response = Http::acceptJson()
->connectTimeout(3)
->timeout(10)
->get(config('services.website_company.endpoint'), [
'website' => $website,
'token' => $token,
]);
} catch (ConnectionException) {
throw new EnrichmentException('connection', true);
}
if (! $response->successful()) {
$status = $response->status();
$retryable = $status === 408 || $status === 429 || $status >= 500;
$kind = match ($status) {
401, 403 => 'authentication',
429 => 'rate_limited',
default => $retryable ? 'upstream' : 'request_rejected',
};
$retryAfter = filter_var(
$response->header('Retry-After'),
FILTER_VALIDATE_INT
);
throw new EnrichmentException(
kind: $kind,
retryable: $retryable,
status: $status,
retryAfter: $retryAfter === false
? null
: min(300, max(1, $retryAfter)),
);
}
$payload = $response->json();
if (! is_array($payload)) {
throw new EnrichmentException('invalid_response', false);
}
return CompanyEnrichment::fromApi($payload);
}
}
Неуспесите при валидација и автентикација се трајни сè додека не се променат внесот или конфигурацијата, па нивното повторување само би трошело квота. Неуспесите при поврзување, истекувањата на барања, ограничувањата на стапка и грешките на серверот се разумни кандидати за повторен обид.
Извршете збогатување во отпорна задача во редица
Задачата е безбедна за повторно извршување по пад на работник: завршена понуда излегува рано, додека друго успешно извршување ги презапишува истите колони за збогатување. Одложувањето расте од десет секунди на една минута, а потоа на пет минути. Нумеричка вредност Retry-After обезбедена од серверот се почитува во граница од пет минути.
namespace App\Jobs;
use App\Exceptions\EnrichmentException;
use App\Models\QuoteRequest;
use App\Services\WebsiteCompanyClient;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
use Throwable;
final class EnrichQuoteRequest implements ShouldQueue
{
use Queueable;
public int $tries = 4;
public int $timeout = 20;
public bool $failOnTimeout = true;
public function __construct(public QuoteRequest $quoteRequest) {}
public function backoff(): array
{
return [10, 60, 300];
}
public function handle(WebsiteCompanyClient $client): void
{
if ($this->quoteRequest->fresh()->enrichment_status === 'completed') {
return;
}
$this->quoteRequest->update(['enrichment_status' => 'processing']);
try {
$data = $client->extract($this->quoteRequest->website);
} catch (EnrichmentException $exception) {
Log::warning('Quote enrichment attempt failed', [
'quote_request_id' => $this->quoteRequest->public_id,
'kind' => $exception->kind,
'status' => $exception->status,
'attempt' => $this->attempts(),
]);
if ($exception->kind === 'rate_limited') {
$this->release($exception->retryAfter ?? 60);
return;
}
if ($exception->retryable) {
throw $exception;
}
$this->quoteRequest->update([
'enrichment_status' => 'failed',
'enrichment_error' => $exception->kind,
]);
return;
}
$this->quoteRequest->update([
'enrichment_status' => 'completed',
'company_data' => $data->company,
'contact_data' => $data->contact,
'email_data' => $data->email,
'phone_data' => $data->phone,
'people_data' => $data->people,
'enrichment_error' => null,
]);
}
public function failed(?Throwable $exception): void
{
$this->quoteRequest->update([
'enrichment_status' => 'failed',
'enrichment_error' => 'retries_exhausted',
]);
}
}
Прифатете го формуларот без чекање
Контролерот валидира HTTP или HTTPS URL, ја потврдува понудата и ја испраќа задачата само откако тоа потврдување ќе успее. Применете ограничување на стапка на рутата бидејќи анонимен формулар инаку може да ги троши и локалниот капацитет на редицата и квотата на услугата.
namespace App\Http\Controllers;
use App\Jobs\EnrichQuoteRequest;
use App\Models\QuoteRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
final class QuoteRequestController extends Controller
{
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:120'],
'email' => ['required', 'email', 'max:254'],
'project' => ['required', 'string', 'max:5000'],
'website' => ['required', 'url:http,https', 'max:2048'],
]);
$quote = DB::transaction(function () use ($validated) {
$quote = QuoteRequest::create($validated);
EnrichQuoteRequest::dispatch($quote)->afterCommit();
return $quote;
});
return response()->json([
'id' => $quote->public_id,
'enrichment_status' => $quote->enrichment_status,
], 202);
}
public function show(QuoteRequest $quoteRequest): JsonResponse
{
return response()->json([
'id' => $quoteRequest->public_id,
'enrichment_status' => $quoteRequest->enrichment_status,
'company' => $quoteRequest->company_data,
'contact' => $quoteRequest->contact_data,
'email' => $quoteRequest->email_data,
'phone' => $quoteRequest->phone_data,
'people' => $quoteRequest->people_data,
]);
}
}
use App\Http\Controllers\QuoteRequestController;
use Illuminate\Support\Facades\Route;
Route::middleware('throttle:10,1')->group(function () {
Route::post('/quote-requests', [QuoteRequestController::class, 'store']);
Route::get('/quote-requests/{quoteRequest}', [QuoteRequestController::class, 'show']);
});
Јавниот UUID го отежнува случајното набројување, но не е замена за авторизација. Ако деталите за понудата се врзани за сметка, заштитете ја крајната точка за статус со вообичаените проверки за автентикација и политики на апликацијата.
Тестирајте испраќање, мапирање и трајни неуспеси
Http::fake() ги одржува тестовите детерминистички и докажува дека не се потребни вистинска акредитација или мрежна врска. Главните случаи се моментално испраќање, точни параметри за барање и мапирање, како и отсуство на јамка за повторен обид при одбиени акредитации.
namespace Tests\Feature;
use App\Jobs\EnrichQuoteRequest;
use App\Models\QuoteRequest;
use App\Services\WebsiteCompanyClient;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request as ClientRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
final class QuoteEnrichmentTest extends TestCase
{
use RefreshDatabase;
public function test_form_queues_enrichment_and_returns_immediately(): void
{
Queue::fake();
$response = $this->postJson('/api/quote-requests', [
'name' => 'Ada Developer',
'email' => '[email protected]',
'project' => 'A new product quote',
'website' => 'https://example.com',
]);
$response->assertStatus(202);
Queue::assertPushed(EnrichQuoteRequest::class);
}
public function test_job_maps_the_contracted_response_fields(): void
{
config(['services.website_company.token' => 'TEST_TOKEN']);
Http::fake([
'https://ai.mihajlo.mk/*' => Http::response([
'company' => ['name' => 'Example Company'],
'contact' => ['page' => '/contact'],
'email' => ['[email protected]'],
'phone' => ['+1 555 0100'],
'people' => [['name' => 'Example Person']],
]),
]);
$quote = QuoteRequest::create([
'name' => 'Ada Developer',
'email' => '[email protected]',
'project' => 'A quote',
'website' => 'https://example.com',
]);
(new EnrichQuoteRequest($quote))
->handle(app(WebsiteCompanyClient::class));
$this->assertDatabaseHas('quote_requests', [
'id' => $quote->id,
'enrichment_status' => 'completed',
]);
Http::assertSent(function (ClientRequest $request): bool {
return $request->method() === 'GET'
&& $request->data()['website'] === 'https://example.com'
&& $request->data()['token'] === 'TEST_TOKEN';
});
}
public function test_authentication_failure_is_recorded_without_throwing(): void
{
config(['services.website_company.token' => 'TEST_TOKEN']);
Http::fake(['https://ai.mihajlo.mk/*' => Http::response([], 401)]);
$quote = QuoteRequest::create([
'name' => 'Ada Developer',
'email' => '[email protected]',
'project' => 'A quote',
'website' => 'https://example.com',
]);
(new EnrichQuoteRequest($quote))
->handle(app(WebsiteCompanyClient::class));
$this->assertDatabaseHas('quote_requests', [
'id' => $quote->id,
'enrichment_status' => 'failed',
'enrichment_error' => 'authentication',
]);
}
}
Извршете го пакетот со php artisan test. Додавајте случаи за неправилен JSON, исклучоци при поврзување, ограничување на стапка и исцрпени повторни обиди како што се развива интеграцијата.
Безбедносни граници и набљудливост
Никогаш не ги евидентирајте токенот, целосниот URL на барањето, низата за авторизација во параметрите на барањето или суровиот одговор од нагорниот систем. Акредитациите во параметрите на барањето може да протечат преку невнимателни дневници за пристап и известување за исклучоци, па структурираните дневници на апликацијата треба да содржат само јавен идентификатор на понудата, категорија на неуспех, статусен код и број на обид.
Валидирајте ја веб-страницата пред да ја ставите во редица и ограничете ги анонимните поднесувања. Далечинската услуга ја изведува екстракцијата на веб-страницата, но прифаќањето само јавни HTTP и HTTPS URL-адреси на компании сепак ја одржува намерата на апликацијата јасна. Размислете за дополнителни деловни правила за localhost имиња, буквални приватни IP-адреси и домени што вашиот производ не ги поддржува.
Оперативно, следете ги бројот и староста на записите за збогатување во состојби pending, processing, completed и failed. Поставете предупредувања за трајни неуспеси при автентикација, невообичаен обем на ограничувања на стапка, растечка длабочина на редицата и стари редови во обработка. Тие сигнали разликуваат лоши акредитации од проблеми со капацитетот и инциденти кај нагорниот систем.
Распоредете го работникот, не само веб-апликацијата
Продукцијата бара континуирано надгледуван работник за редица. Доставете го WEBSITE_COMPANY_TOKEN преку складиштето за тајни на хостинг-платформата, извршете миграции, кеширајте ја конфигурацијата и стартувајте го работникот под systemd, Supervisor или механизмот за работници на платформата.
php artisan migrate --force
php artisan config:cache
php artisan queue:restart
php artisan queue:work --queue=default --tries=4 --timeout=20
queue:restart бара од постојните работници да излезат уредно по нивната тековна задача, овозможувајќи му на надзорникот на процеси да стартува работници со новиот код и токен. Осигурете се дека временското ограничување на самиот процес на работникот е подолго од временското ограничување на задачата. Задржете ги записите за неуспешни задачи и проверувајте ги со php artisan queue:failed; повторно извршувајте ги само откако ќе ја исправите основната причина.
Вообичаени неуспеси што вреди прецизно да се дијагностицираат
- Секое барање пријавува автентикација: потврдете дека околината го содржи активниот токен ограничен на услугата, повторно изградете го кешот на конфигурацијата и рестартирајте ги работниците. Повторно генериран токен го поништува својот претходник.
- Понудите остануваат pending: веб-апликацијата испраќа задачи, но ниту еден работник не ја троши конфигурираната редица.
- Понудите остануваат processing: проверете ги неуспешните задачи и дневниците за прекинување на работниците. Присилното исклучување на процесот може да спречи извршување на повратниот повик за неуспех на задачата.
- Ограничувањата на стапка се повторуваат: намалете ја злоупотребата на поднесувања, проверете го капацитетот на планот и задржете го ограниченото одложување. Додавањето непосредни повторни обиди го влошува проблемот.
- curl барањето работи, но Laravel не: споредете ја околината на работникот со интерактивната школка, исчистете ја застарената конфигурација и проверете го излезниот HTTPS-пристап.
- Полињата се неочекувано празни: проверете дезинфициран одговор во контролирана развојна околина и ажурирајте го само граничниот мапер. Не распрскувајте претпоставки за обликот на одговорот низ контролери и прегледи.
Конечна контролна листа за проверка
- Чекорите за регистрација, активација на план, документација и токен за услуга се завршени.
- Токенот постои само во конфигурација поткрепена од околината.
- Минималното
GETбарање успева со параметрите за барањеwebsiteиtoken. - Поднесувањето понуда враќа
202без да чека збогатување. - Вистински работник од редицата го менува записот од pending во completed.
- Податоците за компанија, контакт, е-пошта, телефон и лица се зачувуваат преку DTO-маперот.
- Неуспесите при автентикација и валидација не се повторуваат.
- Истекувањата на време, ограничувањата на стапка и неуспесите на серверот добиваат ограничени повторни обиди и корисни дневници.
- Тестовите поминуваат без надворешен мрежен пристап или вистински акредитации.
Важниот резултат е поголем од една збогатена форма. Барањето за понуда останува доверливо дури и кога давателот на збогатување е бавен или привремено недостапен, додека корисниот контекст за компанијата пристигнува неколку моменти подоцна. Таа поделба — брзо прифаќање на работ, дисциплинирана несигурност во заднина — е она што удобниот API-повик го претвора во продукциска интеграција.