บทช่วยสอน Laravel API: วิธีสร้างและทดสอบ RESTful API

เผยแพร่แล้ว: 2022-03-11

ด้วยการพัฒนามือถือและเฟรมเวิร์ก JavaScript ที่เพิ่มขึ้น การใช้ RESTful API เป็นตัวเลือกที่ดีที่สุดในการสร้างอินเทอร์เฟซเดียวระหว่างข้อมูลและไคลเอนต์ของคุณ

Laravel เป็นเฟรมเวิร์ก PHP ที่พัฒนาขึ้นโดยคำนึงถึงประสิทธิภาพการทำงานของนักพัฒนา PHP เขียนและดูแลโดย Taylor Otwell เฟรมเวิร์กมีความเห็นอย่างมากและมุ่งมั่นที่จะประหยัดเวลาของนักพัฒนาด้วยการเลือกแบบแผนมากกว่าการกำหนดค่า กรอบงานนี้มีจุดมุ่งหมายเพื่อพัฒนาไปพร้อมกับเว็บ และได้รวมเอาคุณลักษณะและแนวคิดใหม่ๆ หลายอย่างไว้ในโลกของการพัฒนาเว็บแล้ว เช่น คิวงาน การตรวจสอบสิทธิ์ API ที่พร้อมใช้งานทันที การสื่อสารแบบเรียลไทม์ และอื่นๆ อีกมากมาย

บทช่วยสอน Laravel API - การสร้างบริการเว็บ RESTful

ในบทช่วยสอนนี้ เราจะสำรวจวิธีที่คุณสามารถสร้างและทดสอบ API ที่มีประสิทธิภาพโดยใช้ Laravel พร้อมการรับรองความถูกต้อง เราจะใช้ Laravel 5.4 และรหัสทั้งหมดมีให้สำหรับอ้างอิงใน GitHub

RESTful APIs

อันดับแรก เราต้องเข้าใจก่อนว่าอะไรคือ RESTful API REST ย่อมาจาก REpresentational State Transfer และเป็นรูปแบบสถาปัตยกรรมสำหรับการสื่อสารเครือข่ายระหว่างแอปพลิเคชัน ซึ่งอาศัยโปรโตคอลไร้สัญชาติ (โดยปกติคือ HTTP) สำหรับการโต้ตอบ

HTTP Verbs แสดงถึงการกระทำ

ใน RESTful API เราใช้กริยา HTTP เป็นการกระทำ และปลายทางคือทรัพยากรที่ดำเนินการ เราจะใช้กริยา HTTP สำหรับความหมายเชิงความหมาย:

  • GET : ดึงทรัพยากร
  • POST : สร้างทรัพยากร
  • PUT : อัปเดตทรัพยากร
  • DELETE : ลบทรัพยากร

กริยา HTTP: GET, POST, PUT และ DELETE เป็นการกระทำใน RESTful APIs

อัปเดตการดำเนินการ: PUT กับ POST

RESTful APIs เป็นเรื่องของการถกเถียงกันมากมายและมีความคิดเห็นมากมายว่าควรอัปเดตด้วย POST , PATCH หรือ PUT ดีที่สุดหรือไม่ หรือถ้าปล่อยไว้เป็นกริยา PUT การดำเนินการ create นั้นดีที่สุด ในบทความนี้ เราจะใช้ PUT สำหรับการดำเนินการอัปเดต ตาม HTTP RFC PUT หมายถึงการสร้าง/อัปเดตทรัพยากรในตำแหน่งเฉพาะ ข้อกำหนดอีกประการสำหรับกริยา PUT คือ idempotence ซึ่งในกรณีนี้โดยทั่วไปหมายความว่าคุณสามารถส่งคำขอนั้น 1, 2 หรือ 1,000 ครั้งและผลลัพธ์จะเหมือนกัน: ทรัพยากรที่อัปเดตหนึ่งรายการในฐานข้อมูล

ทรัพยากร

ทรัพยากรจะเป็นเป้าหมายของการดำเนินการ ในกรณีของเรา บทความและผู้ใช้ และมีจุดสิ้นสุดของตนเอง:

  • /articles
  • /users

ในบทช่วยสอน laravel api นี้ ทรัพยากรจะมีการแสดงแบบ 1:1 บนโมเดลข้อมูลของเรา แต่นั่นไม่ใช่ข้อกำหนด คุณสามารถมีทรัพยากรที่แสดงในรูปแบบข้อมูลมากกว่าหนึ่งตัว (หรือไม่มีการแสดงเลยในฐานข้อมูล) และแบบจำลองนั้นไม่มีขีดจำกัดสำหรับผู้ใช้ ในท้ายที่สุด คุณจะต้องตัดสินใจว่าจะออกแบบทรัพยากรและแบบจำลองอย่างไรให้เหมาะสมกับแอปพลิเคชันของคุณ

หมายเหตุเกี่ยวกับความสม่ำเสมอ

ข้อได้เปรียบที่ยิ่งใหญ่ที่สุดของการใช้ชุดข้อตกลงเช่น REST คือ API ของคุณจะใช้งานและพัฒนาได้ง่ายขึ้นมาก จุดปลายบางจุดค่อนข้างตรงไปตรงมา และด้วยเหตุนี้ API ของคุณจะใช้งานและบำรุงรักษาง่ายกว่ามาก เมื่อเทียบกับการมีจุดปลายเช่น GET /get_article?id_article=12 และ POST /delete_article?number=40 ฉันเคยสร้าง API ที่แย่มากๆ แบบนี้มาก่อน และฉันยังเกลียดตัวเองสำหรับมัน

อย่างไรก็ตาม อาจมีบางกรณีที่การแมปกับสคีมา Create/Retrieve/Update/Delete schema เป็นเรื่องยาก โปรดจำไว้ว่า URL ไม่ควรมีกริยาและทรัพยากรไม่จำเป็นต้องเป็นแถวในตาราง สิ่งที่ควรทราบอีกประการหนึ่งคือ คุณไม่จำเป็นต้องดำเนินการทุกอย่างสำหรับทรัพยากรทุกอย่าง

การตั้งค่าโครงการบริการเว็บ Laravel

เช่นเดียวกับเฟรมเวิร์ก 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

มาเริ่มกันที่โมเดลแรกและการโยกย้ายของเรา—the Article. บทความควรมีชื่อเรื่องและช่องเนื้อหาตลอดจนวันที่สร้าง 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 และ Article::update models:

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

ฟิลด์ภายในคุณสมบัติ $fillable สามารถกำหนดได้จำนวนมากโดยใช้เมธอด create() และ update() ของ Eloquent คุณยังสามารถใช้คุณสมบัติ $guarded เพื่ออนุญาตคุณสมบัติทั้งหมด ยกเว้นบางส่วน

การเพาะฐานข้อมูล

การเพาะฐานข้อมูลเป็นกระบวนการเติมฐานข้อมูลของเราด้วยข้อมูลจำลองที่เราสามารถใช้ทดสอบได้ Laravel มาพร้อมกับ Faker ซึ่งเป็นไลบรารีที่ยอดเยี่ยมสำหรับการสร้างรูปแบบข้อมูลจำลองที่ถูกต้องสำหรับเรา มาสร้าง seeder แรกของเรากัน:

 $ php artisan make:seeder ArticlesTableSeeder

Seeders จะอยู่ในไดเร็กทอรี /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, ]); } } }

เรามารันคำสั่ง seed กัน:

 $ php artisan db:seed --class=ArticlesTableSeeder

มาทำขั้นตอนซ้ำกันเพื่อสร้าง Users seeder:

 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, ]); } } }

เราสามารถทำให้ง่ายขึ้นโดยการเพิ่ม seeders ของเราไปยังคลาส 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/ และมิดเดิลแวร์การควบคุมปริมาณ API จะถูกนำไปใช้กับเส้นทางเหล่านี้โดยอัตโนมัติ (หากคุณต้องการลบคำนำหน้า คุณสามารถแก้ไขคลาส RouteServiceProvider บน /app/Providers/RouteServiceProvider.php )

ทีนี้มาย้ายโค้ดนี้ไปที่ Controller ของตัวเองกัน:

 $ 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 ที่ถูกต้อง

หากคุณพยายามดึงทรัพยากรที่ไม่มีอยู่จริง คุณจะได้รับข้อยกเว้นและคุณจะได้รับ stacktrace ทั้งหมดดังนี้:

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); }

ในกรณีนี้ คำขอ API จะต้องมีส่วนหัว 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 เป็นภาษาอังกฤษ การสนทนาเกี่ยวกับการตรวจสอบสิทธิ์ 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 เพื่อสนับสนุน API ของเราได้:

 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');

ตอนนี้ สมมติว่ามีการเรียกใช้ seeders นี่คือสิ่งที่เราได้รับเมื่อเราส่งคำขอ 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 Facing

 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 Stacktrace

เนื่องจากเราจำเป็นต้องแก้ไขวิธีการปัจจุบันที่ไม่ผ่านการ unauthenticated สิทธิ์ในคลาส Handler ของเรา เวอร์ชันปัจจุบันส่งคืน JSON เฉพาะเมื่อคำขอมีส่วนหัว Accept: application/json ดังนั้นเรามาเปลี่ยนกัน:

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

เมื่อแก้ไขแล้ว เราสามารถกลับไปที่จุดสิ้นสุดของบทความเพื่อรวมไว้ในมิดเดิลแวร์ auth:api เราสามารถทำได้โดยใช้กลุ่มเส้นทาง:

 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

มีเครื่องมือภายนอกจำนวนหนึ่งที่คุณสามารถใช้เพื่อทดสอบ 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'); } }

สิ่งสุดท้ายที่ฉันชอบทำคือเพิ่มคำสั่ง test ลงใน 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 ให้ดีขึ้นอย่างแน่นอน และความง่ายในการทดสอบด้วยมันทำให้ฉันสนใจในกรอบงานมากขึ้น มันไม่สมบูรณ์แบบ แต่มีความยืดหยุ่นเพียงพอที่จะให้คุณแก้ไขปัญหาได้

หากคุณกำลังออกแบบ API สาธารณะ ลองดู 5 กฎทองสำหรับการออกแบบ API เว็บที่ยอดเยี่ยม

ที่เกี่ยวข้อง: การ ตรวจสอบสิทธิ์ผู้ใช้แบบเต็มและการควบคุมการเข้าถึง – บทช่วยสอน Laravel Passport, Pt. 1