作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
André Castelo
Verified Expert in Engineering
11 Years of Experience

andr Castelo是一个专注于PHP和JavaScript的web开发人员. 他还开发了Laravel应用和api,以及AngularJS应用.

Expertise

Share

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

Laravel PHP框架是如何开发的 PHP developer productivity in mind. Written and maintained by Taylor Otwell, 该框架非常固执己见,并努力通过支持约定而不是配置来节省开发人员的时间. 该框架还旨在随着web的发展而发展,并且已经在web开发领域纳入了一些新的特性和想法,比如作业队列, API身份验证开箱即用, 实时通信, and much more.

Laravel API教程-构建RESTful Web服务

In this tutorial, 我们将探索使用带身份验证的Laravel构建和测试健壮API的方法. 我们将使用Laravel 5.4、所有的代码都可供参考 GitHub.

RESTful APIs

首先,我们需要理解什么是RESTful API. REST stands for 表征状态转移 是一种用于应用程序之间网络通信的体系结构风格, 它依赖于无状态协议(通常是HTTP)进行交互.

HTTP动词表示动作

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

  • GET: retrieve resources
  • POST: create resources
  • PUT: update resources
  • DELETE: delete resources

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

更新动作:PUT vs. POST

RESTful api是一个有很多争议的问题,关于是否最好更新的问题有很多意见 POST, PATCH, or PUT,或者创建操作最好留给 PUT verb. 在本文中我们将使用 PUT 更新动作,根据HTTP RFC, PUT 用于在特定位置创建/更新资源. 另一个要求是 PUT verb is idempotence, 也就是说,你可以发送请求1, 2或1000次,结果将是相同的:数据库中的一个更新资源.

Resources

资源将成为行动的目标, 在我们的例子中是文章和用户, and they have their own endpoints:

  • /articles
  • /users

在这个laravel api教程中, 这些资源在我们的数据模型上将具有1:1的表示形式, 但这不是必要条件. 您可以让资源在多个数据模型中表示(或者根本不在数据库中表示),并且模型完全不受用户的限制. In the end, 您可以决定如何以适合您的应用程序的方式构建资源和模型.

关于一致性的说明

使用一组约定(如REST)的最大优点是,您的API将更容易使用和开发. 有些端点很简单, as a result, 您的API将更容易使用和维护,而不是拥有端点,例如 GET /get_article?id_article=12 and POST /delete_article?number=40. 我以前也构建过类似的糟糕的api,现在我仍然为此痛恨自己.

然而,在某些情况下,很难映射到创建/检索/更新/删除模式. 请记住,url不应该包含动词,资源也不一定是表中的行. 另一件要记住的事情是,你不必为每个资源执行每个操作.

Setting Up a Laravel Web Service Project

与所有现代PHP框架一样,我们需要 Composer to install and handle our dependencies. 在遵循下载说明(并将其添加到路径环境变量中)之后, install Laravel using the command:

$ composer global require laravel/installer

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

$ laravel new myapp

For the above command, you need to have ~ /作曲家/供应商/ bin in your $PATH. 如果你不想处理这个问题,你也可以使用Composer创建一个新项目:

laravel/laravel myapp

安装了Laravel后,你应该能够启动服务器并测试是否一切正常:

$ php artisan serve
Laravel development server started: 

当您在浏览器上打开localhost:8000时,您应该会看到Laravel示例页面

When you open localhost:8000 在浏览器上,您应该看到这个示例页面.

迁移和模型

在实际编写第一个迁移之前, 确保为该应用程序创建了一个数据库,并将其凭据添加到 .env file located in the root of the project.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE =家园
DB_USERNAME =家园
DB_PASSWORD=secret

你也可以使用Homestead, a Vagrant box specially crafted for Laravel, 但这超出了本文的讨论范围. 如果你想知道更多, refer to the Homestead documentation.

让我们开始我们的第一个模型和迁移—文章. 文章应该有标题和正文字段,以及创建日期. Laravel通过Artisan-Laravel的命令行工具提供了几个命令,帮助我们生成文件并将它们放入正确的文件夹中. To create the Article model, we can run:

$ php artisan make:model Article -m

The -m option is short for --migration 它告诉Artisan为我们的模型创建一个. 下面是生成的迁移:

increments('id');
            $table->timestamps();
        });
    }

    /**
     *反向迁移.
     *
     * @return void
     */
    公共函数down()
    {
        模式:dropIfExists(“文章”);
    }
}

让我们仔细分析一下:

  • The up() and down() 方法将分别在迁移和回滚时运行;
  • $table->increments('id') 用名称设置一个自动递增的整数 id;
  • $table->timestamps() will set up the timestamps for us—created_at and updated_at,但不用担心设置默认值,Laravel会在需要时更新这些字段.
  • And finally, 模式:dropIfExists () 当然,如果桌子存在,你会删除它吗.

解决了这个问题,让我们在 up() method:

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

The string() method creates a VARCHAR 等效列while text() creates a TEXTequivalent. With that done, let’s go ahead and migrate:

$ PHP工匠迁移

You can also use the --step option here, 它将把每个迁移分离到自己的批处理中,以便您可以在需要时单独回滚它们.

Laravel开箱即用有两种迁移方式, create_users_table and create_password_resets_table. 我们不会用 password_resets table, but having the users table ready for us will be helpful.

现在让我们回到模型并将这些属性添加到 $fillable field so that we can use them in our Article::create and Article::update models:

类Article扩展Model
{
    protected $fillable = ['title', 'body'];
}

Fields inside the $fillable 属性可以使用Eloquent’s批量分配 create() and update() methods. You can also use the $guarded 属性,允许除少数属性外的所有属性.

Database Seeding

数据库播种是用可用于测试的虚拟数据填充数据库的过程. Laravel comes with Faker,一个为我们生成正确格式的虚拟数据的伟大库. 让我们创建第一个种子器:

$ php工匠制作:种子ArticlesTableSeeder

The seeders will be located in the /database/seeds directory. 下面是我们创建几篇文章后的样子:

class ArticlesTableSeeder extends Seeder
{
    公共函数run()
    {
        //让我们截断现有的记录,从头开始.
        Article::truncate();

        $faker = \Faker\Factory::create();

        //现在,让我们在数据库中创建一些文章:
        for ($i = 0; $i < 50; $i++) {
            Article::create([
                'title' => $faker->sentence,
                'body' => $faker->paragraph,
            ]);
        }
    }
}

那么让我们运行seed命令:

$ PHP工匠db:种子——class=ArticlesTableSeeder

让我们重复这个过程来创建一个Users种子:

class UsersTableSeeder extends Seeder
{
    公共函数run()
    {
        // Let's clear the users table first
        User::truncate();

        $faker = \Faker\Factory::create();

        //让我们确保每个人都有相同的密码和 
        //在循环之前散列它,否则就是我们的种子 
        // will be too slow.
        $password = Hash::make(' total ');

        User::create([
            'name' => 'Administrator',
            'email' => 'admin@test.com',
            'password' => $password,
        ]);

        //现在让我们为我们的应用程序生成几十个用户:
        for ($i = 0; $i < 10; $i++) {
            User::create([
                'name' => $faker->name,
                'email' => $faker->email,
                'password' => $password,
            ]);
        }
    }
}

我们可以通过将我们的播种器添加到主程序中来简化它 DatabaseSeeder class inside the database/seeds folder:

class DatabaseSeeder extends Seeder
{
    公共函数run()
    {
        $this->call(ArticlesTableSeeder::class);
        $this->call(UsersTableSeeder::class);
    }
}

这样,我们就可以简单地跑了 $ PHP工匠db:种子 它会运行所有被调用的类 run() method.

路由和控制器

让我们为应用程序创建基本端点:create, retrieve the list, 检索单个数据, update, and delete. On the routes/api.php 文件,我们可以简单地这样做:

Use App\Article;
 
Route::get('articles', function() {
    //如果Content-Type和Accept头文件设置为'application/json', 
    // this will return a JSON structure. 稍后将进行清理.
    返回文章::所有();
});
 
Route::get('articles/{id}', function($id) {
    返回文章::找($ id);
});

post('articles', function(Request $ Request) {
    return Article::create($request->all);
});

路由::put('articles/{id}', function(Request $ Request, $id) {
    $article = Article::findOrFail($id);
    $article->update($request->all());

    return $article;
});

删除('文章/{id}',函数($id) {
    Article::find($id)->delete();

    return 204;
})

The routes inside api.php Will的前缀是 /api/ 并且API节流中间件将自动应用于这些路由(如果您想删除前缀,可以编辑 RouteServiceProvider class on /app/Providers/RouteServiceProvider.php).

现在让我们将这段代码移动到它自己的控制器:

$ php artisan make:controller ArticleController

ArticleController.php:

use App\Article;
 
class ArticleController extends Controller
{
    公共功能索引()
    {
        返回文章::所有();
    }
 
    公共函数show($id)
    {
        返回文章::找($ id);
    }

    public function store(Request $request)
    {
        return Article::create($request->all());
    }

    公共函数更新(请求$ Request, $id)
    {
        $article = Article::findOrFail($id);
        $article->update($request->all());

        return $article;
    }

    公共函数删除(请求$ Request, $id)
    {
        $article = Article::findOrFail($id);
        $article->delete();

        return 204;
    }
}

The routes/api.php file:

路线:获得(“文章”、“ArticleController@index”);
路线:get(文章/ {id},“ArticleController@show”);
路线:文章(“文章”、“ArticleController@store”);
路线::把(文章/ {id},“ArticleController@update”);
路线::删除(文章/ {id},“ArticleController@delete”);

我们可以通过使用隐式路由模型绑定来改进端点. 通过这种方式,Laravel将注入 Article 实例,如果没有找到,则自动返回404. 我们必须对路由文件和控制器进行修改:

路线:获得(“文章”、“ArticleController@index”);
路线:get(“文章/{}条”,“ArticleController@show”);
路线:文章(“文章”、“ArticleController@store”);
路线::把(“文章/{}条”,“ArticleController@update”);
路线::删除(“文章/{}条”,“ArticleController@delete”);
class ArticleController extends Controller
{
    公共功能索引()
    {
        返回文章::所有();
    }

    public function show(Article $article)
    {
        return $article;
    }

    public function store(Request $request)
    {
        $article = Article::create($request->all());

        return response()->json($article, 201);
    }

    公共函数更新(请求$ Request,文章$ Article)
    {
        $article->update($request->all());

        return response()->json($article, 200);
    }

    public function delete(Article $article)
    {
        $article->delete();

        return response()->json(null, 204);
    }
}

HTTP状态码和响应格式说明

We’ve also added the response()->json() 呼叫我们的端点. 这使我们可以显式地返回JSON数据,并发送可由客户端解析的HTTP代码. 你将返回的最常见代码是:

  • 200: OK. 标准成功代码和默认选项.
  • 201: Object created. Useful for the store actions.
  • 204: No content. 当一个操作被成功执行,但没有内容返回时.
  • 206: Partial content. 当您必须返回资源的分页列表时非常有用.
  • 400: Bad request. 未能通过验证的请求的标准选项.
  • 401: Unauthorized. The user needs to be authenticated.
  • 403: Forbidden. 用户已通过身份验证,但没有执行操作的权限.
  • 404: Not found. 当没有找到资源时,Laravel会自动返回.
  • 500:服务器内部错误. 理想情况下,你不会显式返回这个, but if something unexpected breaks, this is what your user is going to receive.
  • 503:服务不可用. 不言自明, 还有另一段代码不会被应用程序显式返回.

发送正确的404响应

如果您试图获取不存在的资源, 你会被抛出一个异常,你会收到整个堆栈跟踪, like this:

NotFoundHttpException加亮

我们可以通过编辑异常处理程序类来修复这个问题 应用程序/异常处理程序.php,返回JSON响应:

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

    返回parent::render($request, $exception);
}

下面是一个返回的例子:

{
    数据:"资源未找到"
}

如果您使用Laravel来提供其他页面,则必须编辑代码以使用 Accept 否则,来自常规请求的404错误也将返回JSON.

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

    返回parent::render($request, $exception);
}

在这种情况下,API请求将需要标头 接受:application / json.

Authentication

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

To get started, we’ll need to add an api_token field to the users table:

——table=users adds_api_token_to_users_table

然后实现迁移:

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

公共函数down()
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropColumn(['api_token']);
    });
}

After that, just run the migration using:

$ PHP工匠迁移

创建注册端点

我们将利用 RegisterController (in the Auth 文件夹),以便在注册时返回正确的响应. Laravel自带开箱即用的身份验证功能, 但是我们仍然需要稍微调整它来返回我们想要的响应.

如果api是英文的,这就是api身份验证对话听起来的样子

The controller makes use of the trait RegistersUsers 实现注册. Here’s how it works:

public function register(Request $request)
{
    //请求在这里被验证. 已定位验证器方法
    //在RegisterController内部,并确保name, email
    // password和password_confirmation字段是必需的.
    $this->validator($request->all())->validate();

    //一个已注册的事件被创建,并将触发任何相关的
    //观察员,如发送确认电子邮件或任何 
    //用户创建后需要立即运行的代码.
    event(new Registered($user = $this->create($request->all())));

    //用户创建后,他已经登录.
    $this->guard()->login($user);

    //最后,这是我们想要的钩子. If there is no
    // registered()方法或返回null,将其重定向到
    // some other URL. In our case, we just need to implement
    //该方法返回正确的响应.
    return $this->registered($request, $user)
                    ?: redirect($this->redirectPath());
}

我们只需要实现 registered() method in our RegisterController. 该方法接收 $request and the $user这就是我们想要的. 下面是该方法在控制器中的样子:

保护函数注册(请求$ Request, $user)
{
    $user->generateToken();

    return response()->json(['data' => $user->toArray()], 201);
}

And we can link it on the routes file:

路线::文章(“注册”,“身份验证\ RegisterController@register”);

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

class User extends Authenticatable
{
    ...
    公共函数generateToken()
    {
        $this->api_token = str_random(60);
        $this->save();

        return $this->api_token;
    }
}

And that’s it. 用户现在已经注册了,感谢Laravel的验证和开箱即用的身份验证 name, email, password, and password_confirmation 字段是必需的,反馈是自动处理的. Checkout the validator() method inside the RegisterController to see how the rules are implemented.

下面是我们到达端点时得到的结果:

$ curl -X POST http://localhost:8000/api/register \
 -H "接受:application / json" \
 -H "Content-Type: application/json" \
 -d '{"name": "John", "email": "john.doe@toptal.Com ", "password": "toptal123", "password_confirmation": "toptal123"} "
{
    "data": {
        “api_token”:“0 syhnl0y9joifszq11ec2cbqwcfobmvscrzyo5o2ilzpnohvndh797ndnyat”,
        "created_at": "2017-06-20 21:17:15",
        "email": "john.doe@joyerianicaragua.com",
        "id": 51,
        "name": "John",
        "updated_at": "2017-06-20 21:17:15"
    }
}

创建登录端点

就像注册端点一样,我们可以编辑 LoginController (in the Auth folder) to support our API authentication. The login method of the AuthenticatesUsers trait可以被重写以支持我们的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);
}

And we can link it on the routes file:

路线::文章(“登录”、“身份验证\ LoginController@login”);

现在,假设播种机已经运行,这是我们发送a时得到的结果 POST 请求该航线:

$ curl -X POST localhost:8000/api/login \
  -H "接受:application / json" \
  -H "Content-type: application/json" \
  -d "{\"email\": \"admin@test.com\", \"password\": \"toptal\" }"
{
    "data": {
        "id":1,
        “名称”:“管理员”,
        "email":"admin@test.com",
        "created_at":"2017-04-25 01:05:34",
        "updated_at":"2017-04-25 02:50:40",
        :“api_token Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw”
    }
}

要在请求中发送令牌,可以通过发送属性来实现 api_token 在有效负载中或以承载令牌的形式在请求头中 授权:Bearer Jll7q0BSijLOrzaOSm5Dr5hW9cJRZAJKOzvDlxjKCXepwAeZ7JR6YP5zQqnw.

Logging Out

按照我们目前的策略, 如果标记错误或丢失, 用户应该收到一个未经身份验证的响应(我们将在下一节中实现). 因此,对于一个简单的注销端点,我们将发送令牌,它将从数据库中删除.

routes/api.php:

路线::文章(“注销”,“身份验证\ 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);
}

Using this strategy, whatever token the user has will be invalid, API将拒绝访问(使用中间件), 如下一节所述). 这需要与前端协调,以避免用户在没有访问任何内容的情况下保持登录状态.

Using Middlewares to Restrict Access

With the api_token 创建后,我们可以在路由文件中切换身份验证中间件:

路线::中间件(身份验证:api)
    ->get('/user', function (Request $request) {
        return $request->user();
    });

We can access the current user using the $request->user() 方法或通过Auth facade

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

我们得到这样的结果:

An InvalidArgumentException Stacktrace

This is because we need to edit the current unauthenticated 方法. 当前版本仅在请求具有 接受:application / json 头,让我们改变它:

受保护的函数未认证($request, AuthenticationException $exception)
{
    return response()->json(['error' => 'Unauthenticated'], 401);
}

解决了这个问题后,我们可以返回到文章端点,将它们包装在 auth:api middleware. We can do that by using route groups:

Route::group(['middleware' => 'auth:api'], function() {
    路线:获得(“文章”、“ArticleController@index”);
    路线:get(“文章/{}条”,“ArticleController@show”);
    路线:文章(“文章”、“ArticleController@store”);
    路线::把(“文章/{}条”,“ArticleController@update”);
    路线::删除(“文章/{}条”,“ArticleController@delete”);
});

这样我们就不必为每条路由设置中间件了. 它现在不会节省很多时间,但随着项目的发展,它有助于保持路线DRY.

Testing Our Endpoints

Laravel包含了与PHPUnit的开箱即用的集成 phpunit.xml already set up. 该框架还为我们提供了几个帮助器和额外的断言,使我们的工作更加轻松, 特别是用于测试api.

There are a number of external tools you can use to test your API; however, 在Laravel内部测试是一个更好的选择——我们可以在保留对数据库的完全控制的同时,获得测试API结构和结果的所有好处. For the list endpoint, for example, 我们可以运行几个工厂,并断言响应包含这些资源.

在开始之前,我们需要调整一些设置来使用内存中的SQLite数据库. 使用它可以让我们的测试快速进行, 但是代价是一些迁移命令(约束), (例如)将无法在该特定设置中正常工作. 我建议在测试中远离SQLite,当你开始得到迁移错误,或者如果你更喜欢一组更强大的测试而不是性能运行.

我们还将在每次测试之前运行迁移. 这种设置将允许我们为每个测试构建数据库,然后销毁它, 避免测试之间任何类型的依赖.

In our config/database.php 文件,我们需要设置 database field in the sqlite configuration to :memory::

...
'connections' => [

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

然后启用SQLite in phpunit.xml by adding the environment variable DB_CONNECTION:

    
        
        
        
        
        
    

解决了这些问题,剩下的就是配置我们的基地了 TestCase 类来使用迁移,并在每次测试之前为数据库播种. 为此,我们需要添加 DatabaseMigrations 性状,然后加一个 Artisan call on our setUp() method. Here’s the class after the changes:

使用说明\ \测试\ DatabaseMigrations基础;
使用Illuminate\Foundation\Testing\TestCase作为BaseTestCase;
use Illuminate\Support\Facades\Artisan;

抽象类TestCase继承了BaseTestCase
{
    use CreatesApplication, DatabaseMigrations;

    公共函数setUp()
    {
        parent::setUp();
        艺人::电话(db:种子);
    }
}

我喜欢做的最后一件事是将test命令添加到 composer.json:

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

test命令将像这样可用:

$ composer test

Setting Up Factories for Our Tests

工厂将允许我们使用正确的测试数据快速创建对象. 它们位于 database/factories folder. Laravel从盒子里出来,有一个工厂 User 类,我们添加一个 Article class:

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

The Faker 库已经被注入,以帮助我们为模型创建正确格式的随机数据.

Our First Tests

我们可以使用Laravel的assert方法轻松地命中端点并评估其响应. 让我们使用以下命令创建第一个测试,即登录测试:

$ php artisan make:test Feature/LoginTest

And here is our test:

类LoginTest扩展TestCase
{
    public function testRequiresEmailAndLogin()
    {
        $this->json('POST', 'api/login')
            ->assertStatus(422)
            ->assertJson([
                'email' => ['The email field is required.'],
                'password' => ['The password field is required.'],
            ]);
    }


    公共函数testUserLoginsSuccessfully()
    {
        $user = factory(User::class)->create([
            'email' => 'testlogin@user.com',
            'password' => bcrypt('toptal123'),
        ]);

        $payload = ['email' => 'testlogin@user.com', 'password' => 'toptal123'];

        $this->json('POST', 'api/login', $payload)
            ->assertStatus(200)
            ->assertJsonStructure([
                'data' => [
                    'id',
                    'name',
                    'email',
                    'created_at',
                    'updated_at',
                    'api_token',
                ],
            ]);

    }
}

这些方法测试了几个简单的案例. The json() 方法命中端点,而其他断言则不言自明. One detail about assertJson():此方法将响应转换为数组,并搜索参数, 所以顺序很重要. 你可以把多个 assertJson() calls in that case.

现在,让我们创建注册端点测试,并为该端点编写一对代码:

$ php artisan make:test RegisterTest
class RegisterTest extends TestCase
{
    公共函数testsRegistersSuccessfully()
    {
        $payload = [
            'name' => 'John',
            'email' => 'john@joyerianicaragua.com',
            'password' => 'toptal123',
            'password_confirmation' => 'toptal123',
        ];

        $this->json('post', '/api/register', $payload)
            ->assertStatus(201)
            ->assertJsonStructure([
                'data' => [
                    'id',
                    'name',
                    'email',
                    'created_at',
                    'updated_at',
                    'api_token',
                ],
            ]);;
    }

    公共函数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.'],
            ]);
    }

    公共函数testsRequirePasswordConfirmation()
    {
        $payload = [
            'name' => 'John',
            'email' => 'john@joyerianicaragua.com',
            '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
{
    公共函数testuserisloggedoutproper ()
    {
        $user = factory(User::class)->create(['email' => 'user@test.com']);
        $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' => 'user@test.com']);
        $token = $user->generateToken();
        $headers = ['Authorization' => "Bearer $token"];

        // Simulating logout
        $user->api_token = null;
        $user->save();

        $this->json('get', '/api/articles', [], $headers)->assertStatus(401);
    }
}

注意这一点很重要, during testing, Laravel应用程序不会在新请求时再次实例化. 这意味着当我们访问身份验证中间件时,它将当前用户保存在 TokenGuard 实例,以避免再次访问数据库. A wise choice, however—in this case, 这意味着我们必须将注销测试分成两个部分, 以避免先前缓存的用户出现任何问题.

测试Article端点也很简单:

class ArticleTest extends TestCase
{
    公共函数testsarticlesaereatedcorrect ()
    {
        $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']);
    }

    公共函数testsarticlesareupdatedcorrect ()
    {
        $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' 
            ]);
    }

    公共函数testsartisaredeletedcorrect ()
    {
        $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);
    }

    公共函数testarticlesarelistdcorrect ()
    {
        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'],
            ]);
    }

}

Next Steps

这就是它的全部. 当然还有改进的空间——您可以使用 Passport 包,集成分页和转换层(我推荐) Fractal), 这个列表还在继续,但我想通过在Laravel中创建和测试API的基础知识,而不使用外部包.

Laravel development has certainly improved my experience with PHP 使用它进行测试的便利性巩固了我对该框架的兴趣. 它不是完美的,但它足够灵活,可以让你解决它的问题.

如果您正在设计公共API,请查看 5 Golden Rules for Great Web API Design.

了解基本知识

  • What is Laravel?

    Laravel is an opinionated PHP framework. 它抽象了构建web应用程序的细节,以提高生产率, maintenance, 以及前向兼容性.

  • What is REST?

    表征状态转移 (REST)和RESTful web服务代表了应用程序之间的一种网络通信风格,通过无状态协议(如HTTP)传输应用程序状态。.

  • What is the difference between JSON and XML?

    JSON and XML are textual data formats. JSON (JavaScript对象表示法)使用JavaScript语法来表示数据并使其可解析,而XML(可扩展标记语言)使用标记标记和嵌套来实现相同的功能.

  • What is Composer?

    Composer是一个用于PHP的包管理器,它在应用程序级别管理软件依赖项.

Hire a Toptal expert on this topic.
Hire Now
André Castelo

André Castelo

Verified Expert in Engineering
11 Years of Experience

jo o Pessoa, Paraíba -巴西Paraíba州

2016年8月29日成为会员

About the author

andr Castelo是一个专注于PHP和JavaScript的web开发人员. 他还开发了Laravel应用和api,以及AngularJS应用.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

World-class articles, delivered weekly.

输入您的电子邮件,即表示您同意我们的 privacy policy.

World-class articles, delivered weekly.

输入您的电子邮件,即表示您同意我们的 privacy policy.

Toptal Developers

Join the Toptal® community.