دروس Laravel API: كيفية بناء واختبار واجهة برمجة تطبيقات RESTful

نشرت: 2022-03-11

مع ظهور تطوير الأجهزة المحمولة وأطر عمل JavaScript ، يعد استخدام RESTful API هو الخيار الأفضل لبناء واجهة واحدة بين بياناتك وعميلك.

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

دروس Laravel API - بناء خدمة ويب مريحة

في هذا البرنامج التعليمي ، سوف نستكشف الطرق التي يمكنك من خلالها إنشاء - واختبار - واجهة برمجة تطبيقات قوية باستخدام Laravel مع المصادقة. سنستخدم Laravel 5.4 ، وكل الشفرة متاحة للرجوع إليها على GitHub.

واجهات برمجة تطبيقات مريحة

أولاً ، نحتاج إلى فهم ما يعتبر بالضبط واجهة برمجة تطبيقات RESTful. يرمز REST إلى REpresentational State Transfer وهو أسلوب معماري للاتصال الشبكي بين التطبيقات ، والذي يعتمد على بروتوكول عديم الحالة (عادةً HTTP) للتفاعل.

أفعال HTTP تمثل الإجراءات

في RESTful APIs ، نستخدم أفعال HTTP كإجراءات ، ونقاط النهاية هي الموارد التي نعمل عليها. سنستخدم أفعال HTTP لمعناها الدلالي:

  • GET : استرداد الموارد
  • POST : إنشاء الموارد
  • PUT : تحديث الموارد
  • DELETE : حذف الموارد

أفعال HTTP: GET و POST و PUT و DELETE هي إجراءات في RESTful APIs

إجراء التحديث: PUT مقابل POST

تعد RESTful APIs مسألة جدل وهناك الكثير من الآراء حول ما إذا كان من الأفضل التحديث باستخدام POST أو PATCH أو PUT ، أو إذا كان من الأفضل ترك إجراء الإنشاء لفعل PUT . في هذه المقالة PUT لإجراء التحديث ، وفقًا لـ HTTP RFC ، يعني PUT إنشاء / تحديث مورد في موقع معين. هناك مطلب آخر لفعل PUT وهو idempotence ، والذي يعني في هذه الحالة أنه يمكنك إرسال هذا الطلب مرة أو مرتين أو 1000 مرة وستكون النتيجة هي نفسها: مورد واحد محدث في قاعدة البيانات.

موارد

ستكون الموارد هي أهداف الإجراءات ، في حالتنا المقالات والمستخدمين ، ولديهم نقاط النهاية الخاصة بهم:

  • /articles
  • /users

في هذا البرنامج التعليمي لـ Laravel api ، سيكون للموارد تمثيل 1: 1 على نماذج البيانات لدينا ، لكن هذا ليس مطلبًا. يمكن أن يكون لديك موارد ممثلة في أكثر من نموذج بيانات واحد (أو غير ممثلة على الإطلاق في قاعدة البيانات) ونماذج خارج الحدود تمامًا للمستخدم. في النهاية ، عليك أن تقرر كيفية تصميم الموارد والنماذج بطريقة تناسب تطبيقك.

ملاحظة حول الاتساق

أكبر ميزة لاستخدام مجموعة من الاصطلاحات مثل REST هي أن واجهة برمجة التطبيقات الخاصة بك ستكون أسهل بكثير في الاستهلاك والتطوير. بعض نقاط النهاية واضحة جدًا ، ونتيجة لذلك ، ستكون واجهة برمجة التطبيقات الخاصة بك أسهل بكثير في الاستخدام والصيانة بدلاً من وجود نقاط نهاية مثل GET /get_article?id_article=12 و POST /delete_article?number=40 . لقد أنشأت واجهات برمجة تطبيقات رهيبة كهذه في الماضي وما زلت أكره نفسي بسبب ذلك.

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

إعداد مشروع Laravel Web Service

كما هو الحال مع جميع أطر PHP الحديثة ، سنحتاج إلى Composer لتثبيت والتعامل مع تبعياتنا. بعد اتباع تعليمات التنزيل (والإضافة إلى متغير بيئة المسار الخاص بك) ، قم بتثبيت Laravel باستخدام الأمر:

 $ composer global require laravel/installer

بعد انتهاء التثبيت ، يمكنك إنشاء تطبيق جديد مثل هذا:

 $ laravel new myapp

للأمر أعلاه ، يجب أن يكون لديك ~/composer/vendor/bin في $PATH . إذا كنت لا ترغب في التعامل مع ذلك ، يمكنك أيضًا إنشاء مشروع جديد باستخدام Composer:

 $ composer create-project --prefer-dist laravel/laravel myapp

مع تثبيت Laravel ، يجب أن تكون قادرًا على بدء تشغيل الخادم واختبار ما إذا كان كل شيء يعمل:

 $ php artisan serve Laravel development server started: <http://127.0.0.1:8000> 

عندما تفتح localhost: 8000 على متصفحك ، يجب أن تشاهد نموذج صفحة Laravel

عندما تفتح localhost:8000 على متصفحك ، يجب أن تشاهد نموذج الصفحة هذا.

الهجرات والنماذج

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

 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret

يمكنك أيضًا استخدام Homestead ، وهو صندوق Vagrant المصمم خصيصًا لـ Laravel ، لكن هذا خارج نطاق هذه المقالة قليلاً. إذا كنت ترغب في معرفة المزيد ، راجع وثائق Homestead.

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

 $ php artisan make:model Article -m

يعتبر الخيار -m اختصارًا لـ --migration ويخبر Artisan بإنشاء واحد لنموذجنا. ها هي الهجرة المتولدة:

 <?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateArticlesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('articles'); } }

دعونا نحلل هذا لمدة ثانية:

  • سيتم تشغيل التابعين up() و down() عندما نقوم بالترحيل والتراجع على التوالي ؛
  • $table->increments('id') يُنشئ عددًا صحيحًا متزايدًا تلقائيًا مع id الاسم ؛
  • $table->timestamps() ستقوم بإعداد الطوابع الزمنية بالنسبة لنا - created_at و updated_at ، ولكن لا تقلق بشأن تعيين افتراضي ، فإن Laravel يعتني بتحديث هذه الحقول عند الحاجة.
  • وأخيرًا ، Schema::dropIfExists() ، بالطبع ، بإسقاط الجدول إذا كان موجودًا.

بعد ذلك ، دعنا نضيف سطرين إلى طريقة up() :

 public function up() { Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->text('body'); $table->timestamps(); }); }

تقوم الطريقة string() بإنشاء عمود VARCHAR مكافئ بينما ينشئ text() مكافئ TEXT . بعد ذلك ، دعنا نمضي قدمًا ونهاجر:

 $ php artisan migrate

يمكنك أيضًا استخدام الخيار --step هنا ، وسيقوم بفصل كل عملية ترحيل إلى مجموعتها الخاصة بحيث يمكنك التراجع عنها بشكل فردي إذا لزم الأمر.

يأتي Laravel خارج الصندوق مع عمليتي تهجير ، create_users_table و create_password_resets_table . لن نستخدم الجدول password_resets ، ولكن سيكون من المفيد تجهيز جدول users لنا.

الآن دعنا نعود إلى نموذجنا ونضيف هذه السمات إلى الحقل $fillable حتى نتمكن من استخدامها في نماذجنا Article::create and Article::update :

 class Article extends Model { protected $fillable = ['title', 'body']; }

يمكن تعيين كتلة الحقول داخل الخاصية $fillable باستخدام التابعين create() و update() في Eloquent. يمكنك أيضًا استخدام الخاصية $guarded للسماح لجميع العقارات باستثناء عدد قليل منها.

بذر قاعدة البيانات

بذر قاعدة البيانات هو عملية ملء قاعدة البيانات الخاصة بنا ببيانات وهمية يمكننا استخدامها لاختبارها. يأتي Laravel مع Faker ، وهي مكتبة رائعة لإنشاء التنسيق الصحيح للبيانات الوهمية لنا. لنقم بإنشاء أول بذر لدينا:

 $ php artisan make:seeder ArticlesTableSeeder

سيكون البذر موجودًا في دليل /database/seeds . إليك كيف يبدو الأمر بعد أن قمنا بإعداده لإنشاء بعض المقالات:

 class ArticlesTableSeeder extends Seeder { public function run() { // Let's truncate our existing records to start from scratch. Article::truncate(); $faker = \Faker\Factory::create(); // And now, let's create a few articles in our database: for ($i = 0; $i < 50; $i++) { Article::create([ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]); } } }

لذلك دعونا نشغل أمر البذور:

 $ php artisan db:seed --class=ArticlesTableSeeder

دعنا نكرر العملية لإنشاء مصدر للمستخدمين:

 class UsersTableSeeder extends Seeder { public function run() { // Let's clear the users table first User::truncate(); $faker = \Faker\Factory::create(); // Let's make sure everyone has the same password and // let's hash it before the loop, or else our seeder // will be too slow. $password = Hash::make('toptal'); User::create([ 'name' => 'Administrator', 'email' => '[email protected]', 'password' => $password, ]); // And now let's generate a few dozen users for our app: for ($i = 0; $i < 10; $i++) { User::create([ 'name' => $faker->name, 'email' => $faker->email, 'password' => $password, ]); } } }

يمكننا تسهيل الأمر عن طريق إضافة أدوات البذر إلى فئة DatabaseSeeder الرئيسية داخل مجلد database/seeds :

 class DatabaseSeeder extends Seeder { public function run() { $this->call(ArticlesTableSeeder::class); $this->call(UsersTableSeeder::class); } }

بهذه الطريقة ، يمكننا ببساطة تشغيل $ php artisan db:seed وسيقوم بتشغيل جميع الفئات المسماة في طريقة run() .

الطرق وأجهزة التحكم

لنقم بإنشاء نقاط النهاية الأساسية لتطبيقنا: إنشاء واسترداد القائمة واسترداد واحدة وتحديث وحذف. في ملف routes/api.php ، يمكننا القيام بذلك ببساطة:

 Use App\Article; Route::get('articles', function() { // If the Content-Type and Accept headers are set to 'application/json', // this will return a JSON structure. This will be cleaned up later. return Article::all(); }); Route::get('articles/{id}', function($id) { return Article::find($id); }); Route::post('articles', function(Request $request) { return Article::create($request->all); }); Route::put('articles/{id}', function(Request $request, $id) { $article = Article::findOrFail($id); $article->update($request->all()); return $article; }); Route::delete('articles/{id}', function($id) { Article::find($id)->delete(); return 204; })

ستكون المسارات داخل api.php مسبوقة بـ /api/ وسيتم تطبيق البرنامج الوسيط الخانق لواجهة برمجة التطبيقات تلقائيًا على هذه المسارات (إذا كنت تريد إزالة البادئة ، يمكنك تحرير فئة RouteServiceProvider على /app/Providers/RouteServiceProvider.php ).

الآن دعنا ننقل هذا الرمز إلى وحدة التحكم الخاصة به:

 $ php artisan make:controller ArticleController

ArticleController.php:

 use App\Article; class ArticleController extends Controller { public function index() { return Article::all(); } public function show($id) { return Article::find($id); } public function store(Request $request) { return Article::create($request->all()); } public function update(Request $request, $id) { $article = Article::findOrFail($id); $article->update($request->all()); return $article; } public function delete(Request $request, $id) { $article = Article::findOrFail($id); $article->delete(); return 204; } }

routes/api.php :

 Route::get('articles', 'ArticleController@index'); Route::get('articles/{id}', 'ArticleController@show'); Route::post('articles', 'ArticleController@store'); Route::put('articles/{id}', 'ArticleController@update'); Route::delete('articles/{id}', 'ArticleController@delete');

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

 Route::get('articles', 'ArticleController@index'); Route::get('articles/{article}', 'ArticleController@show'); Route::post('articles', 'ArticleController@store'); Route::put('articles/{article}', 'ArticleController@update'); Route::delete('articles/{article}', 'ArticleController@delete');
 class ArticleController extends Controller { public function index() { return Article::all(); } public function show(Article $article) { return $article; } public function store(Request $request) { $article = Article::create($request->all()); return response()->json($article, 201); } public function update(Request $request, Article $article) { $article->update($request->all()); return response()->json($article, 200); } public function delete(Article $article) { $article->delete(); return response()->json(null, 204); } }

ملاحظة حول أكواد حالة HTTP وتنسيق الاستجابة

لقد أضفنا أيضًا استدعاء response()->json() إلى نقاط النهاية الخاصة بنا. يتيح لنا ذلك إرجاع بيانات JSON بشكل صريح بالإضافة إلى إرسال رمز HTTP يمكن تحليله بواسطة العميل. الرموز الأكثر شيوعًا التي ستردها ستكون:

  • 200 : حسنًا. رمز النجاح القياسي والخيار الافتراضي.
  • 201 : تم إنشاء الكائن. مفيدة لإجراءات store .
  • 204 : لا محتوى. عندما يتم تنفيذ الإجراء بنجاح ، ولكن لا يوجد محتوى لإرجاعه.
  • 206 : محتوى جزئي. مفيد عندما يتعين عليك إرجاع قائمة مرقمة من الموارد.
  • 400 : طلب غير صالح. الخيار القياسي للطلبات التي تفشل في اجتياز التحقق من الصحة.
  • 401 : غير مصرح به. يحتاج المستخدم إلى المصادقة.
  • 403 : ممنوع. تم مصادقة المستخدم ، ولكن ليس لديه الأذونات اللازمة لتنفيذ إجراء.
  • 404 : غير موجود. سيعيد Laravel هذا تلقائيًا عندما لا يتم العثور على المصدر.
  • 500 : خطأ داخلي في الخادم. من الناحية المثالية ، لن تعيد هذا بشكل صريح ، ولكن إذا حدث شيء غير متوقع ، فهذا ما سيحصل عليه المستخدم.
  • 503 : الخدمة غير متوفرة. تشرح نفسها بنفسها ، ولكن أيضًا رمز آخر لن يتم إرجاعه صراحةً بواسطة التطبيق.

إرسال استجابة 404 صحيحة

إذا حاولت جلب مورد غير موجود ، فسيتم طرح استثناء وستتلقى تتبع المكدس بالكامل ، مثل هذا:

NotFoundHttpException Stacktrace

يمكننا إصلاح ذلك من خلال تعديل فئة معالج الاستثناءات الموجودة في app/Exceptions/Handler.php ، لإرجاع استجابة JSON:

 public function render($request, Exception $exception) { // This will replace our 404 response with // a JSON response. if ($exception instanceof ModelNotFoundException) { return response()->json([ 'error' => 'Resource not found' ], 404); } return parent::render($request, $exception); }

إليك مثال على الإرجاع:

 { data: "Resource not found" }

إذا كنت تستخدم Laravel لخدمة صفحات أخرى ، فيجب عليك تحرير الكود للعمل مع رأس Accept ، وإلا فإن أخطاء 404 من الطلبات العادية ستعيد JSON أيضًا.

 public function render($request, Exception $exception) { // This will replace our 404 response with // a JSON response. if ($exception instanceof ModelNotFoundException && $request->wantsJson()) { return response()->json([ 'data' => 'Resource not found' ], 404); } return parent::render($request, $exception); }

في هذه الحالة ، ستحتاج طلبات واجهة برمجة التطبيقات إلى العنوان Accept: application/json .

المصادقة

هناك العديد من الطرق لتطبيق مصادقة API في Laravel (إحداها Passport ، طريقة رائعة لتطبيق OAuth2) ، لكن في هذه المقالة ، سنتخذ أسلوبًا مبسطًا للغاية.

للبدء ، سنحتاج إلى إضافة حقل api_token إلى جدول users :

 $ php artisan make:migration --table=users adds_api_token_to_users_table

ثم ننفذ الهجرة:

 public function up() { Schema::table('users', function (Blueprint $table) { $table->string('api_token', 60)->unique()->nullable(); }); } public function down() { Schema::table('users', function (Blueprint $table) { $table->dropColumn(['api_token']); }); }

بعد ذلك ، ما عليك سوى تشغيل الترحيل باستخدام:

 $ php artisan migrate

إنشاء نقطة نهاية التسجيل

سنستخدم RegisterController (في مجلد Auth ) لإرجاع الاستجابة الصحيحة عند التسجيل. يأتي Laravel مصحوبًا بمصادقة خارج الصندوق ، لكننا ما زلنا بحاجة إلى تعديله قليلاً لإرجاع الاستجابة التي نريدها.

إذا كانت واجهات برمجة التطبيقات باللغة الإنجليزية ، فهذا ما ستبدو عليه محادثة مصادقة API

تستفيد وحدة التحكم من سمات المستخدمين RegistersUsers لتنفيذ التسجيل. وإليك كيف يعمل:

 public function register(Request $request) { // Here the request is validated. The validator method is located // inside the RegisterController, and makes sure the name, email // password and password_confirmation fields are required. $this->validator($request->all())->validate(); // A Registered event is created and will trigger any relevant // observers, such as sending a confirmation email or any // code that needs to be run as soon as the user is created. event(new Registered($user = $this->create($request->all()))); // After the user is created, he's logged in. $this->guard()->login($user); // And finally this is the hook that we want. If there is no // registered() method or it returns null, redirect him to // some other URL. In our case, we just need to implement // that method to return the correct response. return $this->registered($request, $user) ?: redirect($this->redirectPath()); }

نحتاج فقط إلى تنفيذ الطريقة registered() في RegisterController . تستقبل الطريقة $request $user ، لذلك هذا كل ما نريده حقًا. إليك كيف يجب أن تبدو الطريقة داخل وحدة التحكم:

 protected function registered(Request $request, $user) { $user->generateToken(); return response()->json(['data' => $user->toArray()], 201); }

ويمكننا ربطه بملف المسارات:

 Route::post('register', 'Auth\RegisterController@register');

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

 class User extends Authenticatable { ... public function generateToken() { $this->api_token = str_random(60); $this->save(); return $this->api_token; } }

وهذا كل شيء. تم تسجيل المستخدم الآن وبفضل التحقق من صحة Laravel والمصادقة خارج الصندوق ، فإن حقول name email password وتأكيد كلمة password_confirmation مطلوبة ، ويتم التعامل مع الملاحظات تلقائيًا. تحقق من طريقة validator() داخل RegisterController لمعرفة كيفية تنفيذ القواعد.

هذا ما نحصل عليه عندما نصل إلى نقطة النهاية هذه:

 $ curl -X POST http://localhost:8000/api/register \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{"name": "John", "email": "[email protected]", "password": "toptal123", "password_confirmation": "toptal123"}'
 { "data": { "api_token":"0syHnl0Y9jOIfszq11EC2CBQwCfObmvscrZYo5o2ilZPnohvndH797nDNyAT", "created_at": "2017-06-20 21:17:15", "email": "[email protected]", "id": 51, "name": "John", "updated_at": "2017-06-20 21:17:15" } }

إنشاء نقطة نهاية تسجيل الدخول

تمامًا مثل نقطة نهاية التسجيل ، يمكننا تحرير LoginController (في مجلد Auth ) لدعم مصادقة API الخاصة بنا. يمكن تجاوز طريقة login لسمة AuthenticatesUsers لدعم واجهة برمجة التطبيقات الخاصة بنا:

 public function login(Request $request) { $this->validateLogin($request); if ($this->attemptLogin($request)) { $user = $this->guard()->user(); $user->generateToken(); return response()->json([ 'data' => $user->toArray(), ]); } return $this->sendFailedLoginResponse($request); }

ويمكننا ربطه بملف المسارات:

 Route::post('login', 'Auth\LoginController@login');

الآن ، بافتراض تشغيل البذر ، إليك ما نحصل عليه عندما نرسل طلب POST إلى هذا الطريق:

 $ curl -X POST localhost:8000/api/login \ -H "Accept: application/json" \ -H "Content-type: application/json" \ -d "{\"email\": \"[email protected]\", \"password\": \"toptal\" }"
 { "data": { "id":1, "name":"Administrator", "email":"[email protected]", "created_at":"2017-04-25 01:05:34", "updated_at":"2017-04-25 02:50:40", "api_token":"Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw" } }

لإرسال الرمز المميز في طلب ، يمكنك القيام بذلك عن طريق إرسال سمة api_token في الحمولة أو كرمز لحاملها في رؤوس الطلب في شكل Authorization: Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw .

تسجيل الخروج

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

routes/api.php :

 Route::post('logout', 'Auth\LoginController@logout');

Auth\LoginController.php :

 public function logout(Request $request) { $user = Auth::guard('api')->user(); if ($user) { $user->api_token = null; $user->save(); } return response()->json(['data' => 'User logged out.'], 200); }

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

استخدام البرامج الوسيطة لتقييد الوصول

مع إنشاء api_token ، يمكننا تبديل برمجية المصادقة الوسيطة في ملف المسارات:

 Route::middleware('auth:api') ->get('/user', function (Request $request) { return $request->user(); });

يمكننا الوصول إلى المستخدم الحالي باستخدام طريقة $request->user() أو من خلال واجهة Auth

 Auth::guard('api')->user(); // instance of the logged user Auth::guard('api')->check(); // if a user is authenticated Auth::guard('api')->id(); // the id of the authenticated user

ونحصل على نتيجة مثل هذه:

متراصة InvalidArgumentException

هذا لأننا نحتاج إلى تعديل الطريقة الحالية غير unauthenticated عليها في فئة المعالج. يعرض الإصدار الحالي JSON فقط إذا كان الطلب يحتوي على رأس Accept: application/json ، لذلك دعونا نغيره:

 protected function unauthenticated($request, AuthenticationException $exception) { return response()->json(['error' => 'Unauthenticated'], 401); }

مع هذا الإصلاح ، يمكننا العودة إلى نقاط نهاية المقالة لالتفافها في auth:api middleware. يمكننا القيام بذلك باستخدام مجموعات المسارات:

 Route::group(['middleware' => 'auth:api'], function() { Route::get('articles', 'ArticleController@index'); Route::get('articles/{article}', 'ArticleController@show'); Route::post('articles', 'ArticleController@store'); Route::put('articles/{article}', 'ArticleController@update'); Route::delete('articles/{article}', 'ArticleController@delete'); });

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

اختبار نقاط النهاية لدينا

يتضمن Laravel التكامل مع PHPUnit خارج الصندوق مع إعداد phpunit.xml بالفعل. يوفر لنا إطار العمل أيضًا العديد من المساعدين والتأكيدات الإضافية التي تجعل حياتنا أسهل كثيرًا ، خاصةً لاختبار واجهات برمجة التطبيقات.

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

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

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

في ملف config/database.php الخاص بنا ، سنحتاج إلى إعداد حقل database في تكوين sqlite إلى :memory: :

 ... 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], ... ]

ثم قم بتمكين SQLite في phpunit.xml عن طريق إضافة متغير البيئة DB_CONNECTION :

 <php> <env name="APP_ENV" value="testing"/> <env name="CACHE_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/> <env name="QUEUE_DRIVER" value="sync"/> <env name="DB_CONNECTION" value="sqlite"/> </php>

بعد ذلك ، كل ما تبقى هو تكوين فئة TestCase الأساسية الخاصة بنا لاستخدام عمليات الترحيل وبذور قاعدة البيانات قبل كل اختبار. للقيام بذلك ، نحتاج إلى إضافة سمة DatabaseMigrations ، ثم إضافة استدعاء Artisan على طريقة setUp() الخاصة بنا. ها هو الفصل بعد التغييرات:

 use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Support\Facades\Artisan; abstract class TestCase extends BaseTestCase { use CreatesApplication, DatabaseMigrations; public function setUp() { parent::setUp(); Artisan::call('db:seed'); } }

آخر شيء أود القيام به هو إضافة أمر الاختبار إلى composer.json :

 "scripts": { "test" : [ "vendor/bin/phpunit" ], ... },

سيكون أمر الاختبار متاحًا مثل هذا:

 $ composer test

إنشاء مصانع لاختباراتنا

ستسمح لنا المصانع بإنشاء كائنات بسرعة بالبيانات الصحيحة للاختبار. إنها موجودة في مجلد database/factories . يخرج Laravel من الصندوق بمصنع لفئة User ، لذلك دعونا نضيف واحدًا لفئة Article :

 $factory->define(App\Article::class, function (Faker\Generator $faker) { return [ 'title' => $faker->sentence, 'body' => $faker->paragraph, ]; });

تم بالفعل حقن مكتبة Faker لمساعدتنا في إنشاء التنسيق الصحيح للبيانات العشوائية لنماذجنا.

اختباراتنا الأولى

يمكننا استخدام طرق التأكيد في Laravel للوصول بسهولة إلى نقطة نهاية وتقييم استجابتها. لنقم بإنشاء اختبارنا الأول ، اختبار تسجيل الدخول ، باستخدام الأمر التالي:

 $ php artisan make:test Feature/LoginTest

وهنا اختبارنا:

 class LoginTest extends TestCase { public function testRequiresEmailAndLogin() { $this->json('POST', 'api/login') ->assertStatus(422) ->assertJson([ 'email' => ['The email field is required.'], 'password' => ['The password field is required.'], ]); } public function testUserLoginsSuccessfully() { $user = factory(User::class)->create([ 'email' => '[email protected]', 'password' => bcrypt('toptal123'), ]); $payload = ['email' => '[email protected]', 'password' => 'toptal123']; $this->json('POST', 'api/login', $payload) ->assertStatus(200) ->assertJsonStructure([ 'data' => [ 'id', 'name', 'email', 'created_at', 'updated_at', 'api_token', ], ]); } }

تختبر هذه الأساليب بضع حالات بسيطة. تصل طريقة json() إلى نقطة النهاية وتكون التأكيدات الأخرى تشرح نفسها بنفسها. أحد التفاصيل حول assertJson() : يحول هذا التابع الاستجابة إلى مصفوفة بحث عن الوسيطة ، لذا فإن الترتيب مهم. يمكنك ربط عدة assertJson() في هذه الحالة.

الآن ، دعنا ننشئ اختبار نقطة نهاية التسجيل ونكتب زوجًا لنقطة النهاية هذه:

 $ php artisan make:test RegisterTest
 class RegisterTest extends TestCase { public function testsRegistersSuccessfully() { $payload = [ 'name' => 'John', 'email' => '[email protected]', 'password' => 'toptal123', 'password_confirmation' => 'toptal123', ]; $this->json('post', '/api/register', $payload) ->assertStatus(201) ->assertJsonStructure([ 'data' => [ 'id', 'name', 'email', 'created_at', 'updated_at', 'api_token', ], ]);; } public function testsRequiresPasswordEmailAndName() { $this->json('post', '/api/register') ->assertStatus(422) ->assertJson([ 'name' => ['The name field is required.'], 'email' => ['The email field is required.'], 'password' => ['The password field is required.'], ]); } public function testsRequirePasswordConfirmation() { $payload = [ 'name' => 'John', 'email' => '[email protected]', 'password' => 'toptal123', ]; $this->json('post', '/api/register', $payload) ->assertStatus(422) ->assertJson([ 'password' => ['The password confirmation does not match.'], ]); } }

وأخيرًا ، نقطة نهاية تسجيل الخروج:

 $ php artisan make:test LogoutTest
 class LogoutTest extends TestCase { public function testUserIsLoggedOutProperly() { $user = factory(User::class)->create(['email' => '[email protected]']); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $this->json('get', '/api/articles', [], $headers)->assertStatus(200); $this->json('post', '/api/logout', [], $headers)->assertStatus(200); $user = User::find($user->id); $this->assertEquals(null, $user->api_token); } public function testUserWithNullToken() { // Simulating login $user = factory(User::class)->create(['email' => '[email protected]']); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; // Simulating logout $user->api_token = null; $user->save(); $this->json('get', '/api/articles', [], $headers)->assertStatus(401); } }

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

يعد اختبار نقاط نهاية المقالة واضحًا أيضًا:

 class ArticleTest extends TestCase { public function testsArticlesAreCreatedCorrectly() { $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $payload = [ 'title' => 'Lorem', 'body' => 'Ipsum', ]; $this->json('POST', '/api/articles', $payload, $headers) ->assertStatus(200) ->assertJson(['id' => 1, 'title' => 'Lorem', 'body' => 'Ipsum']); } public function testsArticlesAreUpdatedCorrectly() { $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $article = factory(Article::class)->create([ 'title' => 'First Article', 'body' => 'First Body', ]); $payload = [ 'title' => 'Lorem', 'body' => 'Ipsum', ]; $response = $this->json('PUT', '/api/articles/' . $article->id, $payload, $headers) ->assertStatus(200) ->assertJson([ 'id' => 1, 'title' => 'Lorem', 'body' => 'Ipsum' ]); } public function testsArtilcesAreDeletedCorrectly() { $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $article = factory(Article::class)->create([ 'title' => 'First Article', 'body' => 'First Body', ]); $this->json('DELETE', '/api/articles/' . $article->id, [], $headers) ->assertStatus(204); } public function testArticlesAreListedCorrectly() { factory(Article::class)->create([ 'title' => 'First Article', 'body' => 'First Body' ]); factory(Article::class)->create([ 'title' => 'Second Article', 'body' => 'Second Body' ]); $user = factory(User::class)->create(); $token = $user->generateToken(); $headers = ['Authorization' => "Bearer $token"]; $response = $this->json('GET', '/api/articles', [], $headers) ->assertStatus(200) ->assertJson([ [ 'title' => 'First Article', 'body' => 'First Body' ], [ 'title' => 'Second Article', 'body' => 'Second Body' ] ]) ->assertJsonStructure([ '*' => ['id', 'body', 'title', 'created_at', 'updated_at'], ]); } }

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

هذا كل ما في الامر. هناك بالتأكيد مجال للتحسين - يمكنك تنفيذ OAuth2 مع حزمة Passport ، ودمج ترقيم الصفحات وطبقة التحويل (أوصي Fractal) ، والقائمة تطول - لكنني أردت الاطلاع على أساسيات إنشاء واختبار API في Laravel بدون الحزم الخارجية.

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

إذا كنت تصمم واجهة برمجة تطبيقات عامة ، فراجع 5 قواعد ذهبية لتصميم رائع لواجهة برمجة تطبيقات الويب.

الموضوعات ذات الصلة: المصادقة الكاملة للمستخدم والتحكم في الوصول - برنامج تعليمي لـ Laravel Passport ، Pt. 1