บทช่วยสอน Laravel API: วิธีสร้างและทดสอบ RESTful API
เผยแพร่แล้ว: 2022-03-11ด้วยการพัฒนามือถือและเฟรมเวิร์ก JavaScript ที่เพิ่มขึ้น การใช้ RESTful API เป็นตัวเลือกที่ดีที่สุดในการสร้างอินเทอร์เฟซเดียวระหว่างข้อมูลและไคลเอนต์ของคุณ
Laravel เป็นเฟรมเวิร์ก PHP ที่พัฒนาขึ้นโดยคำนึงถึงประสิทธิภาพการทำงานของนักพัฒนา PHP เขียนและดูแลโดย Taylor Otwell เฟรมเวิร์กมีความเห็นอย่างมากและมุ่งมั่นที่จะประหยัดเวลาของนักพัฒนาด้วยการเลือกแบบแผนมากกว่าการกำหนดค่า กรอบงานนี้มีจุดมุ่งหมายเพื่อพัฒนาไปพร้อมกับเว็บ และได้รวมเอาคุณลักษณะและแนวคิดใหม่ๆ หลายอย่างไว้ในโลกของการพัฒนาเว็บแล้ว เช่น คิวงาน การตรวจสอบสิทธิ์ API ที่พร้อมใช้งานทันที การสื่อสารแบบเรียลไทม์ และอื่นๆ อีกมากมาย
ในบทช่วยสอนนี้ เราจะสำรวจวิธีที่คุณสามารถสร้างและทดสอบ 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
: ลบทรัพยากร
อัปเดตการดำเนินการ: 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
บนเบราว์เซอร์ของคุณ คุณควรเห็นหน้าตัวอย่างนี้การย้ายถิ่นฐานและแบบจำลอง
ก่อนที่จะเขียนการโยกย้ายครั้งแรกของคุณจริง ๆ ตรวจสอบให้แน่ใจว่าคุณได้สร้างฐานข้อมูลสำหรับแอปนี้และเพิ่มข้อมูลรับรองในไฟล์ .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 ทั้งหมดดังนี้:
เราสามารถแก้ไขได้โดยแก้ไขคลาสตัวจัดการข้อยกเว้นซึ่งอยู่ใน 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 มาพร้อมกับการรับรองความถูกต้องตั้งแต่เริ่มต้น แต่เรายังต้องปรับแต่งเล็กน้อยเพื่อส่งคืนการตอบกลับที่เราต้องการ
ผู้ควบคุมใช้ประโยชน์จากลักษณะ 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
และเราได้ผลลัพธ์ดังนี้:
เนื่องจากเราจำเป็นต้องแก้ไขวิธีการปัจจุบันที่ไม่ผ่านการ 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 เว็บที่ยอดเยี่ยม