手把手建立 Laravel api

本範例分成幾個部份來實作 Laravel API:

  • 安裝 Dingo api
  • 安裝 tymon/jwt-auth
  • 安裝 laravel 5 repository
  • 安裝 barryvdh/laravel-cors
  • 建立 RESTful api 範例
  • 坑點

環境

  • 安裝 laravel
    本範例使用版本為 5.7.28



安裝 Dingo api

安裝

在 composer.json 裡加入

1
2
3
4
5
"require": {
"dingo/api": "2.0.0-alpha1"
},
"minimum-stability": "dev",
"prefer-stable": true

然後執行

1
composer update

產生 api configuration file

1
php artisan vendor:publish --provider="Dingo\Api\Provider\LaravelServiceProvider"

以上指令面會產生一個config/api.php,之後可以在這個檔案設定關於 api 的參數

設定.env

參數內容可視需求調整

1
2
3
4
5
API_STANDARDS_TREE=vnd
API_VERSION=v1
API_DEBUG=true
API_PREFIX=api
API_DOMAIN=api.myapp.com

config/app.php 中注册 Service Provider

Laravel 5.4 以下加入

1
2
3
4
5
6
'providers' => [
/**
* Dingo api
*/
Dingo\Api\Provider\LaravelServiceProvider::class
]




安裝 tymon/jwt-auth

安裝

1
composer require tymon/jwt-auth 1.0.*

產生 jwt-auth configuration file

1
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

config/app.php 中注册 Service Provider

Laravel 5.4 以下加入

1
2
3
4
5
6
'providers' => [
/**
* Tymon/jwt-auth
*/
Tymon\JWTAuth\Providers\LaravelServiceProvider::class
]

產生 jwt secret key

1
php artisan jwt:secret

設定 api 的 auth 方式為 jwt

在 config/api.php 加上

1
2
3
'auth' => [
'jwt' => Dingo\Api\Auth\Provider\JWT::class
]

在 config/app.php 加上

1
2
3
'aliases' => [
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class
]




安裝 laravel 5 repository

安裝

1
composer require prettus/l5-repository

產生 repository configuration file

1
php artisan vendor:publish --provider "Prettus\Repository\Providers\RepositoryServiceProvider"

config/repository.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'generator' => [
'basePath' => app()->path(),
'rootNamespace' => 'App\\',
'stubsOverridePath' => app()->path(),
'paths' => [
'models' => 'Entities',
'repositories' => 'Repositories',
'interfaces' => 'Repositories',
'transformers' => 'Transformers',
'presenters' => 'Presenters',
'validators' => 'Validators',
'controllers' => 'Http/Controllers/Api',//api controller 專用路徑
'provider' => 'RepositoryServiceProvider',
'criteria' => 'Criteria'
]
]

config/app.php

1
2
3
4
'providers' => [
...
App\Providers\RepositoryServiceProvider::class
]




安裝 barryvdh/laravel-cors

安裝

1
composer require barryvdh/laravel-cors

產生 CORS configuration file

1
php artisan vendor:publish --provider="Barryvdh\Cors\ServiceProvider"

config/api.php 加入 CORS middleware

1
2
3
'middleware' => [
Barryvdh\Cors\HandleCors::class
]




建立 RESTful api

基本準備

1
php artisan make:entity user

app/User.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
namespace App\Entities;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
use Notifiable;

protected $hidden = [
'password', 'remember_token',
];

protected $fillable = ['name','email','password'];

/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}

/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}

}

app/Http/Controllers/Api/UsersController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace App\Http\Controllers\Api;

use Dingo\Api\Routing\Helpers;
use Illuminate\Routing\Controller;

class UsersController extends Controller
{
use Helpers;

public function __construct()
{
$this->middleware('api.auth');
}

public function index(){

$user = $this->auth->user();

return $user;
}
}

app/laravel-api/app/Http/Requests/UserCreateRequest.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UserCreateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email|unique:users',
'password' => 'required'
];
}
}

app/Validators/UserValidator.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace App\Validators;

use \Prettus\Validator\Contracts\ValidatorInterface;
use \Prettus\Validator\LaravelValidator;

/**
* Class UserValidator.
*
* @package namespace App\Validators;
*/
class UserValidator extends LaravelValidator
{
/**
* Validation Rules
*
* @var array
*/
protected $rules = [
ValidatorInterface::RULE_CREATE => [
'email' => 'required|email',
'password' => 'required'
],
ValidatorInterface::RULE_UPDATE => [],
];
}

app/Http/Controllers/Api/Auth/RegisterController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
namespace App\Http\Controllers\Api\Auth;

use App\Http\Requests\UserCreateRequest;
use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use Prettus\Validator\Exceptions\ValidatorException;
use \Throwable;

class RegisterController extends Controller
{

/**
* Register new user
*
* @param UserCreateRequest
* @param UserRepository
* @return \Symfony\Component\HttpFoundation\Response
*/
public function register(UserCreateRequest $request,UserRepository $repository)
{
try{

$user = $repository->create([
'name' => $request->input('name'),
'email' => $request->input('email'),
'password' =>app('hash')->make($request->input('password'))
]);

return response()->json([
'message' => 'Success',
'data' => $user
]);

} catch (ValidatorException $e) {

return response()->json(['error' => 'validation exception'], 500);

} catch (Throwable $e) {

return response()->json(['errors' => 'Error'], 500);
}

}
}

app/Http/Controllers/Api/Auth/LoginController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
namespace App\Http\Controllers\Api\Auth;

use App\Http\Requests\UserLoginRequest;
use Illuminate\Http\Response;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use App\Http\Controllers\Controller;

class LoginController extends Controller
{
/**
* Handle a login request to the application.
*
* @param App\Http\Requests\UserLoginRequest
* @return \Symfony\Component\HttpFoundation\Response
*/
public function login(UserLoginRequest $request)
{
/**
* Verify the credentials and create a token for the user
*/
try {
if (!$token = JWTAuth::attempt(
$this->getCredentials($request)
)) {
return $this->onUnauthorized();
}
} catch (JWTException $e) {

return $this->onJwtGenerationError();
}

return $this->onAuthorized($token);
}

/**
* Unauthorized
*
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function onUnauthorized()
{
return response()->json([
'message' => 'invalid_credentials'
], Response::HTTP_UNAUTHORIZED);
}

/**
* Can not generate a token
*
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function onJwtGenerationError()
{
return response()->json([
'message' => 'could_not_create_token'
], Response::HTTP_INTERNAL_SERVER_ERROR);
}

/**
* Authorized
*
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function onAuthorized($token)
{
return response()->json([
'message' => 'token_generated',
'data' => [
'token' => $token,
]
]);
}

/**
* Get the needed authorization credentials from the request
*
* @param App\Http\Requests\UserLoginRequest
* @return array
*/
protected function getCredentials(UserLoginRequest $request)
{
return $request->only('email', 'password');
}

/**
* Invalidate a token
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function deleteInvalidate()
{
$token = JWTAuth::parseToken();

$token->invalidate();

return response()->json(['message' => 'token_invalidated']);
}

/**
* Refresh a token
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function patchRefresh()
{
$token = JWTAuth::parseToken();

$newToken = $token->refresh();

return response()->json([
'message' => 'token_refreshed',
'data' => [
'token' => $newToken
]
]);
}

/**
* Get authenticated user
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function getUser()
{
return response()->json([
'message' => 'authenticated_user',
'data' => JWTAuth::parseToken()->authenticate()
]);
}
}

修改 config/repository.php

修改serializer,使用自己建立的,也可不改,使用預設的,但是回傳值會用 data 這個 key 值包起來

1
2
3
4
5
6
7
8
9
 'fractal' => [
'params' => [
'include' => 'include'
],
/**
* use custom array serializer
*/
'serializer' => App\Tools\ArraySerializer::class
],

加上客制化 class

如果在上一步驟有將 serializer 改成使用客制化的 serializer,則這一步驟為建立所需的 serializer

app/Tools/ArraySerializer.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace App\Tools;

use League\Fractal\Serializer\ArraySerializer as FractalArraySerializer;

class ArraySerializer extends FractalArraySerializer
{
public function collection($resourceKey, array $data)
{
if ($resourceKey) {
return [$resourceKey => $data];
}

return $data;
}

public function item($resourceKey, array $data)
{
if ($resourceKey) {
return [$resourceKey => $data];
}
return $data;
}
}

來寫一支 API 吧

情境:寫一個 book 資源 的 CRUD

撰寫 API 路徑
routes/api.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$api = app('Dingo\Api\Routing\Router');

$api->version('v1', function ($api) {
$api->group(['namespace' => 'App\Http\Controllers\Api'], function () use ($api) {

$api->group(['namespace' => 'Auth'], function () use ($api) {
$api->post('auth/register', 'RegisterController@register');
$api->post('auth/login', 'LoginController@login');
});

$api->group(['middleware' => 'api.auth'], function ($api) {
/**
* Get a book
*/
$api->get('/books/{id}', 'BooksController@show');

/**
* Create a book
*/
$api->post('/books', 'BooksController@store');

/**
* Update a book
*/
$api->patch('/books/{id}', 'BooksController@update');

/**
* Delete a book
*/
$api->delete('/books/{id}', 'BooksController@destroy');
});
});
});

使用 artisan 指令輕鬆產出 entity, controller, presenter, validator, transformer…

1
php artisan make:entity Book

編寫 table 欄位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

/**
* Class CreateBooksTable.
*/
class CreateBooksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('books', function(Blueprint $table) {
$table->increments('id');
$table->string('name',64);
$table->string('author',64);
$table->decimal('price',10,2);
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('books');
}
}

產生 table

1
php artisan migrate
依照需求修改 controller, repository, validator, transformer

app/Http/Controllers/Api/BooksController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Prettus\Validator\Exceptions\ValidatorException;
use App\Http\Requests\BookCreateRequest;
use App\Http\Requests\BookUpdateRequest;
use App\Repositories\BookRepository;
use \Throwable;

/**
* Class BooksController.
*
* @package namespace App\Http\Controllers\Api;
*/
class BooksController extends Controller
{
/**
* @var BookRepository
*/
protected $repository;

/**
* BooksController constructor.
*
* @param BookRepository $repository
* @param BookValidator $validator
*/
public function __construct(BookRepository $repository)
{
$this->repository = $repository;
}

/**
* Store a newly created resource in storage.
*
* @param BookCreateRequest $request
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Prettus\Validator\Exceptions\ValidatorException
* @throws \Throwable
*/
public function store(BookCreateRequest $request)
{
try {
$book = $this->repository->create(
[
'name' => $request->input('name'),
'author' => $request->input('author'),
'price' => $request->input('price'),
]
);

return response()->json([
'message' => 'Book created.',
'data' => $book
]);
} catch (ValidatorException $e) {
return response()->json([
'error' => true,
'message' => $e->getMessageBag()
], 500);
} catch (Throwable $e) {
return response()->json([
'error' => true,
'message' => $e->getMessage()
], 500);
}
}

/**
* Display the specified resource.
*
* @param int $id
* @return \Symfony\Component\HttpFoundation\Response
*/
public function show($id)
{
$book = $this->repository->find($id);

return response()->json([
'message' => 'Get a book.',
'data' => $book,
]);
}

/**
* Update the specified resource in storage.
*
* @param BookUpdateRequest $request
* @param integer $id
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Prettus\Validator\Exceptions\ValidatorException
* @throws \Throwable
*/
public function update(BookUpdateRequest $request, $id)
{
try {
$book = $this->repository->update(
[
'name' => $request->input('name'),
'author' => $request->input('author'),
'price' => $request->input('price'),
],
$id
);

return response()->json([
'message' => 'Book updated.',
'data' => $book
]);
} catch (ValidatorException $e) {
return response()->json([
'error' => true,
'message' => $e->getMessageBag()
], 500);
} catch (Throwable $e) {
return response()->json([
'error' => true,
'message' => $e->getMessage()
], 500);
}
}

/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Throwable
*/
public function destroy($id)
{
try {
$deleted = $this->repository->delete($id);

return response()->json([
'message' => 'Book deleted.',
'data' => $deleted
]);
} catch (Throwable $e) {
return response()->json([
'error' => true,
'message' => $e->getMessage()
], 500);
}
}
}

設定 validator 的內容
app/Validators/BookValidator.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** Validation Rules
*
* @var array
*/
protected $rules = [
ValidatorInterface::RULE_CREATE => [
'name' => "required|string",
'author' => "required|string",
'price' => 'required|numeric|between:0.01,1000000000.99'
],
ValidatorInterface::RULE_UPDATE => [
'name' => "required|string",
'author' => "required|string",
'price' => 'required|numeric|between:0.01,1000000000.99'
],
];
用 Postman 來測試看看!

先註冊
register

再登入
login

建立一個 book 資源
create a book

更新 book
update a book

取得 book
get a book

刪除 book
delete a book

Postman 小技巧

在拿到 token 後,跟 api 發出一個請求都要夾帶 token,這裡可以手動貼上,也可以在 test 的部份貼上一段程式碼,將 token 存成變數,之後在 header 裡 Authorization 直接對應 Bearer {{AUTH_TOKEN}} ,Postman 就會帶入剛剛拿到的 token 了。

1
2
3
4
5
6
7
8
9
tests["Body matches string"] = responseBody.has("token");
tests["Status code is 200"] = responseCode.code === 200;

var jsonData = JSON.parse(responseBody);
var token = jsonData.data.token;

tests["Has token inside it"] = typeof token === "string";

postman.setGlobalVariable("AUTH_TOKEN", token);

坑點

在測試 cors 情境的時候,遇到 server 回傳的 response header 一直沒有夾帶 Access-Control-Allow 系列的參數,以為是設定有錯,追了原始碼才確認不是 PHP 段的問題。最終發現,問題出在 server 安裝的 pagespeed module,所以在/etc/apache2/sites-available/000-default.conf/etc/apache2/sites-available/000-default-le-ssl.conf 將 api server 的 pagespeed 關閉,cors 就能正常使用了。這個 issue 可以參考
Missing CORS headers in response from PHP/Apache2

Reference

Dingo Api
https://github.com/dingo/api

tymon/jwt-auth
https://github.com/tymondesigns/jwt-auth

Laravel 5 repository
https://github.com/andersao/l5-repository

Laravel CORS
https://github.com/barryvdh/laravel-cors

Missing CORS headers in response from PHP/Apache2
https://stackoverflow.com/questions/51934338/missing-cors-headers-in-response-from-php-apache2

How to update environment variables based on a response in Postman
https://ppolyzos.com/2016/11/20/how-to-update-environment-variables-based-on-a-response-in-postman/