بناء واجهة برمجة تطبيقات REST لمشاريع PHP القديمة

نشرت: 2022-03-11

لا يعد بناء أو تصميم واجهة برمجة تطبيقات REST مهمة سهلة ، خاصة عندما يتعين عليك القيام بذلك لمشاريع PHP القديمة. يوجد الكثير من مكتبات الطرف الثالث في الوقت الحاضر التي تجعل من السهل تنفيذ واجهة برمجة تطبيقات REST ، ولكن دمجها في قواعد الرموز القديمة الحالية قد يكون أمرًا شاقًا إلى حد ما. وليس لديك دائمًا رفاهية العمل مع أطر العمل الحديثة ، مثل Laravel و Symfony. مع مشاريع PHP القديمة ، يمكنك غالبًا أن تجد نفسك في مكان ما وسط أطر العمل الداخلية المهملة ، والتي تعمل فوق الإصدارات القديمة من PHP.

بناء واجهة برمجة تطبيقات REST لمشاريع PHP القديمة

بناء واجهة برمجة تطبيقات REST لمشاريع PHP القديمة
سقسقة

في هذه المقالة ، سنلقي نظرة على بعض التحديات الشائعة لمحاولة تنفيذ واجهات برمجة تطبيقات REST من البداية ، وبعض الطرق للتغلب على هذه المشكلات واستراتيجية عامة لبناء خوادم API مخصصة تستند إلى PHP للمشاريع القديمة.

على الرغم من أن المقالة تستند إلى PHP 5.3 وما بعده ، إلا أن المفاهيم الأساسية صالحة لجميع إصدارات PHP التي تتجاوز الإصدار 5.0 ، ويمكن حتى تطبيقها على مشاريع بخلاف PHP. هنا ، لن نغطي ماهية واجهة برمجة تطبيقات REST بشكل عام ، لذلك إذا لم تكن معتادًا عليها ، فتأكد من قراءتها أولاً.

لتسهيل المتابعة ، إليك قائمة ببعض المصطلحات المستخدمة في هذه المقالة ومعانيها:

  • خادم API: تطبيق REST الرئيسي الذي يخدم واجهة برمجة التطبيقات ، في هذه الحالة ، مكتوب بلغة PHP.
  • نقطة نهاية واجهة برمجة التطبيقات (API): "طريقة" للواجهة الخلفية يتواصل معها العميل لتنفيذ إجراء وتحقيق النتائج.
  • عنوان URL لنقطة نهاية واجهة برمجة التطبيقات (API): عنوان URL يمكن من خلاله الوصول إلى نظام الواجهة الخلفية للعالم.
  • رمز API المميز: معرّف فريد يتم تمريره عبر رؤوس HTTP أو ملفات تعريف الارتباط التي يمكن من خلالها التعرف على المستخدم.
  • التطبيق: تطبيق العميل الذي سيتواصل مع تطبيق REST عبر نقاط نهاية API. في هذه المقالة سنفترض أنه مستند إلى الويب (سواء أكان سطح مكتب أم جوال) ، ولذا فهو مكتوب بلغة JavaScript.

الخطوات الأولية

أنماط المسار

من أول الأشياء التي نحتاج إلى تحديدها هو مسار عنوان URL الذي ستكون نقاط نهاية واجهة برمجة التطبيقات متاحة فيه. هناك طريقتان شائعتان:

  • أنشئ نطاقًا فرعيًا جديدًا ، مثل api.example.com.
  • قم بإنشاء مسار ، مثل example.com/api.

في لمحة ، قد يبدو أن الخيار الأول أكثر شعبية وجاذبية. ومع ذلك ، في الواقع ، إذا كنت تقوم ببناء واجهة برمجة تطبيقات خاصة بمشروع معين ، فقد يكون من الأنسب اختيار المتغير الثاني.

أحد أهم الأسباب وراء اتباع الطريقة الثانية هو أن هذا يسمح باستخدام ملفات تعريف الارتباط كوسيلة لنقل بيانات الاعتماد. سيرسل العملاء المعتمدون على المتصفح ملفات تعريف الارتباط المناسبة تلقائيًا ضمن طلبات XHR ، مما يلغي الحاجة إلى رأس تفويض إضافي.

سبب مهم آخر هو أنك لست بحاجة إلى القيام بأي شيء فيما يتعلق بتكوين النطاق الفرعي أو مشاكل الإدارة حيث قد يتم تجريد الرؤوس المخصصة بواسطة بعض الخوادم الوكيلة. يمكن أن يكون هذا محنة مملة في المشاريع القديمة.

يمكن اعتبار استخدام ملفات تعريف الارتباط ممارسة "غير مريحة" حيث يجب أن تكون طلبات REST عديمة الحالة. في هذه الحالة يمكننا تقديم حل وسط وتمرير قيمة الرمز المميز في ملف تعريف الارتباط بدلاً من تمريرها عبر رأس مخصص. بشكل فعال ، نحن نستخدم ملفات تعريف الارتباط كطريقة لتمرير قيمة الرمز المميز بدلاً من معرف الجلسة مباشرة. يمكن اعتبار هذا الأسلوب عديم الجنسية ، لكن يمكننا تركه حسب تفضيلاتك.

يمكن أيضًا تعيين إصدارات من عناوين URL لنقطة نهاية واجهة برمجة التطبيقات. بالإضافة إلى ذلك ، يمكنهم تضمين تنسيق الاستجابة المتوقعة كامتداد في اسم المسار. على الرغم من أن هذه ليست حرجة ، خاصة خلال المراحل الأولى من تطوير API ، إلا أن هذه التفاصيل يمكن أن تؤتي ثمارها على المدى الطويل. خاصة عندما تحتاج إلى تنفيذ ميزات جديدة. من خلال التحقق من الإصدار الذي يتوقعه العميل وتقديم التنسيق المطلوب للتوافق مع الإصدارات السابقة ، يمكن أن يكون الحل الأفضل.

يمكن أن تظهر بنية عنوان URL لنقطة نهاية واجهة برمجة التطبيقات على النحو التالي:

 example.com/api/${version_code}/${actual_request_path}.${format}

ومثال حقيقي:

 example.com/api/v1.0/records.json

التوجيه

بعد اختيار عنوان URL أساسي لنقاط نهاية API ، فإن الشيء التالي الذي يتعين علينا القيام به هو التفكير في نظام التوجيه الخاص بنا. يمكن دمجها في إطار عمل موجود ، ولكن إذا كان ذلك مرهقًا للغاية ، فإن الحل البديل المحتمل هو إنشاء مجلد باسم "api" في جذر المستند. بهذه الطريقة يمكن أن يكون لواجهة برمجة التطبيقات منطق منفصل تمامًا. يمكنك توسيع هذا الأسلوب عن طريق وضع منطق API في ملفاته الخاصة ، مثل هذا:

يمكنك التفكير في "www / api / Apis / Users.php" على أنها "وحدة تحكم" منفصلة لنقطة نهاية API معينة. سيكون من الرائع إعادة استخدام التطبيقات من قاعدة البيانات الحالية ، على سبيل المثال إعادة استخدام النماذج التي تم تنفيذها بالفعل في المشروع للتواصل مع قاعدة البيانات.

أخيرًا ، تأكد من توجيه جميع الطلبات الواردة من "/ api / *" إلى "/api/index.php". يمكن القيام بذلك عن طريق تغيير تكوين خادم الويب الخاص بك.

فئة API

الإصدار والشكل

يجب عليك دائمًا تحديد الإصدارات والتنسيقات التي تقبلها نقاط نهاية واجهة API بشكل واضح وما هي الإصدارات الافتراضية. سيسمح لك هذا ببناء ميزات جديدة في المستقبل مع الحفاظ على الوظائف القديمة. يمكن أن يكون إصدار API عبارة عن سلسلة بشكل أساسي ولكن يمكنك استخدام القيم الرقمية لفهم أفضل وقابلية للمقارنة. من الجيد أن يكون لديك أرقام احتياطية للإصدارات الثانوية لأنها تشير بوضوح إلى اختلاف أشياء قليلة فقط:

  • الإصدار 1.0 يعني الإصدار الأول.
  • الإصدار الأول v1.1 مع بعض التغييرات الطفيفة.
  • سيكون الإصدار 2.0 إصدارًا جديدًا تمامًا.

يمكن أن يكون التنسيق أي شيء يحتاجه العميل بما في ذلك على سبيل المثال لا الحصر JSON و XML وحتى CSV. من خلال توفيره عبر عنوان URL كملحق ملف ، يضمن عنوان URL لنقطة نهاية واجهة برمجة التطبيقات إمكانية القراءة ويصبح من السهل على مستهلك واجهة برمجة التطبيقات معرفة التنسيق الذي يمكنهم توقعه:

  • سيعيد "/api/v1.0/records.json" مجموعة سجلات JSON
  • قد يعرض "/api/v1.0/records.xml" ملف سجلات XML

تجدر الإشارة إلى أنك ستحتاج أيضًا إلى إرسال رأس نوع المحتوى المناسب في الاستجابة لكل من هذه التنسيقات.

عند تلقي طلب وارد ، فإن أول ما يجب عليك فعله هو التحقق مما إذا كان خادم واجهة برمجة التطبيقات يدعم الإصدار والتنسيق المطلوبين. في طريقتك الرئيسية ، التي تتعامل مع الطلب الوارد ، قم بتحليل $ _SERVER ['PATH_INFO'] أو $ _SERVER ['REQUEST_URI'] لتحديد ما إذا كان التنسيق والإصدار المطلوبان مدعومين. بعد ذلك ، قم إما بالمتابعة أو إرجاع استجابة 4xx (على سبيل المثال 406 "غير مقبول"). يتمثل الجزء الأكثر أهمية هنا في إرجاع شيء يتوقعه العميل دائمًا. قد يكون البديل لهذا هو التحقق من عنوان الطلب "قبول" بدلاً من امتداد مسار URL.

الطرق المسموح بها

يمكنك إعادة توجيه كل شيء بشفافية إلى وحدات التحكم في واجهة برمجة التطبيقات ، ولكن قد يكون من الأفضل استخدام مجموعة من المسارات المسموح بها في القائمة البيضاء. سيؤدي ذلك إلى تقليل المرونة قليلاً ولكنه سيوفر رؤية واضحة جدًا لما تبدو عليه عناوين URL لنقطة نهاية واجهة برمجة التطبيقات في المرة التالية التي تعود فيها إلى الكود.

 private $public_routes = array( 'system' => array( 'regex' => 'system', ), 'records' => array( 'regex' => 'records(?:/?([0-9]+)?)', ), );

يمكنك أيضًا نقل هذه الملفات إلى ملفات منفصلة لجعل الأشياء أكثر نظافة. سيتم استخدام التكوين أعلاه لتمكين الطلبات إلى عناوين URL هذه:

 /api/v1.0/system.json /api/v1.0/records.json /api/v1.0/records/7.json

التعامل مع بيانات PUT

تتعامل PHP تلقائيًا مع بيانات POST الواردة وتضعها تحت $ _POST superglobal. ومع ذلك ، ليس هذا هو الحال مع طلبات PUT. يتم "دفن" كافة البيانات في php: // input . لا تنسَ تحليلها إلى بنية أو مصفوفة منفصلة قبل استدعاء طريقة API الفعلية. يمكن أن يكون parse_str بسيطًا كافيًا ، ولكن إذا كان العميل يرسل طلبًا متعدد الأجزاء ، فقد تكون هناك حاجة إلى تحليل إضافي للتعامل مع حدود النموذج. تتضمن حالة الاستخدام النموذجية للطلبات متعددة الأجزاء تحميل الملفات. يمكن الكشف عن الطلبات متعددة الأجزاء والتعامل معها على النحو التالي:

 self::$input = file_get_contents('php://input'); // For PUT/DELETE there is input data instead of request variables if (!empty(self::$input)) { preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); if (isset($matches[1]) && strpos(self::$input, $matches[1]) !== false) { $this->parse_raw_request(self::$input, self::$input_data); } else { parse_str(self::$input, self::$input_data); } }

هنا ، يمكن تنفيذ parse_raw_request على النحو التالي:

 /** * Helper method to parse raw requests */ private function parse_raw_request($input, &$a_data) { // grab multipart boundary from content type header preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); $boundary = $matches[1]; // split content by boundary and get rid of last -- element $a_blocks = preg_split("/-+$boundary/", $input); array_pop($a_blocks); // loop data blocks foreach ($a_blocks as $id => $block) { if (empty($block)) { continue; } // parse uploaded files if (strpos($block, 'application/octet-stream') !== false) { // match "name", then everything after "stream" (optional) except for prepending newlines preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); // parse all other fields } else { // match "name" and optional value in between newline sequences preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); } $a_data[$matches[1]] = $matches[2]; } }

باستخدام هذا ، يمكننا الحصول على حمولة الطلب الضرورية في Api :: $ input كمدخلات أولية و Api :: $ input_data كمصفوفة ترابطية .

تزوير وضع / حذف

في بعض الأحيان يمكنك أن ترى نفسك في موقف لا يدعم فيه الخادم أي شيء إلى جانب أساليب GET / POST HTTP القياسية. الحل الشائع لهذه المشكلة هو "وهمية" PUT / DELETE أو أي طريقة طلب مخصصة أخرى. لذلك يمكنك استخدام المعامل "magic" ، مثل "_method". إذا كنت تراه في مصفوفة $ _REQUEST الخاصة بك ، فافترض ببساطة أن الطلب من النوع المحدد. تحتوي الأطر الحديثة مثل Laravel على مثل هذه الوظائف المضمنة فيها. يوفر توافقًا رائعًا في حالة وجود قيود على الخادم أو العميل (على سبيل المثال ، يستخدم شخص شبكة Wi-Fi لوظيفته خلف وكيل الشركة الذي لا يسمح بطلبات PUT.)

إعادة التوجيه إلى API محدد

إذا لم يكن لديك رفاهية إعادة استخدام برامج التحميل التلقائي للمشروع الحالي ، فيمكنك إنشاء أدواتك الخاصة بمساعدة وظيفة spl_autoload_register . حدده في صفحة "api / index.php" الخاصة بك واستدع فئة API الموجودة في "api / Api.php". تعمل فئة API كبرنامج وسيط وتستدعي الطريقة الفعلية. على سبيل المثال ، يجب أن ينتهي طلب "/api/v1.0/records/7.json" باستدعاء طريقة GET "Apis / Records.php" مع المعلمة 7. وهذا من شأنه أن يضمن فصل الاهتمامات ويوفر طريقة للحفاظ على منظف ​​المنطق. بالطبع ، إذا كان من الممكن دمج هذا بشكل أعمق في إطار العمل الذي تستخدمه وإعادة استخدام عناصر التحكم أو المسارات المحددة الخاصة به ، فيجب عليك التفكير في هذا الاحتمال أيضًا.

مثال "api / index.php" مع أداة التحميل التلقائي البدائية:

 <?php // Let's define very primitive autoloader spl_autoload_register(function($classname){ $classname = str_replace('Api_', 'Apis/', $classname); if (file_exists(__DIR__.'/'.$classname.'.php')) { require __DIR__.'/'.$classname.'.php'; } }); // Our main method to handle request Api::serve();

سيؤدي هذا إلى تحميل فئة Api الخاصة بنا والبدء في تقديمها بشكل مستقل عن المشروع الرئيسي.

طلبات OPTIONS

عندما يستخدم العميل رأسًا مخصصًا لإعادة توجيه رمزه المميز الفريد ، يحتاج المتصفح أولاً إلى التحقق متى يدعم الخادم هذا العنوان. هذا هو المكان الذي يأتي فيه طلب OPTIONS. والغرض منه هو ضمان أن كل شيء على ما يرام وآمن لكل من العميل وخادم API. لذلك يمكن إطلاق طلب OPTIONS في كل مرة يحاول فيها العميل فعل أي شيء. ومع ذلك ، عندما يستخدم العميل ملفات تعريف الارتباط للحصول على بيانات الاعتماد ، فإنه يحفظ المتصفح من الاضطرار إلى إرسال طلب OPTIONS الإضافي هذا.

إذا طلب العميل POST /users/8.json مع ملفات تعريف الارتباط ، فسيكون طلبه قياسيًا جدًا:

  • ينفذ التطبيق طلب POST إلى /users/8.json.
  • يقوم المتصفح بتنفيذ الطلب ويتلقى ردًا.

ولكن مع التفويض المخصص أو رأس الرمز المميز:

  • ينفذ التطبيق طلب POST إلى /users/8.json.
  • يتوقف المتصفح عن معالجة الطلب ويبدأ طلب OPTIONS بدلاً من ذلك.
  • يتم إرسال طلب OPTIONS إلى /users/8.json.
  • يتلقى المتصفح استجابة بقائمة بجميع الطرق والعناوين المتاحة ، على النحو المحدد بواسطة واجهة برمجة التطبيقات.
  • يستمر المستعرض بطلب POST الأصلي فقط إذا كان الرأس المخصص موجودًا في قائمة الرؤوس المتاحة.

ومع ذلك ، ضع في اعتبارك أنه حتى عند استخدام ملفات تعريف الارتباط ، مع PUT / DELETE ، قد تستمر في تلقي طلب OPTIONS الإضافي هذا. لذا كن مستعدًا للرد عليه.

سجلات API

تركيب اساسي

مثالنا Records API واضح ومباشر. سيحتوي على جميع طرق الطلب ويعيد الإخراج مرة أخرى إلى نفس فئة واجهة برمجة التطبيقات الرئيسية. علي سبيل المثال:

 <?php class Api_Records { public function __construct() { // In here you could initialize some shared logic between this API and rest of the project } /** * Get individual record or records list */ public function get($id = null) { if ($id) { return $this->getRecord(intval($id)); } else { return $this->getRecords(); } } /** * Update record */ public function put($record_id = null) { // In real world there would be call to model with validation and probably token checking // Use Api::$input_data to update return Api::responseOk('OK', array()); } // ...

لذا فإن تحديد كل طريقة HTTP سيسمح لنا ببناء واجهة برمجة تطبيقات بأسلوب REST بسهولة أكبر.

إخراج التنسيق

يمكن أن يؤدي الرد بسذاجة مع كل ما يتم استلامه من قاعدة البيانات إلى العميل إلى عواقب وخيمة. من أجل تجنب أي عرض غير مقصود للبيانات ، أنشئ طريقة تنسيق محددة لا تعرض سوى مفاتيح القائمة البيضاء.

فائدة أخرى للمفاتيح المدرجة في القائمة البيضاء هي أنه يمكنك كتابة الوثائق بناءً على هذه والقيام بجميع عمليات التحقق من النوع لضمان ، على سبيل المثال ، أن يكون user_id دائمًا عددًا صحيحًا ، وستكون العلامة is_banned دائمًا صحيحة أو خطأ منطقيًا ، وسيكون لأوقات التاريخ معيار واحد تنسيق الاستجابة.

إخراج النتائج

الرؤوس

ستضمن الطرق المنفصلة لإخراج الرؤوس صحة كل شيء يتم إرساله إلى المتصفح. يمكن أن تستخدم هذه الطريقة مزايا إتاحة الوصول إلى واجهة برمجة التطبيقات عبر نفس المجال مع الحفاظ على إمكانية تلقي رأس تفويض مخصص. يمكن أن يحدث الاختيار بين نفس المجال أو مجال الطرف الثالث بمساعدة رؤوس خادم HTTP_ORIGIN و HTTP_REFERER. إذا اكتشف التطبيق أن العميل يستخدم ترخيص x (أو أي رأس مخصص آخر) ، فيجب أن يسمح بالوصول من جميع الأصول ، والسماح بالرأس المخصص. لذلك يمكن أن تبدو كالتالي:

 header('Access-Control-Allow-Origin: *'); header('Access-Control-Expose-Headers: x-authorization'); header('Access-Control-Allow-Headers: origin, content-type, accept, x-authorization'); header('X-Authorization: '.YOUR_TOKEN_HERE);

ومع ذلك ، إذا كان العميل يستخدم بيانات الاعتماد المستندة إلى ملف تعريف الارتباط ، فقد تكون الرؤوس مختلفة قليلاً ، مما يسمح فقط للمضيف المطلوب والرؤوس ذات الصلة بملفات تعريف الارتباط لبيانات الاعتماد:

 header('Access-Control-Allow-Origin: '.$origin); header('Access-Control-Expose-Headers: set-cookie, cookie'); header('Access-Control-Allow-Headers: origin, content-type, accept, set-cookie, cookie'); // Allow cookie credentials because we're on the same domain header('Access-Control-Allow-Credentials: true'); if (strtolower($_SERVER['REQUEST_METHOD']) != 'options') { setcookie(TOKEN_COOKIE_NAME, YOUR_TOKEN_HERE, time()+86400*30, '/', '.'.$_SERVER['HTTP_HOST']); }

ضع في اعتبارك أن طلب OPTIONS لا يدعم ملفات تعريف الارتباط ، لذا لن يرسلها التطبيق معه. وأخيرًا ، يسمح هذا لجميع طرق HTTP المطلوبة بانتهاء صلاحية التحكم في الوصول:

 header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE'); header('Access-Control-Max-Age: 86400');

الجسم

يجب أن يحتوي الجسم نفسه على الاستجابة بالتنسيق الذي طلبه العميل بحالة 2xx HTTP عند النجاح ، وحالة 4xx عند الفشل بسبب العميل وحالة 5xx عند الفشل بسبب الخادم. يمكن أن يختلف هيكل الاستجابة ، على الرغم من أن تحديد حقلي "الحالة" و "الاستجابة" يمكن أن يكون مفيدًا أيضًا. على سبيل المثال ، إذا كان العميل يحاول تسجيل مستخدم جديد وكان اسم المستخدم مأخوذًا بالفعل ، فيمكنك إرسال رد بحالة HTTP 200 ولكن JSON في النص يبدو مثل:

 {“status”: “ERROR”, “response”: ”username already taken”}

… بدلا من خطأ HTTP 4xx مباشرة.

خاتمة

لا يوجد مشروعان متماثلان تمامًا. قد تكون الإستراتيجية الموضحة في هذه المقالة مناسبة لحالتك وقد لا تكون كذلك ، ولكن يجب أن تكون المفاهيم الأساسية متشابهة مع ذلك. تجدر الإشارة إلى أنه لا يمكن أن تحتوي كل صفحة على أحدث إطار عمل شائع أو محدث وراءها ، وأحيانًا يمكن تحويل الغضب بشأن "سبب عدم عمل حزمة REST Symfony الخاصة بي هنا" إلى دافع لبناء شيء مفيد ، شيء يعمل. قد لا تكون النتيجة النهائية لامعة ، حيث ستكون دائمًا تنفيذًا مخصصًا ومخصصًا للمشروع ، ولكن في نهاية اليوم سيكون الحل شيئًا يعمل حقًا ؛ وفي مثل هذا السيناريو ، يجب أن يكون هدف كل مطور لواجهة برمجة التطبيقات.

تم تحميل أمثلة على تطبيقات المفاهيم التي تمت مناقشتها هنا إلى مستودع GitHub للراحة. قد لا ترغب في استخدام هذه الرموز النموذجية مباشرةً في الإنتاج كما هي ، ولكن هذا يمكن أن يعمل بسهولة كنقطة بداية لمشروع تكامل PHP API القديم التالي.

هل كان عليك تنفيذ خادم REST API لبعض المشاريع القديمة مؤخرًا؟ شارك تجربتك معنا في قسم التعليقات أدناه.