Перейти до вмісту

Транспорт Fetch

За замовчуванням fetch() у Phasis використовує PHP і ext-curl — CURLOPT_FOLLOWLOCATION=false (цикл перенаправлень за специфікацією працює на боці Phasis), а CURLOPT_PROGRESSFUNCTION перевіряє AbortSignal. Для більшості варіантів вбудовування цього достатньо.

Для інших потреб — проксі, власний DNS, наявний клієнт HTTP у застосунку хоста, ізоляція мережі чи імітація в тестах — транспорт можна замінити.

Інтерфейс

PHP
public function setFetchTransport(callable $transport): void

Ваша функція отримає:

PHP
function (array $request, ?\Phasis\Value\JsObject $signal): array

$request — це асоціативний масив:

Ключ Тип Примітки
method string Уже у верхньому регістрі (GET, POST, …)
url string Абсолютний URL після парсера URL і ланцюжка перенаправлень
headers list<list<string>> Параметри [name, value]; регістр нормалізовано згідно зі специфікацією; значення з кількома елементами подано окремими парами (наприклад, Set-Cookie)
body string Необроблені байти. Порожній рядок для методів без тіла.
redirect string "follow" / "error" / "manual" (ваш транспорт не повинен виконувати перенаправлення — цикл працює в Phasis)
timeout int Бюджет реального часу в мілісекундах. 0 = без обмеження.
credentials string "omit" / "same-origin" / "include" — лише інформація; файли cookie підключаються окремо через setCookieJar().

$signal — це null, якщо для запиту не передали сигнал, інакше — JsObject, що надає aborted і reason. Перевіряйте його під час тривалих передавань.

Формат поверненого значення:

PHP
return [
    'status'     => 200,                          // int
    'statusText' => 'OK',                         // string
    'headers'    => [['content-type', 'text/plain'], ['x-custom', '1']],
    'body'       => '...response bytes...',       // string
];

Якщо транспорт викидає помилку, Phasis перехоплює її та перетворює:

  • Phasis\BuiltIn\Fetch\TransportException із $e->kind === "aborted" → fetch відхиляється з AbortError DOMException.
  • kind === "timeout" → TypeError із повідомленням.
  • kind === "network-error" (за замовчуванням) → TypeError.
  • Будь-яке інше значення → TypeError із повідомленням про виняток.

Приклади з поясненнями

Guzzle

PHP
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;

$client = new Client(['allow_redirects' => false]);

$engine->setFetchTransport(function (array $req, $signal) use ($client) {
    try {
        $r = $client->request($req['method'], $req['url'], [
            'headers'     => collect($req['headers'])->groupBy(0)->map->pluck(1)->all(),
            'body'        => $req['body'],
            'timeout'     => max(1, $req['timeout'] / 1000.0),
            'http_errors' => false,
        ]);
    } catch (GuzzleException $e) {
        throw new \Phasis\BuiltIn\Fetch\TransportException($e->getMessage(), 'network-error');
    }
    $headers = [];
    foreach ($r->getHeaders() as $name => $values) {
        foreach ($values as $v) {
            $headers[] = [$name, $v];
        }
    }
    return [
        'status'     => $r->getStatusCode(),
        'statusText' => $r->getReasonPhrase(),
        'headers'    => $headers,
        'body'       => (string) $r->getBody(),
    ];
});

Symfony HttpClient

PHP
use Symfony\Component\HttpClient\HttpClient;

$client = HttpClient::create(['max_redirects' => 0]);

$engine->setFetchTransport(function (array $req, $signal) use ($client) {
    $headers = [];
    foreach ($req['headers'] as [$name, $value]) {
        $headers[$name][] = $value;
    }
    $r = $client->request($req['method'], $req['url'], [
        'headers'      => $headers,
        'body'         => $req['body'],
        'timeout'      => $req['timeout'] / 1000.0,
        'max_redirects'=> 0,
    ]);
    return [
        'status'     => $r->getStatusCode(),
        'statusText' => '',
        'headers'    => collect($r->getHeaders(false))
            ->flatMap(fn ($values, $name) => array_map(fn ($v) => [$name, $v], $values))
            ->values()->all(),
        'body'       => $r->getContent(false),
    ];
});

Заборонити все

PHP
$engine->setFetchTransport(function () {
    throw new \Phasis\BuiltIn\Fetch\TransportException(
        'network access disabled in this sandbox',
        'network-error',
    );
});

У результаті кожен виклик fetch() буде відхилено з TypeError. Зручно для ізоляції коду JS, наданого користувачем.

Імітація для тестів

PHP
$canned = [
    'https://api.example.com/users' => ['status' => 200, 'body' => '{"users":[]}'],
    'https://api.example.com/404'   => ['status' => 404, 'body' => 'not found'],
];

$engine->setFetchTransport(function (array $req) use ($canned) {
    $r = $canned[$req['url']] ?? ['status' => 500, 'body' => 'no mock'];
    return [
        'status'     => $r['status'],
        'statusText' => $r['status'] === 200 ? 'OK' : 'ERR',
        'headers'    => [['content-type', 'application/json']],
        'body'       => $r['body'],
    ];
});

Хук політики

Транспорт виконує запит, а хук політики вирішує, чи запускати його взагалі. Використовуйте його для контролю лише HTTPS, додавання заголовків, переписування URL, списків дозволених адрес або прив’язування облікових даних до орендаря.

PHP
public function setFetchPolicy(callable $hook): void

Сигнатура хука: function (JsObject $request): ?JsObject. Повернення null дозволяє запит без змін. Повернення Request переписує його. Викидання помилки відхиляє fetch із цією помилкою.

PHP
$allowedHosts = ['api.example.com', 'cdn.example.com'];

$engine->setFetchPolicy(function ($req) use ($allowedHosts) {
    $url = parse_url($req->get('url')->value);
    if (!in_array($url['host'] ?? '', $allowedHosts, true)) {
        throw new \RuntimeException("host not allowed: " . ($url['host'] ?? ''));
    }
    // Inject internal-only auth header
    $req->get('headers')->call('set', 'X-Internal-Token', $secret);
    return null;
});

Хук політики виконується до транспорту, тож обійти його не зможе навіть помилковий або ненадійний транспорт.

Зведення поведінки за замовчуванням

Параметр Значення за замовчуванням
Транспорт CurlTransport (PHP ext-curl)
Перенаправлення За специфікацією виконується до 20 переходів. redirect: "manual" повертає Response перенаправлення з type: "opaqueredirect".
Тайм-аут 30 s реального часу, 10 s на з’єднання
Перевірка TLS CURLOPT_SSL_VERIFYPEER = true (не можна вимкнути; змініть транспорт, щоб перевизначити)
User-Agent Phasis/0.1 додається, лише якщо його не задано
Файли cookie Не зберігаються. Щоб увімкнути їх, використовуйте setCookieJar().
Хук політики Відсутній. Усі запити дозволено, якщо їх явно не заборонено.
AbortSignal Враховується під час передавання через опитування curl у зворотному виклику прогресу.

Див. також