Laravel: Pretvorite popise web-stranica u korisne uvide o tvrtkama
Proračunska tablica puna web-mjesta tvrtki izgleda korisno, ali još nije alat za istraživanje. Netko još uvijek mora otvoriti svako web-mjesto, pronaći podatke za kontakt, kopirati informacije o tvrtki i odlučiti koji zapisi zaslužuju pažnju. Taj ponavljajući posao postaje osobito nezgodan kada se popis mijenja svaki tjedan.
Ovaj vodič izrađuje Laravelov proces spreman za produkciju koji uvozi URL-ove web-mjesta iz CSV-a, obogaćuje ih strukturiranim podacima o tvrtki i kontaktima, sigurno obrađuje zahtjeve u pozadini i izvozi proračunsku tablicu za pregled. Dizajn je namjerno skroman: Laravelov HTTP klijent, red baze podataka, alat za sastavljanje upita i alati za automatizirano testiranje su dovoljni.
Pribavite pristup prije pisanja integracijskog koda
Započnite stvaranjem računa na stranici za registraciju ili upotrijebite stranicu za prijavu ako ga već imate.
- Otvorite stranicu usluge Website to Company data.
- Odaberite dostupni plan 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 uslugu.
- Pohranite ga u konfiguraciju okruženja svojeg projekta. Nikada ga nemojte predavati u kontrolu izvornog koda.
Ponovno generiranje tokena usluge opoziva prethodno aktivni token. Ponovno generiranje tretirajte kao rotaciju vjerodajnica: odmah ažurirajte svako implementirano okruženje, ponovno pokrenite dugotrajne radnike i provjerite zahtjev prije nego što rotaciju smatrate dovršenom.
Potvrdite točan HTTP ugovor
Usluga koristi autentificirani GET zahtjev prema https://ai.mihajlo.mk/api/website-to-company-data/v1/extract. Autentifikacija se dostavlja putem parametra upita token, dok se javni URL tvrtke dostavlja putem website.
Iz pouzdanog terminala pošaljite jedan minimalni zahtjev. Imajte na umu da naredbe koje sadržavaju vjerodajnice u nizu upita mogu završiti u povijesti ljuske, stoga po potrebi uklonite unos iz povijesti i nikada ne lijepite naredbu u tikete ili zapisnike.
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'
Nakon što to uspije, smjestite vjerodajnicu u Laravelovu nepotvrđenu datoteku .env:
WEBSITE_COMPANY_SERVICE_TOKEN=YOUR_SERVICE_TOKEN
QUEUE_CONNECTION=database
Izložite je putem config/services.php. Aplikacijski kôd treba čitati konfiguraciju, a nikada izravno pozivati env(), jer implementirane Laravel aplikacije često predmemoriraju konfiguraciju.
<?php
return [
// Existing services...
'website_company' => [
'token' => env('WEBSITE_COMPANY_SERVICE_TOKEN'),
'endpoint' => 'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract',
],
];
Odaberite malu, otpornu arhitekturu
Ulaz će biti CSV izvezen iz Excela, Google Sheetsa ili druge proračunske tablice. Mora sadržavati stupac website. Podrška za izvorne XLSX datoteke zahtijevala bi dodatni paket bez poboljšanja tijeka obogaćivanja, stoga je CSV čišća granica.
Naredba za uvoz validira i uklanja duplikate redaka, trajno sprema rad na čekanju i šalje jedan posao reda po web-mjestu. Namjenski klijent upravlja ugovorom s vanjskim API-jem. Posao sprema normalizirane podatke ili strukturirani neuspjeh. Naposljetku, naredba za izvoz stvara CSV koji osoba može sortirati, bilježiti i pregledati.
app/
Console/Commands/ImportCompanyResearch.php
Console/Commands/ExportCompanyResearch.php
Data/CompanyResearch.php
Exceptions/ServiceFailure.php
Jobs/ResearchWebsite.php
Services/WebsiteCompanyClient.php
database/migrations/
xxxx_xx_xx_create_company_research_table.php
tests/Feature/
WebsiteCompanyIntegrationTest.php
Ovaj asinkroni dizajn važan je za više od brzine. Proračunska tablica može sadržavati neispravne URL-ove, privremene mrežne kvarove ili više redaka nego što trenutačni plan može odmah obraditi. Stavljanje u red održava uvoz responzivnim i daje prolaznim kvarovima ograničenu drugu priliku.
Izradite granicu postojanosti
Izradite migraciju reda, migraciju aplikacije, klase i naredbe pomoću Artisana. Ovisno o verziji Laravel aplikacije, migracija tablice poslova reda možda već postoji; generirajte je samo ako ne postoji.
php artisan make:queue-table
php artisan make:migration create_company_research_table
php artisan make:job ResearchWebsite
php artisan make:command ImportCompanyResearch
php artisan make:command ExportCompanyResearch
php artisan migrate
Tablica istraživanja zadržava izvorni URL, stanje obrade, uspješno učitavanje i sigurne metapodatke o pogrešci. Jedinstveno ograničenje za web-mjesto također štiti od dvostrukih redaka u proračunskoj tablici.
<?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('company_research', function (Blueprint $table): void {
$table->id();
$table->string('website', 2048)->unique();
$table->string('status', 24)->index();
$table->json('data')->nullable();
$table->string('error_kind', 40)->nullable();
$table->string('error_message')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('company_research');
}
};
Mapirajte nesigurne podatke na granici aplikacije
Dokumentirana granica aplikacije sadrži podatke company, contact, email, phone i people. Oblik pojedinačnih vrijednosti može se mijenjati, stoga objekt domene čuva skalarne vrijednosti, nizove ili null vrijednosti umjesto da pretpostavlja nedokumentirana ugniježđena polja.
<?php
// app/Data/CompanyResearch.php
namespace App\Data;
use JsonException;
use UnexpectedValueException;
final readonly class CompanyResearch
{
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
{
$fields = ['company', 'contact', 'email', 'phone', 'people'];
if (array_intersect($fields, array_keys($payload)) === []) {
throw new UnexpectedValueException('Expected research fields are absent.');
}
foreach ($fields as $field) {
$value = $payload[$field] ?? null;
if (! is_null($value) && ! is_scalar($value) && ! is_array($value)) {
throw new UnexpectedValueException("Invalid {$field} value.");
}
}
return new self(
$payload['company'] ?? null,
$payload['contact'] ?? null,
$payload['email'] ?? null,
$payload['phone'] ?? null,
$payload['people'] ?? null,
);
}
public function toArray(): array
{
return [
'company' => $this->company,
'contact' => $this->contact,
'email' => $this->email,
'phone' => $this->phone,
'people' => $this->people,
];
}
}
Ovo je važna produkcijska navika: mapirajte samo ugovor koji posjedujete. Nemojte dopustiti kontrolerima i poslovima da pristupaju spekulativnim strukturama, poput navodnog naziva tvrtke ili polja primarne e-pošte.
Izradite ograničeni HTTP klijent
Klijent u nastavku primjenjuje zasebna vremenska ograničenja za povezivanje i ukupni odgovor. Ponovno pokušava samo kod neuspjelih povezivanja, HTTP odgovora 429 i pogrešaka poslužitelja. Pogreške autentifikacije i validacije vraćaju se odmah jer ponavljanje istog odbijenog zahtjeva troši kvotu i prikriva probleme s konfiguracijom.
<?php
// app/Exceptions/ServiceFailure.php
namespace App\Exceptions;
use RuntimeException;
use Throwable;
final class ServiceFailure extends RuntimeException
{
public function __construct(
public readonly string $kind,
public readonly ?int $status = null,
?Throwable $previous = null,
) {
parent::__construct("Website research failed: {$kind}", 0, $previous);
}
}
// app/Services/WebsiteCompanyClient.php
namespace App\Services;
use App\Data\CompanyResearch;
use App\Exceptions\ServiceFailure;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Throwable;
final class WebsiteCompanyClient
{
public function extract(string $website): CompanyResearch
{
$token = (string) config('services.website_company.token');
$endpoint = (string) config('services.website_company.endpoint');
if ($token === '') {
throw new ServiceFailure('configuration');
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = Http::acceptJson()
->connectTimeout(3)
->timeout(15)
->get($endpoint, [
'token' => $token,
'website' => $website,
]);
} catch (ConnectionException $exception) {
if ($attempt === 3) {
throw new ServiceFailure('transient', null, $exception);
}
usleep((250 * (2 ** ($attempt - 1))) * 1000);
continue;
}
if ($response->successful()) {
$payload = $response->json();
if (! is_array($payload)) {
throw new ServiceFailure('malformed_response', $response->status());
}
try {
return CompanyResearch::fromPayload($payload);
} catch (Throwable $exception) {
throw new ServiceFailure(
'malformed_response',
$response->status(),
$exception,
);
}
}
$status = $response->status();
$retryable = $status === 429 || $status >= 500;
if ($retryable && $attempt < 3) {
$retryAfter = trim($response->header('Retry-After', ''));
$delay = ctype_digit($retryAfter)
? min(10_000, ((int) $retryAfter) * 1000)
: 250 * (2 ** ($attempt - 1));
usleep(($delay + random_int(0, 150)) * 1000);
continue;
}
$kind = match (true) {
$status === 401 || $status === 403 => 'authentication',
$status === 429 => 'rate_limited',
$status === 400 || $status === 422 => 'validation',
$status >= 500 => 'transient',
default => 'service_response',
};
throw new ServiceFailure($kind, $status);
}
throw new ServiceFailure('transient');
}
}
Tijelo odgovora namjerno nije uključeno u poruke iznimki. Tijela uzvodnog odgovora mogu sadržavati podatke za kontakt ili pojedinosti implementacije te ne bi trebala dospjeti u nadzorne ploče reda i centralizirane zapisnike.
Obradite svako web-mjesto kao idempotentan posao
Posao ima dva pokušaja reda uz kratka ponavljanja na razini zahtjeva klijenta. Trajni neuspjesi bilježe se odmah. Iscrpljeni neuspjesi zbog ograničenja stope i prolazni neuspjesi dolaze do failed(), stvarajući zapis za pregled umjesto da nestanu.
<?php
namespace App\Jobs;
use App\Exceptions\ServiceFailure;
use App\Services\WebsiteCompanyClient;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Throwable;
final class ResearchWebsite implements ShouldQueue, ShouldBeUnique
{
use Queueable;
public int $tries = 2;
public int $timeout = 30;
public int $uniqueFor = 3600;
public bool $failOnTimeout = true;
public function __construct(public readonly string $website) {}
public function uniqueId(): string
{
return hash('sha256', $this->website);
}
public function backoff(): array
{
return [120];
}
public function handle(WebsiteCompanyClient $client): void
{
try {
$research = $client->extract($this->website);
} catch (ServiceFailure $failure) {
if (in_array($failure->kind, ['rate_limited', 'transient'], true)) {
throw $failure;
}
DB::table('company_research')
->where('website', $this->website)
->update([
'status' => 'failed',
'error_kind' => $failure->kind,
'error_message' => 'The service rejected or could not map this request.',
'updated_at' => now(),
]);
Log::warning('Company research rejected', [
'website' => $this->website,
'kind' => $failure->kind,
'status' => $failure->status,
]);
return;
}
DB::table('company_research')
->where('website', $this->website)
->update([
'status' => 'complete',
'data' => json_encode($research->toArray(), JSON_THROW_ON_ERROR),
'error_kind' => null,
'error_message' => null,
'updated_at' => now(),
]);
Log::info('Company research completed', ['website' => $this->website]);
}
public function failed(?Throwable $exception): void
{
DB::table('company_research')
->where('website', $this->website)
->update([
'status' => 'failed',
'error_kind' => 'retries_exhausted',
'error_message' => 'Temporary failure persisted after bounded retries.',
'updated_at' => now(),
]);
}
}
Uvezite proračunsku tablicu i izvezite rezultat
Naredba za uvoz očekuje zaglavlje nazvano website. Dodaje https:// kada redak sadržava samo naziv glavnog računala, odbacuje ne-HTTP sheme, preskače duplikate i neispravne retke ostavlja izvan reda.
<?php
namespace App\Console\Commands;
use App\Jobs\ResearchWebsite;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
final class ImportCompanyResearch extends Command
{
protected $signature = 'research:import {file}';
protected $description = 'Import company websites from a CSV file';
public function handle(): int
{
$path = realpath((string) $this->argument('file'));
if ($path === false || ! is_readable($path)) {
$this->error('The CSV file is not readable.');
return self::FAILURE;
}
$stream = fopen($path, 'rb');
$header = fgetcsv($stream);
if ($header === false) {
fclose($stream);
$this->error('The CSV file is empty.');
return self::FAILURE;
}
$header = array_map(fn ($value) => trim((string) $value), $header);
$websiteColumn = array_search('website', $header, true);
if ($websiteColumn === false) {
fclose($stream);
$this->error('A website column is required.');
return self::FAILURE;
}
$seen = [];
$queued = 0;
while (($row = fgetcsv($stream)) !== false) {
$website = trim((string) ($row[$websiteColumn] ?? ''));
if ($website !== '' && ! str_contains($website, '://')) {
$website = 'https://' . $website;
}
$parts = parse_url($website);
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
if (
filter_var($website, FILTER_VALIDATE_URL) === false ||
! in_array($scheme, ['http', 'https'], true) ||
isset($parts['user'], $parts['pass']) ||
isset($seen[$website])
) {
continue;
}
$seen[$website] = true;
$now = now();
DB::table('company_research')->upsert(
[[
'website' => $website,
'status' => 'pending',
'data' => null,
'error_kind' => null,
'error_message' => null,
'created_at' => $now,
'updated_at' => $now,
]],
['website'],
['status', 'data', 'error_kind', 'error_message', 'updated_at'],
);
ResearchWebsite::dispatch($website);
$queued++;
}
fclose($stream);
$this->info("Queued {$queued} unique websites.");
return self::SUCCESS;
}
}
Naredba za izvoz treba iterirati pomoću cursor() kako se veliki skup rezultata ne bi učitao u memoriju. Svako ugovoreno polje kodira se kao JSON kada je strukturirano, čuvajući podatke bez izmišljanja stupaca iz nedokumentiranog oblika.
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
final class ExportCompanyResearch extends Command
{
protected $signature = 'research:export';
protected $description = 'Export the reviewable company research CSV';
public function handle(): int
{
$path = storage_path('app/contact-research.csv');
$stream = fopen($path, 'wb');
if ($stream === false) {
$this->error('Could not create the export.');
return self::FAILURE;
}
fputcsv($stream, [
'website', 'status', 'company', 'contact',
'email', 'phone', 'people', 'error_kind',
]);
foreach (DB::table('company_research')->orderBy('website')->cursor() as $row) {
$data = is_string($row->data)
? json_decode($row->data, true)
: (array) ($row->data ?? []);
$cell = static fn (mixed $value): string =>
is_null($value) ? '' :
(is_scalar($value) ? (string) $value :
json_encode($value, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
fputcsv($stream, [
$row->website,
$row->status,
$cell($data['company'] ?? null),
$cell($data['contact'] ?? null),
$cell($data['email'] ?? null),
$cell($data['phone'] ?? null),
$cell($data['people'] ?? null),
$row->error_kind ?? '',
]);
}
fclose($stream);
$this->info("Exported {$path}");
return self::SUCCESS;
}
}
Dokažite granicu determinističkim testovima
Laravelov Http::fake() sprječava stvarne mrežne pozive i omogućuje testu da potvrdi slanje oba obavezna parametra upita. Drugi test potvrđuje da neuspjesi autentifikacije postaju strukturirani zapisi baze podataka umjesto da se slijepo ponavljaju.
<?php
namespace Tests\Feature;
use App\Jobs\ResearchWebsite;
use App\Services\WebsiteCompanyClient;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class WebsiteCompanyIntegrationTest extends TestCase
{
use RefreshDatabase;
public function test_it_maps_the_documented_boundary(): void
{
config()->set('services.website_company.token', 'test-token');
config()->set(
'services.website_company.endpoint',
'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract',
);
Http::fake([
'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract*' =>
Http::response([
'company' => ['name' => 'Example'],
'contact' => null,
'email' => ['[email protected]'],
'phone' => null,
'people' => [],
], 200),
]);
$result = app(WebsiteCompanyClient::class)
->extract('https://example.com');
$this->assertSame(['[email protected]'], $result->email);
Http::assertSent(fn (Request $request): bool =>
$request['token'] === 'test-token' &&
$request['website'] === 'https://example.com'
);
}
public function test_authentication_failure_is_recorded(): void
{
config()->set('services.website_company.token', 'expired-token');
Http::fake([
'*' => Http::response(['message' => 'Unauthorized'], 401),
]);
DB::table('company_research')->insert([
'website' => 'https://example.com',
'status' => 'pending',
'created_at' => now(),
'updated_at' => now(),
]);
ResearchWebsite::dispatchSync('https://example.com');
$this->assertDatabaseHas('company_research', [
'website' => 'https://example.com',
'status' => 'failed',
'error_kind' => 'authentication',
]);
}
}
Implementirajte, promatrajte i upravljajte procesom
Pri implementaciji dostavite token usluge putem upravitelja tajni hosting platforme, zatim pokrenite php artisan config:cache i php artisan migrate --force. Ponovno pokrenite radnike reda nakon promjene konfiguracije kako bi učitali novi token.
php artisan test
php artisan config:cache
php artisan migrate --force
php artisan queue:work --queue=default --tries=2 --timeout=35
php artisan research:import storage/app/companies.csv
php artisan research:export
Pokrenite radnika reda pod nadzorom procesa koji ga ponovno pokreće nakon rušenja i implementacija. Vremensko ograničenje radnika nešto je dulje od vremenskog ograničenja posla, koje je pak dulje od jednog vremenskog ograničenja HTTP odgovora. Te granice sprječavaju da zaglavljeni zahtjev neograničeno zauzima radnika.
Pratite brojeve zapisa na čekanju, dovršenih, neuspjelih, autentifikacijskih, ograničenih stopom i iscrpljenih zapisa. Postavite upozorenja za neuobičajene omjere neuspjeha ili rastući zaostatak zapisa na čekanju. Zapisnici trebaju sadržavati izvorno web-mjesto, kategoriju neuspjeha, HTTP status i identitet posla, ali nikada token usluge, potpuni URL zahtjeva, tijelo odgovora, adrese e-pošte, telefonske brojeve ili podatke o osobama.
Budući da vjerodajnica putuje u parametru upita, HTTPS je neophodan. Pregledajte postavke proxyja i HTTP instrumentacije kako biste osigurali da su nizovi upita redigirani. Ograničite pristup izvezenom CSV-u jer sadržava istraživanje kontakata, uspostavite pravilo zadržavanja i prikupljajte samo informacije primjerene vašoj zakonitoj poslovnoj svrsi.
Česti neuspjesi koje vrijedi brzo prepoznati
- Svaki redak prijavljuje autentifikaciju: potvrdite aktivaciju plana, konfiguraciju tokena i osvježavanje predmemorije konfiguracije. Ponovno generirani token poništava prethodni.
- Reci ostaju na čekanju: radnik reda vjerojatno je zaustavljen, sluša drugu vezu ili koristi zastarjelu konfiguraciju.
- Mnogi reci ograničeni su stopom: smanjite konkurentnost radnika i potvrdite kapacitet plana. Nemojte agresivno povećavati broj ponavljanja.
- Uspješan odgovor označen je kao neispravan: sigurno pregledajte odgovor izvan uobičajenih zapisnika i usporedite ga sa službenom dokumentacijom prije promjene mapera granice.
- Nijedan redak se ne uvozi: provjerite ima li CSV točno zaglavlje
websitei sadržava li valjana HTTP ili HTTPS odredišta. - Ista tvrtka pojavljuje se više puta: kanonizirajte ulazne URL-ove prema vlastitim poslovnim pravilima prije uvoza; trenutačno jedinstveno ograničenje razlikuje različite nizove URL-ova.
Završni kontrolni popis za provjeru
- Token postoji samo u konfiguraciji podržanoj okruženjem i nije prisutan u kontroli izvornog koda.
- Minimalni GET zahtjev doseže točnu krajnju točku
/v1/extracts parametrimatokeniwebsite. php artisan testprolazi bez kontaktiranja stvarne usluge.- Primijenjene su migracija reda baze podataka i migracija istraživanja.
- Nadzirani radnik reda radi s ograničenim vremenskim ograničenjima i brojem pokušaja.
- Mali CSV uvoz prelazi iz stanja na čekanju u dovršeno stanje ili strukturirano neuspješno stanje.
storage/app/contact-research.csvotvara se kao proračunska tablica za pregled.- Zapisnici, nadzor i proxyji ne otkrivaju tokene ni vraćene podatke za kontakt.
Dovršeni sustav ne pretvara se da je istraživanje potpuno automatizirano. Uklanja mehaničko pregledavanje i prepisivanje, a pritom čuva ljudsku prosudbu koja popis kontakata čini korisnim. To je ispravna podjela rada: softver pretvara raštrkana javna web-mjesta u dosljedne dokaze, a osoba odlučuje što zaslužuje djelovanje.