Laravel obrasci za ponude: obogatite zahtjeve podacima o tvrtki bez žrtvovanja brzine
Obrazac za ponudu trebao bi djelovati neposredno. Posjetitelj unese nekoliko pojedinosti, primi potvrdu i nastavi dalje. Ipak, prodajni tim ima koristi od saznanja više nego što bi obrazac razumno trebao tražiti: naziv tvrtke, javne podatke za kontakt i relevantne osobe povezane s poslanom web-stranicom.
Čisto rješenje nije dulji obrazac ni sinkroni API poziv. To je kratka transakcija nakon koje slijedi obogaćivanje podataka u redu čekanja. Laravel vraća odgovor čim se ponuda pohrani, dok radnik u pozadini pretvara web-stranicu tvrtke u strukturirane podatke.
Dobijte pristup usluzi podataka o tvrtkama
Započnite stvaranjem računa na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako ga već imate.
- Otvorite stranicu usluge Website to Company data.
- Odaberite dostupni paket Free, Plus ili Pro i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte token ograničen na tu uslugu.
Ova usluga nije bez tokena. Zahtijeva token u parametru upita token={serviceToken}. Ponovno generiranje tokena opoziva prethodno aktivni token, stoga uskladite njegovu rotaciju s implementacijom aplikacije umjesto da ga nepromišljeno ponovno generirate.
Točan zahtjev je HTTPS GET prema https://ai.mihajlo.mk/api/website-to-company-data/v1/extract. Prije izrade značajke napravite minimalan zahtjev s vrijednostima rezerviranog mjesta:
curl --get \
--data-urlencode "token=YOUR_SERVICE_TOKEN" \
--data-urlencode "website=https://example.com" \
"https://ai.mihajlo.mk/api/website-to-company-data/v1/extract"
Imajte na umu da povijest naredbi i pregled procesa mogu otkriti argumente naredbenog retka. Koristite ovo samo kao kontrolirani osnovni test, nikada ne lijepite stvarni token u dokumentaciju ili tikete te očistite osjetljivu lokalnu povijest u skladu sa svojim operativnim postupcima.
Sada smjestite vjerodajnicu u okruženje projekta, a ne u kontrolu izvornog koda:
# .env
WEBSITE_COMPANY_TOKEN=YOUR_SERVICE_TOKEN
QUEUE_CONNECTION=database
<?php
// config/services.php
return [
// Existing services...
'website_company' => [
'token' => env('WEBSITE_COMPANY_TOKEN'),
'endpoint' => 'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract',
],
];
U .env.example spremite samo rezerviranu vrijednost. Produkcijske tajne pripadaju spremištu tajni platforme za implementaciju ili zaštićenoj konfiguraciji okruženja.
Arhitektura: prvo spremite, zatim obogatite
Ovaj projekt pretpostavlja PHP 8.3 ili noviji, postojeću Laravel aplikaciju, konfiguriranu bazu podataka i stvarnu asinkronu vezu reda čekanja. Nemojte koristiti upravljački program reda sync u produkciji: on bi izvršio obogaćivanje unutar zahtjeva obrasca i poništio ovaj dizajn.
Putanja zahtjeva ima samo tri odgovornosti:
- Validirati i pohraniti ponudu.
- Poslati zadatak obogaćivanja nakon potvrde transakcije baze podataka.
- Vratiti HTTP
202 Accepted.
Zadatak reda čekanja poziva vanjsku uslugu, mapira company, contact, email, phone i people na granici aplikacije, a zatim sprema normalizirani rezultat. Privremeni se neuspjesi ponovno pokušavaju uz ograničeno postupno odgađanje; neuspjesi autentikacije i validacije odmah se bilježe.
Relevantna struktura projekta namjerno je mala:
app/
Data/CompanyEnrichment.php
Exceptions/EnrichmentExceptions.php
Http/Controllers/QuoteController.php
Http/Requests/StoreQuoteRequest.php
Jobs/EnrichQuoteRequest.php
Models/Quote.php
Services/WebsiteToCompanyClient.php
config/services.php
database/migrations/..._create_quotes_table.php
routes/web.php
tests/Feature/QuoteEnrichmentTest.php
Pohranite eksplicitna stanja obogaćivanja
Samo JSON stupac koji dopušta null ne može razlikovati „nije započeto” od „nije uspjelo”. Uz mapirane podatke pohranite stanje i strojno čitljiv kod pogreške.
<?php
// database/migrations/2026_01_01_000000_create_quotes_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('quotes', function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->string('email');
$table->string('website', 2048);
$table->text('summary');
$table->string('enrichment_status')->default('pending');
$table->json('company_enrichment')->nullable();
$table->string('enrichment_error_code')->nullable();
$table->timestamp('enriched_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('quotes');
}
};
<?php
// app/Models/Quote.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
final class Quote extends Model
{
protected $fillable = ['name', 'email', 'website', 'summary'];
protected function casts(): array
{
return [
'company_enrichment' => 'array',
'enriched_at' => 'immutable_datetime',
];
}
}
Mapirajte nesigurne podatke na granici
Ugovor usluge navodi nazive vraćenih polja, ali produkcijski kod ne bi trebao pretpostavljati nedokumentirane unutarnje strukture. Mapper u nastavku prihvaća skalarne vrijednosti i vrijednosti polja sigurne za JSON, odbacuje objekte ili resurse te pohranjuje samo pet relevantnih polja. Time se sprječava širenje pretpostavki o obliku odgovora kroz aplikaciju.
<?php
// app/Data/CompanyEnrichment.php
namespace App\Data;
final readonly class CompanyEnrichment
{
public function __construct(
public mixed $company,
public mixed $contact,
public mixed $email,
public mixed $phone,
public mixed $people,
) {}
public static function fromPayload(array $payload): self
{
return new self(
self::safe($payload['company'] ?? null),
self::safe($payload['contact'] ?? null),
self::safe($payload['email'] ?? null),
self::safe($payload['phone'] ?? null),
self::safe($payload['people'] ?? null),
);
}
public function toArray(): array
{
return [
'company' => $this->company,
'contact' => $this->contact,
'email' => $this->email,
'phone' => $this->phone,
'people' => $this->people,
];
}
private static function safe(mixed $value): mixed
{
if ($value === null || is_scalar($value)) {
return is_string($value) ? trim($value) : $value;
}
if (! is_array($value)) {
return null;
}
$clean = [];
foreach ($value as $key => $item) {
$clean[$key] = self::safe($item);
}
return $clean;
}
}
Izradite ograničeni HTTP klijent
Laravelov ugrađeni HTTP klijent je dovoljan. Ovdje zadržite politiku prijenosa kako kontroleri i zadaci ne bi morali razumjeti statusne kodove. Neuspjesi povezivanja, neuspjesi poslužitelja, neispravna tijela uspješnih odgovora i HTTP 429 su privremeni. Autentikacija i odbijanje zahtjeva trajni su dok se konfiguracija ili unos ne promijene.
<?php
// app/Exceptions/EnrichmentExceptions.php
namespace App\Exceptions;
use RuntimeException;
use Throwable;
class TransientEnrichmentException extends RuntimeException
{
public function __construct(
public readonly string $reason,
public readonly ?int $status = null,
public readonly ?int $retryAfter = null,
?Throwable $previous = null,
) {
parent::__construct($reason, 0, $previous);
}
}
class PermanentEnrichmentException extends RuntimeException
{
public function __construct(
public readonly string $reason,
public readonly ?int $status = null,
) {
parent::__construct($reason);
}
}
<?php
// app/Services/WebsiteToCompanyClient.php
namespace App\Services;
use App\Data\CompanyEnrichment;
use App\Exceptions\PermanentEnrichmentException;
use App\Exceptions\TransientEnrichmentException;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use LogicException;
final class WebsiteToCompanyClient
{
public function extract(string $website): CompanyEnrichment
{
$token = config('services.website_company.token');
if (! is_string($token) || $token === '') {
throw new LogicException('Website company service token is not configured.');
}
try {
$response = Http::acceptJson()
->connectTimeout(2)
->timeout(8)
->get(config('services.website_company.endpoint'), [
'token' => $token,
'website' => $website,
]);
} catch (ConnectionException $exception) {
throw new TransientEnrichmentException(
'connection_failure',
previous: $exception,
);
}
if ($response->status() === 429) {
$header = filter_var(
$response->header('Retry-After'),
FILTER_VALIDATE_INT,
);
$delay = is_int($header) ? max(10, min(300, $header)) : 60;
throw new TransientEnrichmentException(
'rate_limited',
429,
$delay,
);
}
if ($response->serverError()) {
throw new TransientEnrichmentException(
'upstream_failure',
$response->status(),
);
}
if (in_array($response->status(), [401, 403], true)) {
throw new PermanentEnrichmentException(
'authentication_failure',
$response->status(),
);
}
if ($response->clientError()) {
throw new PermanentEnrichmentException(
'request_rejected',
$response->status(),
);
}
$payload = $response->json();
if (! is_array($payload)) {
throw new TransientEnrichmentException('malformed_response');
}
return CompanyEnrichment::fromPayload($payload);
}
}
Unutar HTTP klijenta namjerno nema petlje za trenutačno ponovno pokušavanje. Ponovni pokušaji na razini reda čekanja izbjegavaju zadržavanje radnika u ponovljenim mrežnim pozivima, a njihovo odgađanje daje ograničenju brzine ili privremenom prekidu vremena za oporavak.
Neka zahtjev obrasca bude brz
<?php
// app/Http/Requests/StoreQuoteRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
final class StoreQuoteRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:120'],
'email' => ['required', 'email', 'max:254'],
'website' => ['required', 'url:http,https', 'max:2048'],
'summary' => ['required', 'string', 'max:5000'],
];
}
}
<?php
// app/Http/Controllers/QuoteController.php
namespace App\Http\Controllers;
use App\Http\Requests\StoreQuoteRequest;
use App\Jobs\EnrichQuoteRequest;
use App\Models\Quote;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
final class QuoteController
{
public function store(StoreQuoteRequest $request): JsonResponse
{
$quote = DB::transaction(function () use ($request): Quote {
$quote = Quote::create($request->safe()->only([
'name', 'email', 'website', 'summary',
]));
EnrichQuoteRequest::dispatch($quote->id)->afterCommit();
return $quote;
});
return response()->json([
'id' => $quote->id,
'status' => 'accepted',
], 202);
}
}
// routes/web.php
use App\Http\Controllers\QuoteController;
use Illuminate\Support\Facades\Route;
Route::post('/quotes', [QuoteController::class, 'store'])
->middleware('throttle:10,1');
Budući da se ova ruta nalazi u web.php, uobičajene predaje iz preglednika također primaju Laravelovu CSRF zaštitu. Ograničavanje sprječava jednostavnu zloupotrebu; javni obrasci mogu dodatno trebati kontrole botova prikladne aplikaciji.
Obradite obogaćivanje s kontroliranim ponovnim pokušajima
<?php
// app/Jobs/EnrichQuoteRequest.php
namespace App\Jobs;
use App\Exceptions\PermanentEnrichmentException;
use App\Exceptions\TransientEnrichmentException;
use App\Models\Quote;
use App\Services\WebsiteToCompanyClient;
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 = 15;
public function __construct(public readonly int $quoteId) {}
public function backoff(): array
{
return [10, 60, 300];
}
public function handle(WebsiteToCompanyClient $client): void
{
$quote = Quote::find($this->quoteId);
if ($quote === null || $quote->enrichment_status === 'complete') {
return;
}
$quote->forceFill([
'enrichment_status' => 'processing',
'enrichment_error_code' => null,
])->save();
try {
$data = $client->extract($quote->website);
} catch (PermanentEnrichmentException $exception) {
$quote->forceFill([
'enrichment_status' => 'failed',
'enrichment_error_code' => $exception->reason,
])->save();
Log::warning('Quote enrichment permanently rejected', [
'quote_id' => $quote->id,
'reason' => $exception->reason,
'status' => $exception->status,
]);
return;
} catch (TransientEnrichmentException $exception) {
$quote->forceFill([
'enrichment_status' => 'pending',
'enrichment_error_code' => $exception->reason,
])->save();
Log::warning('Quote enrichment will be retried', [
'quote_id' => $quote->id,
'reason' => $exception->reason,
'status' => $exception->status,
]);
if ($exception->retryAfter !== null) {
$this->release($exception->retryAfter);
return;
}
throw $exception;
}
$quote->forceFill([
'company_enrichment' => $data->toArray(),
'enrichment_status' => 'complete',
'enrichment_error_code' => null,
'enriched_at' => now(),
])->save();
}
public function failed(?Throwable $exception): void
{
Quote::whereKey($this->quoteId)
->where('enrichment_status', '!=', 'complete')
->update([
'enrichment_status' => 'failed',
'enrichment_error_code' => 'retries_exhausted',
]);
}
}
Zapisnici sadrže ID-jeve ponuda, sigurne kodove razloga i HTTP statuse. Namjerno isključuju web-stranicu, vraćene podatke o osobama, tijelo odgovora, URL zahtjeva i token. To je posebno važno jer vjerodajnice u nizu upita mogu procuriti kroz neselektivno zapisivanje URL-ova.
Testirajte i brzinu i ponašanje na granici
Http::fake() čini vanjski ugovor determinističkim. Jedan test dokazuje da predaja stavlja rad u red bez slanja HTTP zahtjeva; drugi provjerava točnu metodu, krajnju točku, parametre upita i mapiranje odgovora.
<?php
// tests/Feature/QuoteEnrichmentTest.php
namespace Tests\Feature;
use App\Jobs\EnrichQuoteRequest;
use App\Services\WebsiteToCompanyClient;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
final class QuoteEnrichmentTest extends TestCase
{
use RefreshDatabase;
public function test_submission_returns_before_enrichment(): void
{
Queue::fake();
Http::preventStrayRequests();
$response = $this->postJson('/quotes', [
'name' => 'Ava Patel',
'email' => '[email protected]',
'website' => 'https://example.test',
'summary' => 'A small application redesign.',
]);
$response->assertStatus(202)->assertJson([
'status' => 'accepted',
]);
$this->assertDatabaseHas('quotes', [
'website' => 'https://example.test',
'enrichment_status' => 'pending',
]);
Queue::assertPushed(EnrichQuoteRequest::class);
Http::assertNothingSent();
}
public function test_client_maps_the_documented_fields(): void
{
config()->set('services.website_company.token', 'test-token');
Http::fake([
'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract*'
=> Http::response([
'company' => ['name' => 'Example Studio'],
'contact' => ['name' => 'Ava Patel'],
'email' => '[email protected]',
'phone' => '+1 555 0100',
'people' => [['name' => 'Ava Patel']],
], 200),
]);
$data = app(WebsiteToCompanyClient::class)
->extract('https://example.test')
->toArray();
$this->assertSame('Example Studio', $data['company']['name']);
$this->assertSame('[email protected]', $data['email']);
Http::assertSent(function (Request $request): bool {
parse_str(parse_url($request->url(), PHP_URL_QUERY) ?? '', $query);
return $request->method() === 'GET'
&& str_starts_with(
$request->url(),
'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract'
)
&& ($query['token'] ?? null) === 'test-token'
&& ($query['website'] ?? null) === 'https://example.test';
});
}
}
php artisan migrate
php artisan test --filter=QuoteEnrichmentTest
Sigurnost, vidljivost i implementacija
Obogaćivanje tretirajte kao osobne i poslovne kontaktne podatke, a ne kao bezopasne metapodatke. Ograničite pristup osoblja, definirajte politiku zadržavanja i izbjegavajte kopiranje cijelog odgovora uzvodne usluge kada aplikacija treba samo pet polja. Validirajte samo HTTP i HTTPS web-stranice te nikada ne koristite poslani URL kao cilj preusmjeravanja ili lokalnog dohvaćanja drugdje bez zasebnih zaštitnih mjera.
Pratite broj zapisa complete, pending i failed, starost reda čekanja, neuspjehe zadataka, klase HTTP statusa i latenciju obogaćivanja. Upozorite na trajne neuspjehe autentikacije jer oni obično ukazuju na nedostajući, opozvani ili zastarjeli token. Zasebno upozorite na ograničavanje brzine kako bi se kapacitet paketa i promet mogli procijeniti bez zamjene pritiska kvote s prekidom rada.
Implementirajte migraciju baze podataka prije koda koji upisuje nove stupce. Zatim predmemorirajte konfiguraciju, ponovno pokrenite radnike kako bi učitali novi token i kôd te pokrenite nadzirani radnik reda čekanja:
php artisan migrate --force
php artisan config:cache
php artisan queue:restart
php artisan queue:work --queue=default --tries=4 --timeout=20 --max-time=3600
Pokrenite radnika pod nadzornikom procesa operacijskog sustava ili upravljanom radničkom uslugom hosting platforme. Konfigurirajte prozor za ponovni pokušaj veze reda čekanja tako da bude dulji od vremenskih ograničenja zadatka i radnika; u suprotnom spor zadatak može postati vidljiv dvaput i proizvesti preklapajuće izvršavanje.
Uobičajeni neuspjesi koje vrijedi namjerno dijagnosticirati
- Svaki zadatak prijavljuje neuspjeh autentikacije: potvrdite da je token ograničen na tu uslugu prisutan u produkcijskoj konfiguraciji. Ako je ponovno generiran, prethodni aktivni token je opozvan. Osvježite tajnu, ponovno izgradite predmemoriju konfiguracije i ponovno pokrenite radnike.
- Obrazac je i dalje spor: provjerite da
QUEUE_CONNECTIONnijesynci da se slanje u red događa nakon pohrane ponude. - Ponude ostaju na čekanju: provjerite radi li radnik, obrađuje li ispravan red i može li dosegnuti HTTPS krajnju točku.
- HTTP 429 se ponavlja: zadržite ograničeno odgađanje umjesto trenutačnog ponovnog pokušaja. Pregledajte volumen zahtjeva i aktivirani paket Free, Plus ili Pro.
- Podaci su neočekivano prazni: pregledajte lažni odgovor i sigurno redigirani oblik odgovora, a zatim prilagodite samo mapper na granici. Nemojte raspršiti nagađanja o obliku odgovora kroz kontrolere i modele.
Kontrolni popis za završnu provjeru
- Obrazac vraća
202nakon lokalne transakcije baze podataka, bez čekanja na obogaćivanje. - Zahtjev u redu čekanja koristi
GET, točnu krajnju točku/v1/extracti obavezne parametre upitatokeniwebsite. - Aplikacija na granici mapira samo
company,contact,email,phoneipeople. - Vremenska ograničenja povezivanja i odgovora su ograničena, privremeni ponovni pokušaji su ograničeni, a neuspjesi autentikacije ili validacije ne pokušavaju se slijepo ponovno.
- U zapisnicima ili testnim podacima ne pojavljuju se token, puni URL zahtjeva, tijelo odgovora ni osobni kontaktni podaci.
- Produkcijski radnici koriste osvježenu konfiguraciju i nadziru se s obzirom na starost reda, neuspjehe i ograničavanje brzine.
Najjača značajka obogaćivanja gotovo je nevidljiva osobi koja ispunjava obrazac. Posjetitelj dobiva brzu potvrdu; tim nekoliko trenutaka kasnije dobiva koristan kontekst o tvrtki; a privremeni problemi uzvodne usluge postaju kontrolirano stanje u pozadini umjesto pokvarene interakcije s korisnikom. To razdvajanje čini razliku između pukog pozivanja API-ja i njegove odgovorne integracije.