استهلاك خدمة خارجية
حين يحتاج خادمك بيانات من خادم آخر. ⏱ نحو 30 دقيقة.
- تشرح دور خادمك عميلًا لخدمة خارجية، ولماذا يبقى المفتاح على الخادم.
- ترسل طلبات
HTTPمنPHPبـcURLمع مهلة. - تتعامل مع أعطال الخدمة وأشكال ردودها، وترد بـ 502 عند تعطلها.
- تخزّن الردود مؤقتًا وتحفظ المفاتيح السرية خارج الكود.
١الخادم عميلًا لخادم آخر
كثير من الوظائف لا تبنيها بنفسك، بل تطلبها من خدمة خارجية عبر API: أسعار العملات، والطقس، وبوابات الدفع، وإرسال الرسائل النصية، والخرائط. هنا يصير خادمك عميلًا: يرسل طلب HTTP إلى خادم آخر، ويستقبل JSON، ثم يستخدمه في رده على المستخدم.
| الخطوة | من؟ |
|---|---|
| 1. المتصفح يطلب صفحة التحويل | المستخدم ← خادمك |
| 2. خادمك يطلب أسعار العملات | خادمك ← خادم الخدمة |
| 3. الخدمة ترد بـ JSON | خادم الخدمة ← خادمك |
| 4. خادمك يحسب ويرد بصفحة | خادمك ← المستخدم |
لماذا لا يطلب المتصفح الخدمة مباشرة؟ لأن أغلب الخدمات تحتاج مفتاحًا سريًّا API key، ومفتاح في المتصفح يقرؤه أي أحد. فالمفتاح يبقى على خادمك فقط.
٢الطلب الحقيقي بـ cURL
في PHP مكتبة cURL لإرسال طلبات HTTP (مفعّلة في XAMPP). هذه دالة صغيرة نعيد استخدامها، تعيد رمز الحالة والجسم:
<?php// http.php: النسخة الحقيقية (على XAMPP أو الخادم المنشور)function http_get(string $url, array $headers = []): array{ $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, // أعد الجسم بدل طباعته CURLOPT_TIMEOUT => 5, // لا تنتظر أكثر من 5 ثوانٍ CURLOPT_HTTPHEADER => $headers, // مثل: Authorization: Bearer <المفتاح> CURLOPT_USERAGENT => 'tatbeeqha-course', ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($body === false) { return [0, $error]; // فشل الاتصال نفسه } return [$status, $body];}
٣في المختبر: خدمة محاكاة
المختبر يعمل داخل متصفحك بلا اتصال بالإنترنت، فلن يصل cURL لأي خادم. لذلك نستخدم نسخة من http_get بالتوقيع نفسه تقرأ الردود من ملفات محلية. وهذه فكرة يستخدمها المحترفون في الاختبار: كود التطبيق لا يعرف أن الخدمة محاكاة، ولا يتغيّر حين تستبدلها بالحقيقية:
<?phprequire __DIR__ . '/http.php'; $amount = (float) ($_GET['amount'] ?? 100);$to = strtoupper($_GET['to'] ?? 'USD'); [$status, $body] = http_get('https://api.example.com/rates?base=SAR');if ($status !== 200) { http_response_code(502); exit('خدمة الأسعار غير متاحة الآن');}$data = json_decode($body, true);$rate = $data['rates'][$to] ?? null;if ($rate === null) { http_response_code(422); exit("العملة $to غير مدعومة");}echo "$amount ريال = " . round($amount * $rate, 2) . " $to (أسعار {$data['date']})";
<?php// http.php: نسخة المختبر من http_get// المختبر بلا إنترنت، فنحاكي الخدمات بملفات JSON في مجلد mock// وعلى XAMPP استبدل هذا الملف بنسخة cURL (في الدرس) دون تغيير أي شيء آخرfunction http_get(string $url): array{ $name = trim(preg_replace('/[^a-z0-9]+/i', '_', $url), '_'); // اسم ملف من الرابط $file = __DIR__ . '/mock/' . $name . '.json'; if (!file_exists($file)) { return [404, '{"error":"not found"}']; } return [200, file_get_contents($file)];}
{"base":"SAR","date":"2026-10-01","rates":{"USD":0.2666,"EUR":0.2451,"EGP":12.98,"AED":0.9793}}الحالة الثالثة: الخدمة ردت بنجاح لكن لا يوجد فيها الين، فرددنا 422. وإن تعطلت الخدمة نفسها نرد 502 Bad Gateway: «الخادم الذي خلفي فشل».
٤حين تفشل الخدمة
الخدمة الخارجية ليست تحت سيطرتك: قد تبطئ، أو تتعطل، أو تغيّر شكل ردها، أو ترفض لأنك تجاوزت حد الطلبات. الكود الجيد يتوقع ذلك:
| المشكلة | العلاج |
|---|---|
| الخدمة بطيئة جدًّا | مهلة قصيرة CURLOPT_TIMEOUT، فلا يعلق مستخدمك. |
الخدمة متعطلة (5xx) أو لا اتصال | رسالة لطيفة ورمز 502، أو آخر نسخة محفوظة. |
تجاوزت حد الطلبات (429) | خزّن الرد مؤقتًا ولا تطلب كل مرة. |
| الرد بشكل غير متوقع | تحقق من وجود المفاتيح بـ ?? قبل استخدامها. |
والتخزين المؤقت Caching يحل أكثر هذه المشكلات معًا: أسعار العملات لا تتغير كل ثانية، فاحفظ الرد في ملف، واستخدمه ساعة كاملة قبل طلب جديد:
<?phpfunction cached_get(string $url, int $ttl = 3600): array{ $file = sys_get_temp_dir() . '/cache-' . md5($url) . '.json'; if (file_exists($file) && time() - filemtime($file) < $ttl) { return [200, file_get_contents($file)]; // من الذاكرة المؤقتة } [$status, $body] = http_get($url); if ($status === 200) { file_put_contents($file, $body); // احفظ للمرات القادمة } elseif (file_exists($file)) { return [200, file_get_contents($file)]; // الخدمة تعطلت: استخدم آخر نسخة } return [$status, $body];}
٥أين تحفظ المفتاح السري؟
أغلب الخدمات تعطيك مفتاحًا API key يُرسل مع كل طلب. والقاعدة: المفتاح لا يُكتب في الكود الذي قد تشاركه أو ترفعه إلى GitHub، بل في متغير بيئة أو ملف إعداد خارج المجلد العام:
<?php// inc/config.php (خارج public، ولا يُرفع إلى GitHub)return [ 'rates_key' => getenv('RATES_API_KEY') ?: 'ضع-المفتاح-في-البيئة',]; // في الاستخدام:$config = require __DIR__ . '/../inc/config.php';[$status, $body] = http_get($url, ['Authorization: Bearer ' . $config['rates_key']]);
توقّع وتحقّق
لماذا يطلب خادمك الخدمة بدل المتصفح مباشرة؟
الرمز المناسب حين تتعطل الخدمة التي يعتمد عليها خادمك:
Bad Gateway.ما فائدة CURLOPT_TIMEOUT؟
التخزين المؤقت للرد يفيد في:
Caching.الخلاصة
- خادمك يطلب الخدمة، ويستقبل
JSON، ثم يرد على المستخدم. cURLمعCURLOPT_RETURNTRANSFERوCURLOPT_TIMEOUT، وافحص رمز الحالة دائمًا.- دالة
http_getواحدة يسهل استبدالها بمحاكاة للاختبار. - 502 حين تتعطل الخدمة، و422 حين يطلب المستخدم ما لا تدعمه.
- التخزين المؤقت يقلل الطلبات، والمفتاح في متغير بيئة لا في الكود.
تحدٍّ برمجي: حالة الطقس للمعهد
متوسطاكتب weather.php: تطلب حالة الطقس لمدينة من خدمة (محاكاة عبر http_get)، وتعرضها. الملفات http.php وردود الخدمة جاهزة.
- رابط الخدمة:
https://api.example.com/weather?city=متبوعًا باسم المدينة من$_GET['city'](الافتراضي madinah). - الرد JSON فيه
tempوdesc. اعرض:<p>madinah: 31° · مشمس</p> - إن كان رمز الحالة من الخدمة غير 200: رد بـ 502 والنص
خدمة الطقس غير متاحة. - استخدم http_get ولا تقرأ ملفات mock مباشرة.
👀 اعرض الاستجابة المطلوبة
GET /weather.phpGET /weather.php?city=abhaGET /weather.php?city=tabuk
- الكود يعمل بلا أخطاء أو تحذيرات
- بلا مدينة ←
madinah: 31° · مشمس - abha ←
abha: 19° · غائم جزئيًّا - مدينة لا تعرفها الخدمة ← 502
- 502 مع النص المطلوب
- تستخدم http_get
- لا تقرأ ملفات mock مباشرة
💡 تلميح 1
[$status, $body] = http_get('https://api.example.com/weather?city=' . $city);💡 تلميح 2
body, true) واقرأ temp و desc.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
<?phprequire __DIR__ . '/http.php'; $city = $_GET['city'] ?? 'madinah';[$status, $body] = http_get('https://api.example.com/weather?city=' . $city); if ($status !== 200) { http_response_code(502); exit('خدمة الطقس غير متاحة');}$w = json_decode($body, true);echo "<p>$city: {$w['temp']}° · {$w['desc']}</p>";
<?php// http.php: نسخة المختبر من http_get// المختبر بلا إنترنت، فنحاكي الخدمات بملفات JSON في مجلد mock// وعلى XAMPP استبدل هذا الملف بنسخة cURL (في الدرس) دون تغيير أي شيء آخرfunction http_get(string $url): array{ $name = trim(preg_replace('/[^a-z0-9]+/i', '_', $url), '_'); // اسم ملف من الرابط $file = __DIR__ . '/mock/' . $name . '.json'; if (!file_exists($file)) { return [404, '{"error":"not found"}']; } return [200, file_get_contents($file)];}
{"city":"madinah","temp":31,"desc":"مشمس"}{"city":"abha","temp":19,"desc":"غائم جزئيًّا"}