Към съдържанието

Транспорт за 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" — само информативни; бисквитките се свързват отделно чрез 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" връща отговора за пренасочване с type: "opaqueredirect".
Изтичане на времето 30 s реално време, 10 s за свързване
Проверка на TLS CURLOPT_SSL_VERIFYPEER = true (без изключване — сменете транспорта, за да го промените)
User-Agent Phasis/0.1 се добавя само ако не е зададен
Бисквитки Не се съхраняват. Използвайте setCookieJar(), за да ги включите.
Управляваща функция за политиката Няма. Всички заявки са разрешени, освен ако не бъдат изрично забранени.
AbortSignal Проверява се по време на прехвърлянето чрез функцията за напредъка на curl.

Вижте също