Native PHP: Автоматско претпополнување на CRM потенцијални клиенти од веб-страниците на компаниите
Продавачот не треба да мора да копира име на компанија, телефонски број, е-адреса, детали за контакт и информации за тимот од веб-страница во CRM, поле по поле. Подобар работен тек ја бара веб-страницата еднаш, го збогатува нацрт-записот за потенцијалниот клиент и му остава на човекот да го потврди резултатот.
Ова упатство го гради тој работен тек во Native PHP 8.3. Апликацијата изложува мала JSON крајна точка што ја потврдува поднесената веб-страница, ја повикува услугата Website to Company data, го мапира одговорот во доменски објект и враќа полиња подготвени за CRM. Интеграцијата вклучува ограничени временски ограничувања, селективни повторни обиди, структурирани грешки, безбедно евидентирање и детерминистички PHPUnit тестови.
Добијте пристап и копирајте го токенот за услугата
Пред да напишете код за интеграција, регистрирајте сметка или најавете се. Отворете ја страницата на услугата Website to Company data, изберете достапен Free, Plus или Pro план и завршете ја неговата активација.
Потоа, отворете ја официјалната документација за услугата. Најдете го панелот Service token и копирајте го токенот ограничен на услугата што е прикажан таму. Оваа услуга не е без токен: секое барање мора да го испрати тој акредитив преку параметарот за пребарување token.
Повторното генерирање на токенот го поништува претходно активниот токен. Третирајте го повторното генерирање како ротација на акредитиви: ажурирајте ја околината на апликацијата и повторно распоредете ја секоја инстанца што ја користи старата вредност.
Точната API операција е GET https://ai.mihajlo.mk/api/website-to-company-data/v1/extract. Потврдете го пристапот со минимално барање:
curl --fail-with-body --get \
'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract' \
--data-urlencode 'token=YOUR_SERVICE_TOKEN' \
--data-urlencode 'website=https://example.com'
Барањето ги испраќа само документираните параметри token и website. Никогаш не вметнувајте вистински токен во контрола на изворниот код, тест-фикстури, слики од екранот или пораки за поддршка.
Изберете намерно мала архитектура
CRM треба да ја повикува нашата апликација, а не директно надворешниот API. Задржувањето на токенот на серверската страна спречува изложување во прелистувачот и ни дава една граница за валидација, мапирање на одговори, повторни обиди и телеметрија.
Проектот има четири одговорности:
- Контролер: ја прифаќа веб-страницата на продавачот и враќа JSON.
- Клиент: го применува API договорот и политиката за повторни обиди.
- Транспорт: го извршува природното cURL барање.
- Доменски мапер: ги претвора надворешните податоци во стабилен објект насочен кон CRM.
crm-prefill/
├── composer.json
├── .env
├── config/bootstrap.php
├── public/index.php
├── src/
│ ├── Company/CompanyProfile.php
│ ├── Company/IntegrationFailure.php
│ ├── Company/WebsiteCompanyClient.php
│ └── Http/
│ ├── CurlTransport.php
│ └── Transport.php
└── tests/WebsiteCompanyClientTest.php
Создадете ја Composer конфигурацијата и инсталирајте PHPUnit. PHP-овата cURL екстензија е единствената продукциска зависност.
{
"require": {
"php": "^8.3",
"ext-curl": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
composer install
composer dump-autoload
php -m | grep curl
Чувајте ја конфигурацијата надвор од апликацијата
За локален развој, додадете .env во .gitignore и таму зачувајте го копираниот токен:
WEBSITE_COMPANY_TOKEN="YOUR_SERVICE_TOKEN"
Native PHP не вчитува автоматски датотеки со променливи на околината. Следниот bootstrap поддржува едноставна локална датотека, додека им дозволува на продукциските променливи на околината да имаат предност:
<?php
// config/bootstrap.php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
$localFile = dirname(__DIR__) . '/.env';
if (is_file($localFile)) {
$values = parse_ini_file($localFile, false, INI_SCANNER_RAW);
if ($values === false) {
throw new RuntimeException('Unable to parse .env');
}
foreach ($values as $name => $value) {
if (getenv((string) $name) === false) {
putenv($name . '=' . $value);
}
}
}
$token = getenv('WEBSITE_COMPANY_TOKEN');
if (!is_string($token) || trim($token) === '') {
throw new RuntimeException('WEBSITE_COMPANY_TOKEN is not configured');
}
return ['service_token' => $token];
На продукциски PHP-FPM хостови, внесете WEBSITE_COMPANY_TOKEN преку процесниот менаџер или складиштето за тајни наместо да распоредувате .env. Ако се задржи локална датотека, чувајте ја надвор од јавниот корен на документи со рестриктивни дозволи.
Изградете ограничен природен cURL транспорт
Транспортот не следи пренасочувања, ниту за погодност ниту за опоравување. Тоа спречува случајно препраќање на акредитивот од низата за пребарување до друг хост. Временските ограничувања за поврзување и вкупното време обезбедуваат бавниот повик за збогатување да не зафаќа PHP работник неограничено.
<?php
// src/Http/Transport.php
declare(strict_types=1);
namespace App\Http;
interface Transport
{
/**
* @return array{
* status: int,
* headers: array<string,string>,
* body: string
* }
*/
public function get(
string $url,
array $query,
int $connectTimeoutMs,
int $timeoutMs
): array;
}
<?php
// src/Http/CurlTransport.php
declare(strict_types=1);
namespace App\Http;
use RuntimeException;
final class CurlTransport implements Transport
{
public function get(
string $url,
array $query,
int $connectTimeoutMs,
int $timeoutMs
): array {
$headers = [];
$requestUrl = $url . '?' . http_build_query(
$query,
'',
'&',
PHP_QUERY_RFC3986
);
$handle = curl_init($requestUrl);
if ($handle === false) {
throw new RuntimeException('Unable to initialize cURL');
}
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT_MS => $connectTimeoutMs,
CURLOPT_TIMEOUT_MS => $timeoutMs,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_HEADERFUNCTION => static function (
$curl,
string $line
) use (&$headers): int {
$length = strlen($line);
$position = strpos($line, ':');
if ($position !== false) {
$name = strtolower(trim(substr($line, 0, $position)));
$headers[$name] = trim(substr($line, $position + 1));
}
return $length;
},
]);
$body = curl_exec($handle);
if ($body === false) {
throw new RuntimeException('Network error: ' . curl_error($handle));
}
return [
'status' => (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE),
'headers' => $headers,
'body' => $body,
];
}
}
Мапирајте го API на границата на апликацијата
Услугата враќа податоци за компанија, контакт, е-пошта, телефон и лица. Надворешниот JSON не смее неконтролирано да протекува низ CRM, па маперот ги нормализира nullable скаларните полиња и проверува дали people е низа. Неочекуваните облици стануваат експлицитни грешки во шемата, наместо тивко да пополнуваат неточни полиња.
<?php
// src/Company/CompanyProfile.php
declare(strict_types=1);
namespace App\Company;
final readonly class CompanyProfile
{
public function __construct(
public ?string $company,
public ?string $contact,
public ?string $email,
public ?string $phone,
public array $people
) {}
public static function fromApi(array $data): self
{
$people = $data['people'] ?? [];
if (!is_array($people)) {
throw new IntegrationFailure(
'invalid_schema',
'The people field is not an array.'
);
}
return new self(
self::text($data, 'company'),
self::text($data, 'contact'),
self::text($data, 'email'),
self::text($data, 'phone'),
array_values($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(array $data, string $key): ?string
{
$value = $data[$key] ?? null;
if ($value === null || $value === '') {
return null;
}
if (!is_scalar($value)) {
throw new IntegrationFailure(
'invalid_schema',
"The {$key} field is not scalar."
);
}
return trim((string) $value);
}
}
<?php
// src/Company/IntegrationFailure.php
declare(strict_types=1);
namespace App\Company;
use RuntimeException;
final class IntegrationFailure extends RuntimeException
{
public function __construct(
public readonly string $kind,
string $message,
public readonly ?int $upstreamStatus = null
) {
parent::__construct($message);
}
}
Додајте селективни повторни обиди и безбедна телеметрија
Неуспесите со автентикација и одбиените барања се детерминистички, па нивното повторување само троши квота. Мрежните грешки, HTTP 429 одговорите и серверските 5xx неуспеси може да бидат привремени. Клиентот ги повторува тие услови најмногу двапати по првиот обид, почитува нумеричка вредност Retry-After до две секунди, а во спротивно користи ограничен експоненцијален backoff со jitter.
<?php
// src/Company/WebsiteCompanyClient.php
declare(strict_types=1);
namespace App\Company;
use App\Http\Transport;
use Closure;
use JsonException;
use RuntimeException;
final class WebsiteCompanyClient
{
private Closure $pause;
private Closure $log;
public function __construct(
private readonly Transport $transport,
private readonly string $token,
?Closure $pause = null,
?Closure $log = null
) {
$this->pause = $pause ?? static fn(int $ms) => usleep($ms * 1000);
$this->log = $log ?? static function (array $context): void {
error_log((string) json_encode($context, JSON_UNESCAPED_SLASHES));
};
}
public function extract(string $website): CompanyProfile
{
$correlationId = bin2hex(random_bytes(8));
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->transport->get(
'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract',
['token' => $this->token, 'website' => $website],
2000,
8000
);
} catch (RuntimeException $error) {
($this->log)([
'event' => 'company_enrichment_network_error',
'correlation_id' => $correlationId,
'attempt' => $attempt,
]);
if ($attempt === 3) {
throw new IntegrationFailure(
'network',
'The enrichment service could not be reached.'
);
}
($this->pause)($this->backoff($attempt));
continue;
}
$status = $response['status'];
if ($status >= 200 && $status < 300) {
try {
$payload = json_decode(
$response['body'],
true,
512,
JSON_THROW_ON_ERROR
);
} catch (JsonException) {
throw new IntegrationFailure(
'invalid_schema',
'The enrichment service returned invalid JSON.',
$status
);
}
if (!is_array($payload)) {
throw new IntegrationFailure(
'invalid_schema',
'The enrichment response is not an object.',
$status
);
}
return CompanyProfile::fromApi($payload);
}
($this->log)([
'event' => 'company_enrichment_http_error',
'correlation_id' => $correlationId,
'attempt' => $attempt,
'upstream_status' => $status,
]);
if ($status === 401 || $status === 403) {
throw new IntegrationFailure(
'authentication',
'The service token was rejected.',
$status
);
}
$retryable = $status === 429 || $status >= 500;
if (!$retryable) {
throw new IntegrationFailure(
'request_rejected',
'The enrichment request was rejected.',
$status
);
}
if ($attempt === 3) {
$kind = $status === 429 ? 'rate_limited' : 'upstream';
throw new IntegrationFailure(
$kind,
'The enrichment service is temporarily unavailable.',
$status
);
}
($this->pause)($this->delay($attempt, $response['headers']));
}
throw new IntegrationFailure('upstream', 'Enrichment failed.');
}
private function delay(int $attempt, array $headers): int
{
$retryAfter = $headers['retry-after'] ?? null;
if (is_string($retryAfter) && ctype_digit($retryAfter)) {
return min(2000, (int) $retryAfter * 1000);
}
return $this->backoff($attempt);
}
private function backoff(int $attempt): int
{
return min(2000, 200 * (2 ** ($attempt - 1)) + random_int(0, 100));
}
}
Дневниците содржат име на настан, локален ID за корелација, број на обид и статус. Тие намерно ги изоставуваат токенот, целосниот URL на барањето, телото на одговорот и поднесената веб-страница. Бидејќи автентикацијата користи параметар за пребарување, обратните проксија и системите за следење исто така мора да се конфигурираат да ги редигираат низите за пребарување.
Изложете ја CRM крајната точка за претпополнување
Контролерот прифаќа JSON како {"website":"https://example.com"}. Тој дозволува само HTTP и HTTPS URL-адреси со хост, а потоа ги преведува неуспесите на интеграцијата во стабилни состојби на ниво на апликација.
<?php
// public/index.php
declare(strict_types=1);
use App\Company\IntegrationFailure;
use App\Company\WebsiteCompanyClient;
use App\Http\CurlTransport;
$config = require dirname(__DIR__) . '/config/bootstrap.php';
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|| parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) !== '/lead-prefill') {
http_response_code(404);
echo '{"error":{"kind":"not_found"}}';
exit;
}
try {
$input = json_decode(
file_get_contents('php://input'),
true,
32,
JSON_THROW_ON_ERROR
);
$website = is_array($input) ? ($input['website'] ?? null) : null;
$valid = is_string($website)
? filter_var($website, FILTER_VALIDATE_URL)
: false;
$scheme = $valid !== false ? parse_url($website, PHP_URL_SCHEME) : null;
$host = $valid !== false ? parse_url($website, PHP_URL_HOST) : null;
if ($valid === false
|| !in_array($scheme, ['http', 'https'], true)
|| !is_string($host)
|| $host === '') {
http_response_code(422);
echo json_encode([
'error' => [
'kind' => 'validation',
'message' => 'Supply a complete HTTP or HTTPS company website.',
],
], JSON_THROW_ON_ERROR);
exit;
}
$client = new WebsiteCompanyClient(
new CurlTransport(),
$config['service_token']
);
echo json_encode([
'data' => $client->extract($website)->toArray(),
], JSON_THROW_ON_ERROR);
} catch (JsonException) {
http_response_code(400);
echo '{"error":{"kind":"invalid_json"}}';
} catch (IntegrationFailure $failure) {
$status = match ($failure->kind) {
'rate_limited', 'network' => 503,
default => 502,
};
http_response_code($status);
echo json_encode([
'error' => [
'kind' => $failure->kind,
'message' => $failure->getMessage(),
],
], JSON_THROW_ON_ERROR);
}
CRM може да ги постави вратените вредности во незачуван формулар за потенцијален клиент. Оставете ги уредливи: јавните веб-страници може да бидат нецелосни или застарени, а збогатувањето треба да му помага на продавачот наместо тивко да стане авторитативен извор на податоци.
Тестирајте без да правите надворешни барања
Лажен транспорт ги прави успехот, повторните обиди и неуспесите при автентикација детерминистички. Тестовите никогаш не смеат да користат активен токен или да трошат квота од планот.
<?php
// tests/WebsiteCompanyClientTest.php
declare(strict_types=1);
use App\Company\IntegrationFailure;
use App\Company\WebsiteCompanyClient;
use App\Http\Transport;
use PHPUnit\Framework\TestCase;
final class FakeTransport implements Transport
{
public int $calls = 0;
public function __construct(private array $responses) {}
public function get(
string $url,
array $query,
int $connectTimeoutMs,
int $timeoutMs
): array {
$this->calls++;
return array_shift($this->responses);
}
}
final class WebsiteCompanyClientTest extends TestCase
{
public function testMapsAProfile(): void
{
$transport = new FakeTransport([[
'status' => 200,
'headers' => [],
'body' => json_encode([
'company' => 'Example Ltd',
'contact' => 'Sales',
'email' => '[email protected]',
'phone' => '+1 555 0100',
'people' => [['name' => 'Alex']],
], JSON_THROW_ON_ERROR),
]]);
$client = new WebsiteCompanyClient(
$transport,
'test-token',
static fn(int $ms) => null,
static fn(array $context) => null
);
$profile = $client->extract('https://example.com');
self::assertSame('Example Ltd', $profile->company);
self::assertSame('[email protected]', $profile->email);
self::assertCount(1, $profile->people);
}
public function testRetriesAServiceFailureThenSucceeds(): void
{
$transport = new FakeTransport([
['status' => 503, 'headers' => [], 'body' => ''],
['status' => 200, 'headers' => [], 'body' => '{}'],
]);
$client = new WebsiteCompanyClient(
$transport,
'test-token',
static fn(int $ms) => null,
static fn(array $context) => null
);
$client->extract('https://example.com');
self::assertSame(2, $transport->calls);
}
public function testDoesNotRetryAuthenticationFailure(): void
{
$transport = new FakeTransport([[
'status' => 401,
'headers' => [],
'body' => '',
]]);
$client = new WebsiteCompanyClient(
$transport,
'test-token',
static fn(int $ms) => null,
static fn(array $context) => null
);
try {
$client->extract('https://example.com');
self::fail('Expected an IntegrationFailure');
} catch (IntegrationFailure $failure) {
self::assertSame('authentication', $failure->kind);
self::assertSame(1, $transport->calls);
}
}
}
vendor/bin/phpunit tests
php -S 127.0.0.1:8080 -t public
curl --fail-with-body \
-H 'Content-Type: application/json' \
-d '{"website":"https://example.com"}' \
http://127.0.0.1:8080/lead-prefill
Зацврстување за продукција и распоредување
Извршете ги тестовите пред да ги инсталирате продукциските зависности, потоа распоредете зад конфигуриран PHP-FPM веб-сервер со public како корен на документи:
vendor/bin/phpunit tests
composer install --no-dev --classmap-authoritative
php -r 'exit(extension_loaded("curl") ? 0 : 1);'
Поставете ги роковите за барања на апликацијата и проксито малку над осумсекундното временско ограничување на клиентот. Следете успех, неуспех при валидација, неуспех при автентикација, ограничување на стапката, неуспех во надворешниот систем, латентност и број на повторни обиди. Испратете предупредување за трајни неуспеси при автентикација, бидејќи тие најчесто укажуваат на истечен, повторно генериран или неправилно распореден токен.
Не кеширајте го збогатувањето неселективно. Ако повторените пребарувања се чести, кеширајте според нормализиран хост на веб-страницата за краток период одобрен од бизнисот и разгледајте дали деталите за контакт се лични податоци според вашата политика за задржување. Применете CRM авторизација и CSRF заштита таму каде што ги бара околната апликација.
Вообичаени начини на неуспех
- HTTP 401 или 403: потврдете ја активацијата и тековниот токен ограничен на услугата; не обидувајте се повторно автоматски.
- HTTP 429: достапниот капацитет на планот можеби е исцрпен или привремено ограничен; зачувајте го нацртот за потенцијалниот клиент и дозволете му на корисникот да се обиде повторно подоцна.
- Истекување на времето или 5xx одговори: обидете се повторно само во рамките на ограничената политика, а потоа вратете поправлива CRM грешка.
- Невалидна шема: задржете ги санираните метаподатоци за неуспехот и споредете го одговорот со официјалната документација; никогаш не присилувајте неочекувани низи во текстуални полиња.
- Празни полиња: третирајте ги како легитимно делумно збогатување, а не како неуспешно барање.
Конечна контролна листа за потврда
- Планот за услугата е активен, а тековниот токен доаѓа од конфигурација поддржана од променливи на околината.
- Барањето ја користи документираната GET крајна точка само со параметрите за пребарување
tokenиwebsite. - Пренасочувањата се оневозможени, временските ограничувања се ограничени и повторно се прават обиди само за привремени неуспеси.
- Податоците за компанија, контакт, е-пошта, телефон и лица се мапираат на API границата.
- Дневниците и тестовите не содржат вистински акредитиви или податоци од одговорот.
- CRM добива уредливи вредности за претпополнување и го зачувува нацртот на продавачот кога збогатувањето не успее.
- Успехот, грешките при автентикација, ограничувањето на стапката, латентноста и активноста на повторните обиди се видливи по распоредувањето.
Важниот резултат не е само помалку внесување со тастатура. Тоа е чиста граница меѓу надворешна услуга за збогатување и сопствениот модел на податоци на CRM. Со воспоставена таа граница, една веб-страница на компанија станува корисен нацрт за потенцијален клиент без привременото мрежно однесување, променливите јавни информации или чувствителните акредитиви да се претворат во скриен оперативен ризик.