دروس Laravel API: كيفية بناء واختبار واجهة برمجة تطبيقات RESTful
نشرت: 2022-03-11مع ظهور تطوير الأجهزة المحمولة وأطر عمل JavaScript ، يعد استخدام RESTful API هو الخيار الأفضل لبناء واجهة واحدة بين بياناتك وعميلك.
Laravel هو إطار عمل PHP تم تطويره مع وضع إنتاجية مطوري PHP في الاعتبار. إطار العمل ، الذي كتبه وصيانته تايلور أوتويل ، عنيد للغاية ويسعى لتوفير وقت المطور من خلال تفضيل الاصطلاح على التكوين. يهدف إطار العمل أيضًا إلى التطور مع الويب وقد قام بالفعل بدمج العديد من الميزات والأفكار الجديدة في عالم تطوير الويب - مثل قوائم انتظار الوظائف ، ومصادقة واجهة برمجة التطبيقات (API) خارج الصندوق ، والتواصل في الوقت الفعلي ، وغير ذلك الكثير.
في هذا البرنامج التعليمي ، سوف نستكشف الطرق التي يمكنك من خلالها إنشاء - واختبار - واجهة برمجة تطبيقات قوية باستخدام Laravel مع المصادقة. سنستخدم Laravel 5.4 ، وكل الشفرة متاحة للرجوع إليها على GitHub.
واجهات برمجة تطبيقات مريحة
أولاً ، نحتاج إلى فهم ما يعتبر بالضبط واجهة برمجة تطبيقات RESTful. يرمز REST إلى REpresentational State Transfer وهو أسلوب معماري للاتصال الشبكي بين التطبيقات ، والذي يعتمد على بروتوكول عديم الحالة (عادةً HTTP) للتفاعل.
أفعال HTTP تمثل الإجراءات
في RESTful APIs ، نستخدم أفعال HTTP كإجراءات ، ونقاط النهاية هي الموارد التي نعمل عليها. سنستخدم أفعال HTTP لمعناها الدلالي:
-
GET
: استرداد الموارد -
POST
: إنشاء الموارد -
PUT
: تحديث الموارد -
DELETE
: حذف الموارد
إجراء التحديث: 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
على متصفحك ، يجب أن تشاهد نموذج الصفحة هذا.الهجرات والنماذج
قبل كتابة الترحيل الأول فعليًا ، تأكد من أن لديك قاعدة بيانات تم إنشاؤها لهذا التطبيق وأضف بيانات اعتمادها إلى ملف .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 صحيحة
إذا حاولت جلب مورد غير موجود ، فسيتم طرح استثناء وستتلقى تتبع المكدس بالكامل ، مثل هذا:
يمكننا إصلاح ذلك من خلال تعديل فئة معالج الاستثناءات الموجودة في 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 مصحوبًا بمصادقة خارج الصندوق ، لكننا ما زلنا بحاجة إلى تعديله قليلاً لإرجاع الاستجابة التي نريدها.

تستفيد وحدة التحكم من سمات المستخدمين 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
ونحصل على نتيجة مثل هذه:
هذا لأننا نحتاج إلى تعديل الطريقة الحالية غير 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 قواعد ذهبية لتصميم رائع لواجهة برمجة تطبيقات الويب.