Туториали

Native PHP 8.3: Enrich Quote Requests with Company Data Seamlessly

Native PHP 8.3: Беспрекорно збогатете ги барањата за понуди со податоци за компанијата

Формуларот за понуда треба да изгледа моментален, дури и кога корисната работа започнува откако клиентот ќе притисне Испрати. Фриленсер или мал развоен тим можеби ќе сака да знае која компанија бара понуда, но правењето надворешно барање за збогатување во рамките на HTTP животниот циклус на формуларот воведува избеглива латентност и уште една причина поднесувањето да не успее.

Овој проект ја решава таа тензија со мала, за продукција подготвена Native PHP 8.3 апликација. Јавната крајна точка го валидира и зачувува барањето за понуда, става задача за збогатување во ред во SQLite и веднаш враќа 202 Accepted. Одделен worker ја претвора поднесената јавна веб-страница во структурирани податоци за компанија и контакт преку Website to Company data API.

Резултатот е намерно скромна архитектура: PHP, PDO, native cURL и worker надгледуван од оперативниот систем. Нема framework или message broker што треба да се управува, но важните граници остануваат експлицитни и тестабилни.

Добијте пристап и копирајте го сервисниот токен

  1. Создадете сметка на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате.
  2. Отворете ја страницата на услугата Website to Company data.
  3. Изберете го достапниот Free, Plus или Pro план и завршете ја неговата активација.
  4. Отворете ја официјалната документација за услугата.
  5. Најдете го панелот Service token и копирајте го токенот со опсег на услугата што е прикажан таму.

Оваа услуга не е без токен. Секое барање мора да го испрати акредитивот преку параметарот за барање token={serviceToken}. Регенерирањето на сервисниот токен го поништува претходно активниот токен, затоа третирајте ја ротацијата како координирана промена при распоредување, а не како случајна акција во контролната табла.

Потврдете го точниот API повик

Интеграцијата користи GET https://ai.mihajlo.mk/api/website-to-company-data/v1/extract. Ги испраќа и token и website како параметри за барање. Пред да пишувате код за апликацијата, направете едно минимално барање од доверлив терминал:

read -sr -p "Service token: " SERVICE_TOKEN
curl --fail-with-body --silent --show-error --get \
  "https://ai.mihajlo.mk/api/website-to-company-data/v1/extract" \
  --data-urlencode "token=${SERVICE_TOKEN}" \
  --data-urlencode "website=https://example.com"
unset SERVICE_TOKEN

--data-urlencode спречува оштетување на низа за барање. Избегнувајте да го внесувате токенот директно во командата бидејќи историјата на shell, инспекцијата на процеси и копираниот излез од терминалот можат да го изложат.

Сега создадете .env.local надвор од јавниот директориум:

SERVICE_TOKEN="YOUR_SERVICE_TOKEN"
DATABASE_PATH="/var/lib/quote-enricher/quotes.sqlite"

Додајте ги .env.local, SQLite базата на податоци и нејзините придружни датотеки во .gitignore. При распоредување, претпочитајте вистински променливи на процесната околина обезбедени од сервисниот менаџер или складиштето за тајни. Локалната датотека е погодност за развој, а не нешто што треба да се commit-ира или да се постави под web root.

Архитектура и структура на проектот

Патеката на барањето и патеката на збогатувањето имаат различни одговорности:

  • HTTP крајната точка ја валидира понудата, ја внесува и создава задача во истата трансакција на базата на податоци.
  • Worker-от презема една подготвена задача, ја повикува надворешната услуга, го мапира нејзиниот одговор во доменски податоци и ја ажурира понудата.
  • Привремените неуспеси се враќаат во редот со ограничен експоненцијален backoff. Неуспесите при автентикација, барање и шема се означуваат како конечни.

Овој дизајн во стил на трансакциски outbox спречува зачувана понуда да ја изгуби соодветната задача. SQLite е соодветен за едно мало распоредување кога базата на податоци се наоѓа на трајно локално складиште и има само неколку workers. Повеќе јазли на апликацијата треба да користат заедничка трансакциска база на податоци и истата шема на условно преземање.

quote-enricher/
  bin/migrate.php
  bin/worker.php
  public/index.php
  src/CompanyData.php
  src/Http.php
  src/WebsiteCompanyClient.php
  tests/WebsiteCompanyClientTest.php
  bootstrap.php
  composer.json
  phpunit.xml
  .env.local

Извршната околина има потреба од PHP 8.3 со curl, pdo_sqlite и json. Composer се користи само за autoloading и PHPUnit:

{
  "require": {
    "php": "^8.3",
    "ext-curl": "*",
    "ext-json": "*",
    "ext-pdo": "*",
    "ext-pdo_sqlite": "*"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}
composer install
composer dump-autoload --optimize
php bin/migrate.php

Создадете ја базата на податоци и bootstrap за конфигурација

Bootstrap-от прво ја вчитува локалната датотека, а потоа им овозможува на вистинските променливи на околината да ја надминат. Исто така го конфигурира SQLite за истовремени читања и ограничено чекање на заклучување.

<?php
// bootstrap.php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

$local = [];
$file = __DIR__ . '/.env.local';
if (is_file($file)) {
    $local = parse_ini_file($file, false, INI_SCANNER_RAW);
    if ($local === false) {
        throw new RuntimeException('Cannot parse .env.local');
    }
}

$config = [
    'token' => getenv('SERVICE_TOKEN') ?: ($local['SERVICE_TOKEN'] ?? ''),
    'database' => getenv('DATABASE_PATH')
        ?: ($local['DATABASE_PATH'] ?? __DIR__ . '/var/quotes.sqlite'),
];

if ($config['token'] === '') {
    throw new RuntimeException('SERVICE_TOKEN is required');
}

$pdo = new PDO('sqlite:' . $config['database'], options: [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec('PRAGMA journal_mode=WAL');
$pdo->exec('PRAGMA busy_timeout=5000');

return [$config, $pdo];
<?php
// bin/migrate.php
declare(strict_types=1);

[, $pdo] = require dirname(__DIR__) . '/bootstrap.php';

$pdo->exec(<<<'SQL'
CREATE TABLE IF NOT EXISTS quote_requests (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL,
    website TEXT NOT NULL,
    brief TEXT NOT NULL,
    enrichment_status TEXT NOT NULL DEFAULT 'queued',
    company_data TEXT,
    created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS enrichment_jobs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    quote_id INTEGER NOT NULL UNIQUE,
    status TEXT NOT NULL DEFAULT 'queued',
    attempts INTEGER NOT NULL DEFAULT 0,
    available_at TEXT NOT NULL,
    last_error TEXT,
    FOREIGN KEY (quote_id) REFERENCES quote_requests(id)
);
CREATE INDEX IF NOT EXISTS enrichment_ready
    ON enrichment_jobs(status, available_at);
SQL);

echo "Database ready\n";

Изолирајте го HTTP и мапирајте го надворешниот одговор

Транспортот се грижи за механиката на поврзување; клиентот се грижи за API политиката. Одвојувањето на тие грижи овозможува детерминистички тестови без мрежни повици.

<?php
// src/Http.php
namespace App;

final readonly class HttpResponse
{
    public function __construct(
        public int $status,
        public string $body,
        public ?string $networkError = null,
    ) {}
}

interface Transport
{
    public function get(string $url, array $query): HttpResponse;
}

final class CurlTransport implements Transport
{
    public function get(string $url, array $query): HttpResponse
    {
        $requestUrl = $url . '?' . http_build_query(
            $query, '', '&', PHP_QUERY_RFC3986
        );
        $handle = curl_init($requestUrl);
        if ($handle === false) {
            return new HttpResponse(0, '', 'initialization_failed');
        }

        curl_setopt_array($handle, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_CONNECTTIMEOUT_MS => 1500,
            CURLOPT_TIMEOUT_MS => 5000,
            CURLOPT_USERAGENT => 'quote-enricher/1.0',
        ]);

        $body = curl_exec($handle);
        $error = $body === false ? curl_error($handle) : null;
        $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
        curl_close($handle);

        return new HttpResponse($status, $body === false ? '' : $body, $error);
    }
}

Доставениот договор ги именува company, contact, email, phone и people, но не оправдува претпоставка дека секоја вредност секогаш ќе биде пополнета или скаларна. Границата подолу ги нормализира отсутните вредности и отфрла некомпатибилни облици, наместо да дозволи произволни податоци од одговорот да се распространат низ апликацијата.

<?php
// src/CompanyData.php
namespace App;

use JsonSerializable;
use UnexpectedValueException;

final readonly class CompanyData implements JsonSerializable
{
    public function __construct(
        public array $company,
        public array $contact,
        public array $email,
        public array $phone,
        public array $people,
    ) {}

    public static function fromPayload(array $payload): self
    {
        return new self(
            self::object($payload['company'] ?? null, 'company'),
            self::object($payload['contact'] ?? null, 'contact'),
            self::strings($payload['email'] ?? null, 'email'),
            self::strings($payload['phone'] ?? null, 'phone'),
            self::people($payload['people'] ?? null),
        );
    }

    private static function object(mixed $value, string $field): array
    {
        if ($value === null) return [];
        if (is_string($value)) return ['value' => $value];
        if (is_array($value)) return $value;
        throw new UnexpectedValueException("Invalid {$field} data");
    }

    private static function strings(mixed $value, string $field): array
    {
        if ($value === null || $value === '') return [];
        if (is_string($value)) return [$value];
        if (is_array($value)) {
            foreach ($value as $item) {
                if (!is_string($item)) {
                    throw new UnexpectedValueException("Invalid {$field} data");
                }
            }
            return array_values($value);
        }
        throw new UnexpectedValueException("Invalid {$field} data");
    }

    private static function people(mixed $value): array
    {
        if ($value === null) return [];
        if (!is_array($value)) {
            throw new UnexpectedValueException('Invalid people data');
        }

        return array_values(array_map(
            static function (mixed $person): array {
                if (is_string($person)) return ['name' => $person];
                if (is_array($person)) return $person;
                throw new UnexpectedValueException('Invalid person data');
            },
            $value
        ));
    }

    public function jsonSerialize(): array
    {
        return get_object_vars($this);
    }
}

Применете ограничени повторни обиди и структурирани неуспеси

Мрежните неуспеси, одговорите 429 и серверските грешки може да се привремени. Автентикацијата и другите грешки на клиентот генерално не се. Клиентот прави најмногу три кратки обиди; трајниот ред на задачи се справува со подолгите одложувања.

<?php
// src/WebsiteCompanyClient.php
namespace App;

use Closure;
use JsonException;
use RuntimeException;
use Throwable;

final class EnrichmentFailure extends RuntimeException
{
    public function __construct(
        public readonly string $kind,
        public readonly bool $retryable,
    ) {
        parent::__construct($kind);
    }
}

final class WebsiteCompanyClient
{
    private const ENDPOINT =
        'https://ai.mihajlo.mk/api/website-to-company-data/v1/extract';

    private Closure $sleep;

    public function __construct(
        private readonly Transport $transport,
        private readonly string $token,
        ?callable $sleep = null,
    ) {
        $this->sleep = $sleep === null
            ? static fn(int $milliseconds) => usleep($milliseconds * 1000)
            : Closure::fromCallable($sleep);
    }

    public function extract(string $website): CompanyData
    {
        foreach ([200, 600, 0] as $attempt => $delay) {
            $response = $this->transport->get(self::ENDPOINT, [
                'token' => $this->token,
                'website' => $website,
            ]);

            $temporary = $response->networkError !== null
                || $response->status === 429
                || $response->status >= 500;

            if ($temporary && $attempt < 2) {
                ($this->sleep)($delay);
                continue;
            }
            if ($temporary) {
                throw new EnrichmentFailure('temporary_api_failure', true);
            }
            if (in_array($response->status, [401, 403], true)) {
                throw new EnrichmentFailure('authentication_failure', false);
            }
            if ($response->status < 200 || $response->status >= 300) {
                throw new EnrichmentFailure('request_rejected', false);
            }

            try {
                $payload = json_decode(
                    $response->body, true, flags: JSON_THROW_ON_ERROR
                );
                if (!is_array($payload)) {
                    throw new JsonException('Expected an object');
                }
                return CompanyData::fromPayload($payload);
            } catch (Throwable) {
                throw new EnrichmentFailure('invalid_response_schema', false);
            }
        }

        throw new EnrichmentFailure('retry_exhausted', true);
    }
}

Прифатете ја понудата без да чекате збогатување

Јавната крајна точка ги валидира големината и обликот пред да отвори трансакција. Таа не зачувува оддалечени податоци за време на барањето, па прекин во API не може да спречи понудата да биде прифатена.

<?php
// public/index.php
declare(strict_types=1);

[, $pdo] = require dirname(__DIR__) . '/bootstrap.php';
header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    header('Allow: POST');
    echo json_encode(['error' => 'method_not_allowed']);
    exit;
}

$name = trim((string) ($_POST['name'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$website = trim((string) ($_POST['website'] ?? ''));
$brief = trim((string) ($_POST['brief'] ?? ''));
$scheme = strtolower((string) parse_url($website, PHP_URL_SCHEME));

$valid = $name !== ''
    && strlen($name) <= 120
    && filter_var($email, FILTER_VALIDATE_EMAIL)
    && strlen($email) <= 254
    && filter_var($website, FILTER_VALIDATE_URL)
    && in_array($scheme, ['http', 'https'], true)
    && parse_url($website, PHP_URL_HOST) !== null
    && $brief !== ''
    && strlen($brief) <= 5000;

if (!$valid) {
    http_response_code(422);
    echo json_encode(['error' => 'invalid_quote']);
    exit;
}

$now = gmdate('Y-m-d\TH:i:s\Z');

try {
    $pdo->beginTransaction();

    $statement = $pdo->prepare(
        'INSERT INTO quote_requests
         (name, email, website, brief, created_at)
         VALUES (?, ?, ?, ?, ?)'
    );
    $statement->execute([$name, $email, $website, $brief, $now]);
    $quoteId = (int) $pdo->lastInsertId();

    $job = $pdo->prepare(
        'INSERT INTO enrichment_jobs (quote_id, available_at)
         VALUES (?, ?)'
    );
    $job->execute([$quoteId, $now]);
    $pdo->commit();

    http_response_code(202);
    echo json_encode(['quote_id' => $quoteId, 'status' => 'accepted']);
} catch (Throwable $exception) {
    if ($pdo->inTransaction()) $pdo->rollBack();
    error_log(json_encode(['event' => 'quote_store_failed']));
    http_response_code(503);
    echo json_encode(['error' => 'temporarily_unavailable']);
}

Распоредување насочено кон прелистувач исто така треба да примени CSRF заштита, ограничување на стапката, намерна CORS политика и контроли за ботови соодветни за формуларот. Ако се важни дупликат-поднесувањата, прифатете idempotency key и спроведете го со единствено ограничување во базата на податоци.

Извршете го worker-от за збогатување

Worker-от презема задача со условно ажурирање, така што два процеси не можат намерно да го извршат истиот ред во редот. Успехот го зачувува мапираниот JSON. Привремениот неуспех закажува нов обид; конечниот неуспех зачувува структуриран статус за поддршка и известување.

<?php
// bin/worker.php
declare(strict_types=1);

use App\CurlTransport;
use App\EnrichmentFailure;
use App\WebsiteCompanyClient;

[$config, $pdo] = require dirname(__DIR__) . '/bootstrap.php';
$now = gmdate('Y-m-d\TH:i:s\Z');

$pdo->beginTransaction();
$select = $pdo->prepare(
    "SELECT j.*, q.website
     FROM enrichment_jobs j
     JOIN quote_requests q ON q.id = j.quote_id
     WHERE j.status = 'queued' AND j.available_at <= ?
     ORDER BY j.id LIMIT 1"
);
$select->execute([$now]);
$job = $select->fetch();

if (!$job) {
    $pdo->commit();
    exit(0);
}

$claim = $pdo->prepare(
    "UPDATE enrichment_jobs SET status = 'running'
     WHERE id = ? AND status = 'queued'"
);
$claim->execute([$job['id']]);
if ($claim->rowCount() !== 1) {
    $pdo->rollBack();
    exit(0);
}
$pdo->commit();

$client = new WebsiteCompanyClient(
    new CurlTransport(),
    $config['token']
);

try {
    $data = $client->extract($job['website']);
    $pdo->beginTransaction();
    $update = $pdo->prepare(
        "UPDATE quote_requests
         SET company_data = ?, enrichment_status = 'complete'
         WHERE id = ?"
    );
    $update->execute([
        json_encode($data, JSON_THROW_ON_ERROR),
        $job['quote_id'],
    ]);
    $pdo->prepare('DELETE FROM enrichment_jobs WHERE id = ?')
        ->execute([$job['id']]);
    $pdo->commit();

    error_log(json_encode([
        'event' => 'enrichment_complete',
        'quote_id' => (int) $job['quote_id'],
    ]));
} catch (EnrichmentFailure $failure) {
    $attempts = (int) $job['attempts'] + 1;
    $retry = $failure->retryable && $attempts < 5;
    $delay = min(3600, 60 * (2 ** $attempts));
    $available = gmdate('Y-m-d\TH:i:s\Z', time() + $delay);

    $pdo->prepare(
        'UPDATE enrichment_jobs
         SET status = ?, attempts = ?, available_at = ?, last_error = ?
         WHERE id = ?'
    )->execute([
        $retry ? 'queued' : 'failed',
        $attempts,
        $available,
        $failure->kind,
        $job['id'],
    ]);

    if (!$retry) {
        $pdo->prepare(
            "UPDATE quote_requests SET enrichment_status = 'failed'
             WHERE id = ?"
        )->execute([$job['quote_id']]);
    }

    error_log(json_encode([
        'event' => 'enrichment_failed',
        'quote_id' => (int) $job['quote_id'],
        'kind' => $failure->kind,
        'retrying' => $retry,
    ]));
    exit($retry ? 75 : 1);
}

Логовите намерно ги изоставуваат токенот, веб-страницата, е-поштата, телото на одговорот и извлечените лица. Наместо тоа, бележете идентификатори и структурирани видови неуспех. Бидејќи автентикацијата користи параметар за барање, никогаш не го логирајте конструираниот URL на барањето.

Извршувајте го worker-от постојано преку systemd timer, cron или надгледувана јамка. Продукцискиот supervisor треба да стартува повеќе процеси само по валидација на SQLite конкуренцијата при очекуваното оптоварување. Исто така треба да обновува задачи оставени во running по паднат процес, користејќи временска ознака на преземање и конзервативен праг за застарени задачи.

Тестирајте ја границата без повикување на услугата

Детерминистички лажен транспорт ги проверува политиката на мапирање и повторни обиди. Ниту една тест-поставка не содржи вистински акредитив.

<?php
// tests/WebsiteCompanyClientTest.php
declare(strict_types=1);

use App\EnrichmentFailure;
use App\HttpResponse;
use App\Transport;
use App\WebsiteCompanyClient;
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): HttpResponse
    {
        $this->calls++;
        return array_shift($this->responses);
    }
}

final class WebsiteCompanyClientTest extends TestCase
{
    public function testMapsAValidResponse(): void
    {
        $fake = new FakeTransport([new HttpResponse(200, json_encode([
            'company' => ['name' => 'Example Ltd'],
            'contact' => ['city' => 'Skopje'],
            'email' => '[email protected]',
            'phone' => ['+389000000'],
            'people' => [['name' => 'Alex', 'role' => 'Owner']],
        ]))]);

        $data = (new WebsiteCompanyClient(
            $fake, 'test-token', static fn(int $ms) => null
        ))->extract('https://example.com');

        self::assertSame(['[email protected]'], $data->email);
        self::assertSame('Example Ltd', $data->company['name']);
        self::assertSame(1, $fake->calls);
    }

    public function testRetriesRateLimitThenSucceeds(): void
    {
        $fake = new FakeTransport([
            new HttpResponse(429, '{}'),
            new HttpResponse(200, json_encode([
                'company' => [], 'contact' => [],
                'email' => [], 'phone' => [], 'people' => [],
            ])),
        ]);

        (new WebsiteCompanyClient(
            $fake, 'test-token', static fn(int $ms) => null
        ))->extract('https://example.com');

        self::assertSame(2, $fake->calls);
    }

    public function testDoesNotRetryAuthenticationFailure(): void
    {
        $fake = new FakeTransport([new HttpResponse(401, '{}')]);

        try {
            (new WebsiteCompanyClient(
                $fake, 'bad-token', static fn(int $ms) => null
            ))->extract('https://example.com');
            self::fail('Expected EnrichmentFailure');
        } catch (EnrichmentFailure $failure) {
            self::assertSame('authentication_failure', $failure->kind);
            self::assertFalse($failure->retryable);
            self::assertSame(1, $fake->calls);
        }
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
  <testsuites>
    <testsuite name="unit">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>

Извршете го пакетот со vendor/bin/phpunit. Додајте тестови на крајната точка врз привремена SQLite датотека пред да го проширите однесувањето за валидација или идемпотентност.

Распоредете, набљудувајте и дијагностицирајте

Опслужувајте го само public/ преку PHP-FPM или развојниот сервер. Осигурете се дека веб-процесот и worker-от имаат пристап до директориумот на базата на податоци, додека други системски корисници не можат да го читаат. Чувајте ја базата на податоци на трајно складиште, доследно правете резервни копии и не поставувајте активна SQLite база на податоци на несоодветен мрежен датотечен систем.

Следете го бројот на задачи во ред, активни, неуспешни и невообичаено стари задачи. Испраќајте посебно известување за authentication_failure, бидејќи тоа често укажува на недостасувачка променлива на околината или регенериран токен чија замена не стигнала до секој worker. Постојаниот temporary_api_failure може да претставува проблем со поврзувањето, грешки на услугата, притисок од квота или ограничување на стапката; редот треба да го апсорбира без да влијае врз прифаќањето на понудите.

Вообичаените обрасци на неуспех се конкретни:

  • Непосреден 401 или 403: проверете го токенот со опсег на услугата и рестартирајте ги workers по ажурирање на околината. Не правете слепи повторни обиди.
  • Повторени одговори 429: намалете ја паралелноста на worker-ите, задржете траен backoff и проверете го капацитетот на планот.
  • Невалидна шема на одговор: задржете ја понудата, означете го збогатувањето како неуспешно и споредете го граничниот мапер со официјалната документација пред да го менувате.
  • Базата на податоци е заклучена: скратете ги трансакциите, осигурете се дека мрежните повици никогаш не се случуваат во нив и намалете ги истовремените SQLite запишувачи.
  • Задачите остануваат активни: додајте временска ознака на преземање и политика за обновување застарени задачи пред да се потпрете на автоматско обновување по пад.

Конечна листа за проверка

  • Процесот за регистрација, активација на план, документација и сервисен токен е завршен.
  • Токенот постои само во конфигурација поткрепена со околина и отсуствува од изворниот код, поставките, логовите и историјата на команди.
  • Валидно поднесување на формуларот враќа 202 пред да започне какво било барање за збогатување.
  • Понудата и задачата се создаваат атомски.
  • Worker-от ја повикува точната GET крајна точка со параметри за барање token и website.
  • Податоците за компанија, контакт, е-пошта, телефон и лица се валидираат и мапираат на API границата.
  • Временските ограничувања се ограничени, привремените неуспеси користат backoff, а неуспесите при автентикација или валидација не се повторуваат слепо.
  • PHPUnit поминува со детерминистичкиот лажен транспорт.
  • Продукцискиот мониторинг може да прави разлика меѓу доцнење во редот, ограничување на стапката, неуспех при автентикација и неуспех на шема.

Важната карактеристика не е само тоа што веб-страница станува податоци за компанија. Таа е дека понудата останува доверлива кога збогатувањето е бавно, недостапно, ограничено по стапка или неисправно форматирано. Брзото прифаќање, трајната работа, строгата граница на апликацијата и воздржаното логирање претвораат корисен API повик во продукциска интеграција на која може да ѝ се верува.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.