Laravel API 教程:如何构建和测试 RESTful API

已发表: 2022-03-11

随着移动开发和 JavaScript 框架的兴起,使用 RESTful API 是在数据和客户端之间构建单一接口的最佳选择。

Laravel 是一个 PHP 框架,开发时考虑了 PHP 开发人员的生产力。 该框架由 Taylor Otwell 编写和维护,非常固执己见,并通过支持约定而不是配置来努力节省开发人员的时间。 该框架还旨在与 Web 一起发展,并且已经在 Web 开发世界中融入了一些新功能和想法——例如作业队列、开箱即用的 API 身份验证、实时通信等等。

Laravel API 教程 - 构建 RESTful Web 服务

在本教程中,我们将探索使用 Laravel 和身份验证构建和测试强大 API 的方法。 我们将使用 Laravel 5.4,所有代码都可以在 GitHub 上参考。

RESTful API

首先,我们需要了解究竟什么是 RESTful API。 REST 代表REpresentational State Transfer ,是一种应用程序之间网络通信的架构风格,它依赖于无状态协议(通常是 HTTP)进行交互。

HTTP 动词表示动作

在 RESTful API 中,我们使用 HTTP 动词作为操作,端点是所作用的资源。 我们将使用 HTTP 动词的语义含义:

  • GET : 检索资源
  • POST : 创建资源
  • PUT :更新资源
  • DELETE : 删除资源

HTTP 动词:GET、POST、PUT 和 DELETE 是 RESTful API 中的操作

更新操作:PUT 与 POST

RESTful API 是一个有很多争论的问题,关于是否最好使用POSTPATCHPUT进行更新,或者是否最好将创建操作留给PUT动词,有很多意见。 在本文中,我们将使用PUT进行更新操作,根据 HTTP RFC, PUT意味着在特定位置创建/更新资源。 PUT动词的另一个要求是幂等性,在这种情况下,这基本上意味着您可以发送该请求 1、2 或 1000 次,结果将是相同的:数据库中的一个更新资源。

资源

资源将是操作的目标,在我们的例子中是文章和用户,它们有自己的端点:

  • /articles
  • /users

在这个 laravel api 教程中,资源将在我们的数据模型上以 1:1 表示,但这不是必需的。 您可以在多个数据模型中表示资源(或在数据库中根本不表示),并且模型完全不受用户限制。 最后,您将决定如何以适合您的应用程序的方式构建资源和模型。

关于一致性的说明

使用一组约定(如 REST)的最大优势是您的 API 将更容易使用和开发。 一些端点非常简单,因此,与使用GET /get_article?id_article=12POST /delete_article?number=40等端点相比,您的 API 将更易于使用和维护。 我过去曾构建过这样糟糕的 API,但我仍然为此痛恨自己。

但是,在某些情况下,很难映射到 Create/Retrieve/Update/Delete 模式。 请记住,URL 不应包含动词,并且资源不一定是表中的行。 要记住的另一件事是,您不必为每个资源实施每个操作。

设置 Laravel Web 服务项目

与所有现代 PHP 框架一样,我们需要 Composer 来安装和处理我们的依赖项。 按照下载说明(并添加到路径环境变量)后,使用以下命令安装 Laravel:

 $ composer global require laravel/installer

安装完成后,您可以像这样构建一个新应用程序:

 $ laravel new myapp

对于上述命令,您需要在$PATH中有~/composer/vendor/bin 。 如果你不想处理这个问题,你也可以使用 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,一个专为 Laravel 设计的 Vagrant 盒子,但这有点超出了本文的范围。 如果您想了解更多信息,请参阅 Homestead 文档。

让我们开始我们的第一个模型和迁移——文章。 文章应该有一个标题和一个正文字段,以及一个创建日期。 Laravel 通过 Artisan(Laravel 的命令行工具)提供了几个命令,它们通过生成文件并将它们放在正确的文件夹中来帮助我们。 要创建 Article 模型,我们可以运行:

 $ 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_atupdated_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_tablecreate_password_resets_table 。 我们不会使用password_resets表,但是为我们准备好users表会很有帮助。

现在让我们回到我们的模型并将这些属性添加到$fillable字段,以便我们可以在Article::createArticle::update模型中使用它们:

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

$fillable属性中的字段可以使用 Eloquent 的create()update()方法进行批量分配。 您还可以使用$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, ]); } } }

我们可以通过将播种器添加到database/seeds文件夹中的主DatabaseSeeder类来使其更容易:

 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 限制中间件将自动应用于这些路由(如果要删除前缀,可以编辑/app/Providers/RouteServiceProvider.php上的RouteServiceProvider类)。

现在让我们把这段代码移到它自己的 Controller 中:

 $ php artisan make:controller 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 堆栈跟踪

我们可以通过编辑位于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

验证

在 Laravel 中实现 API 身份验证的方法有很多(其中之一是 Passport,这是实现 OAuth2 的好方法),但在本文中,我们将采用一种非常简化的方法。

首先,我们需要在users表中添加一个api_token字段:

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

我们只需要在RegisterController中实现registered()方法。 该方法接收$request$user ,这就是我们想要的。 以下是该方法在控制器内部的外观:

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

我们可以将它链接到路由文件中:

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

在上面的部分中,我们使用了 User 模型上的方法来生成令牌。 这很有用,因此我们只有一种生成令牌的方法。 将以下方法添加到您的用户模型中:

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

就是这样。 用户现在已经注册,并且由于 Laravel 的验证和开箱即用的身份验证, nameemailpasswordpassword_confirmation字段是必需的,并且反馈会自动处理。 检查RegisterController中的validator()方法以查看规则是如何实现的。

这是我们到达那个端点时得到的结果:

 $ 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 身份验证。 AuthenticatesUsers trait 的login方法可以被覆盖以支持我们的 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');

现在,假设播种机已经运行,当我们向该路由发送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 堆栈跟踪

这是因为我们需要在 Handler 类上编辑当前unauthenticated身份验证的方法。 当前版本仅在请求具有Accept: application/json标头时才返回 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文件中,我们需要将sqlite配置中的database字段设置为:memory:

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

然后通过添加环境变量DB_CONNECTIONphpunit.xml中启用 SQLite:

 <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特征,然后在我们的setUp()方法上添加Artisan调用。 这是更改后的课程:

 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 的 assert 方法轻松命中端点并评估其响应。 让我们使用以下命令创建我们的第一个测试,即登录测试:

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

下一步

这里的所有都是它的。 肯定有改进的空间——你可以使用 Passport 包实现 OAuth2,集成分页和转换层(我推荐 Fractal),不胜枚举——但我想了解在 Laravel 中创建和测试 API 的基础知识外部包。

Laravel 开发无疑改善了我使用 PHP 的体验,并且使用它进行测试的便利性巩固了我对该框架的兴趣。 它并不完美,但它足够灵活,可以让您解决它的问题。

如果您正在设计公共 API,请查看 5 条优秀 Web API 设计的黄金法则。

相关:完整的用户身份验证和访问控制 - Laravel Passport 教程,Pt。 1