ناقل Fetch
تمر fetch() الافتراضية في Phasis عبر PHP وext-curl — أي CURLOPT_FOLLOWLOCATION=false (تعمل حلقة التحويل المحددة في المواصفات ضمن بيئة Phasis) مع تحقق CURLOPT_PROGRESSFUNCTION من AbortSignal. وهذا مناسب لمعظم حالات التضمين.
أما سائر الحالات — الوكلاء وDNS المخصصة وعميل HTTP الموجود في تطبيق المضيف وعزل الشبكة والمحاكاة في الاختبارات — فالناقل قابل للتبديل.
الواجهة
public function setFetchTransport(callable $transport): void
تتلقى الدالة القابلة للاستدعاء لديك:
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. تحقّق منها أثناء عمليات النقل الطويلة.
شكل القيمة المعادة:
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
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
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),
];
});
منع الكل
$engine->setFetchTransport(function () {
throw new \Phasis\BuiltIn\Fetch\TransportException(
'network access disabled in this sandbox',
'network-error',
);
});
يؤدي هذا إلى رفض كل استدعاء fetch() بخطأ TypeError. وهو مفيد لعزل JS الذي يقدمه المستخدم.
محاكاة للاختبارات
$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، أو قوائم السماح، أو ربط بيانات الاعتماد بكل مستأجر.
public function setFetchPolicy(callable $hook): void
توقيع الخطاف: function (JsObject $request): ?JsObject. تسمح إعادة null بالطلب كما هو. وتعيد Request كتابة الطلب. أما رمي استثناء فيرفض الجلب بالخطأ المرمِي.
$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. |
راجع أيضًا
- مرجع API — خطافات Fetch — مرجع موجز للتوقيعات.
- واجهات Web API — المجموعة الكاملة لواجهات Fetch Pack.
- حاوية ملفات تعريف الارتباط — الاحتفاظ بملفات تعريف الارتباط بين عمليات الجلب باستخدام حاوية PHP أو كائن حاوية JS.