انتقل إلى المحتوى

ناقل 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 كتابة الطلب. أما رمي استثناء فيرفض الجلب بالخطأ المرمِي.

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.

راجع أيضًا