目录
介绍
大家好!本篇文章主要用来分享laravel开发接口过程中的一些个人心得。接下来我们直接接入主题。
准备工作
创建应用
composer config -g repos.packagist composer https://mirrors.tencent.com/composer/
cd ~/code
composer create-project laravel/laravel bbs --prefer-dist "8.*"
修改Homestead.yaml
创建新的站点
sites:
- map: bbs.test
to: /home/vagrant/code/bbs/public
php: "7.4"
databases:
- bbs
重载homestead
vagrant provision
vagrant reload
修改hosts文件
192.168.10.10 bbs.test
修改.env
APP_URL=http://bbs.test
DB_DATABASE=bbs
DB_USERNAME=homestead
DB_PASSWORD=secret
执行迁移文件
php artisan migrate
API基础环境
关于Restfull的设计分解,可以查看之前发布的一篇文章API开发中就如何提高接口优雅性的理论实践
新增app/http/middleware/AcceptHeader.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AcceptHeader
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}
修改app/http/Kernel.php
.
.
.
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\App\Http\Middleware\AcceptHeader::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
实际开发
用户模块
填充用户数据
去掉DatabaseSeeder.php里面的注释
\App\Models\User::factory(100)->create();
填充数据
php artisan db:seed
安装JWT
composer require tymon/jwt-auth
修改config/auth.php
.
.
.
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
.
.
.
user 模型需要继承 Tymon\JWTAuth\Contracts\JWTSubject 接口,并实现接口的两个方法 getJWTIdentifier() 和 getJWTCustomClaims()。
修改Models/User.php
.
.
.
use Tymon\JWTAuth\Contracts\JWTSubject;
.
.
.
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
.
.
.
getJWTIdentifier 返回了 User 的 id,getJWTCustomClaims 是我们需要额外在 JWT 载荷中增加的自定义内容,这里返回空数组。打开 tinker,执行如下代码,尝试生成一个 token。
$user = User::first();
Auth::guard('api')->login($user);
创建控制器
php artisan make:controller Api/Controller
php artisan make:controller Api/AuthorizationsController
php artisan make:request Api/FormRequest
php artisan make:request Api/AuthorizationRequest
Controller.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller as BaseController;
class Controller extends BaseController
{
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60
]);
}
}
FormRequest.php
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;
class FormRequest extends BaseFormRequest
{
/**
* 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 [
//
];
}
}
AuthorizationRequest.php
<?php
namespace App\Http\Requests\Api;
class AuthorizationRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'username' => 'required|string',
'password' => 'required|alpha_dash|min:6',
];
}
public function attributes()
{
return [
'username' => '用户名',
'password' => '密码',
];
}
}
AuthorizationsController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\Api\AuthorizationRequest;
class AuthorizationsController extends Controller
{
public function store(AuthorizationRequest $request)
{
$credentials['email'] = $request->email;
$credentials['password'] = $request->password;
if (!$token = \Auth::guard('api')->attempt($credentials)) {
error_response('attempt-failed');
}
return $this->respondWithToken($token)->setStatusCode(201);
}
public function update()
{
try {
$token = auth('api')->refresh();
} catch (\Exception $e) {
error_response('token-refresh-failed');
}
return $this->respondWithToken($token);
}
public function destroy()
{
auth('api')->logout();
return response(null, 204);
}
}
添加路由
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->namespace('Api')->name('api.v1.')->group(function () {
Route::middleware('throttle:' . config('api.rate_limits.sign'))
->group(function () {
Route::post('authorizations', 'AuthorizationsController@store')
->name('authorizations.store');
// 刷新token
Route::put('authorizations/current', 'AuthorizationsController@update')
->name('authorizations.update');
// 删除token
Route::delete('authorizations/current', 'AuthorizationsController@destroy')
->name('authorizations.destroy');
});
Route::middleware('throttle:' . config('api.rate_limits.access'))
->group(function () {
});
});
添加config/api.php
<?php
return [
/*
* 接口频率限制
*/
'rate_limits' => [
// 访问频率限制,次数/分钟
'access' => env('RATE_LIMITS', '120,1'),
// 登录相关,次数/分钟
'sign' => env('SIGN_RATE_LIMITS', '60,1'),
],
];
放开Providers/RouteServiceProvider.php的注释
protected $namespace = 'App\\Http\\Controllers';
处理报错
新增app/Exceptions/CustomException.php文件
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Http\Request;
class CustomException extends Exception
{
protected $msgForUser,$errorCode,$data;
public function __construct(string $message, string $msgForUser = '系统内部错误', int $code = 500, int $errorCode = 0, $data = [])
{
parent::__construct($message, $code);
$this->msgForUser = $msgForUser;
$this->errorCode = $errorCode;
$this->data = $data;
}
public function render(Request $request)
{
if ($request->expectsJson()) {
return response()->json(['code' => $this->errorCode, 'message' => $this->msgForUser, 'data' => $this->data], $this->code);
}
session()->flash('danger', $this->msgForUser.':'.pre_array($this->data,'',true));
return redirect()->back()->withInput();
}
}
新增app/helpers.php
<?php
use App\Exceptions\CustomException;
function error_response($value, $data = [], $message = '')
{
$error = config('error-code.' . $value);
$message = empty($message) ? $error['message'] : $message;
throw new CustomException($message, $message, $error['statusCode'], $error['code'], $data);
}
/**
* 格式化数组输出
* @param $vars 需要格式化的数组
* @param $label 名称
* @param $return 是否返回值,默认直接输出
* @return string
*/
function pre_array($vars, $label = '', $return = false)
{
$content = '';
if (ini_get('html_errors')) {
$content = "";
if ($label != '') {
$content .= "<strong>{$label} :</strong>\n";
}
$content .= htmlspecialchars(print_r($vars, true));
$content .= "\n";
} else {
if ($label != '') {
$content .= "{$label} :\n";
}
$content .= print_r($vars, true);
}
if ($return) {
return $content;
}
echo $content;
}
新增config/error-code.php
<?php
return [
'user-does-not-exist' => [
'statusCode' => 403,
'message' => '用户不存在',
'code' => 1002
],
'attempt-failed' => [
'statusCode' => 403,
'message' => '用户名或密码错误',
'code' => 1004
],
'token-refresh-failed' => [
'statusCode' => 401,
'message' => 'token刷新失败',
'code' => 1015
],
];
修改composer.json
{
.
.
.
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
},
"files": [
"app/helpers.php"
]
},
.
.
.
}
重载文件
composer dump-autoload
到这里用户登录结束
帖子模块
php artisan make:model Topic -msfc
迁移文件
2022_07_11_170401_create_topics_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTopicsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('topics', function(Blueprint $table) {
$table->increments('id');
$table->string('title')->index()->comment('标题');
$table->text('body')->comment('主体');
$table->bigInteger('user_id')->unsigned()->index()->comment('用户ID');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->integer('view_count')->unsigned()->default(0)->comment('查看数');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('topics');
}
}
app/Models/Topic.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Topic extends Model
{
use HasFactory;
protected $fillable = [
'title', 'body'
];
public function user()
{
return $this->belongsTo(User::class);
}
}
database/factories/TopicFactory.php
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class TopicFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
$at = $this->faker->dateTimeBetween('2019-01-01', 'now');
return [
'title' => $this->faker->sentence(),
'body' => $this->faker->text(),
'view_count' => rand(1, 10000),
'created_at' => $at,
'updated_at' => $at
];
}
}
填充数据
database/seeders/TopicSeeder.php
<?php
namespace Database\Seeders;
use App\Models\Topic;
use Illuminate\Database\Seeder;
use App\Models\User;
use Faker\Generator;
class TopicSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// 防止内存耗尽的异常
ini_set('memory_limit', -1);
// 所有用户 ID 数组,如:[1,2,3,4]
$user_ids = User::all()->pluck('id')->toArray();
// 获取 Faker 实例
$faker = app(Generator::class);
$topics = Topic::factory()->times(1000)->make()->each(function ($topic,$index) use ($user_ids,$faker){
// 从用户 ID 数组中随机取出一个并赋值
$topic->user_id = $faker->randomElement($user_ids);
});
Topic::insert($topics->toArray());
print("插入完成 \n");
}
}
非常好用查询构造器
composer require spatie/laravel-query-builder
php artisan vendor:publish --provider="Spatie\QueryBuilder\QueryBuilderServiceProvider" --tag="config"
app/http/queries/TopicQuery.php
<?php
namespace App\Http\Queries;
use App\Models\Topic;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
class TopicQuery extends QueryBuilder
{
public function __construct()
{
parent::__construct(Topic::query());
$this->allowedIncludes('user')
->allowedFilters([
'title',
AllowedFilter::exact('user_id'),
])->defaultSort('-created_at');
}
}
app/http/controllers/api/TopicController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Resources\TopicResource;
use App\Http\Queries\TopicQuery;
use App\Models\Topic;
use App\Http\Requests\Api\TopicRequest;
class TopicController extends Controller
{
public function index(TopicQuery $query){
$rows = $query->paginate();
return TopicResource::collection($rows);
}
public function store(TopicRequest $request,Topic $topic){
$topic->fill($request->all());
$topic->user()->associate($request->user());
$topic->save();
return new TopicResource($topic);
}
public function show(Topic $topic){
$topic->load('user');
return new TopicResource($topic);
}
public function destroy(Topic $topic)
{
$this->authorize('destroy', $topic);
$topic->delete();
return response(null, 204);
}
}
策略自动发现
修改app/Providers/AuthServiceProvider.php
use Illuminate\Support\Facades\Gate;
// 修改策略自动发现的逻辑
Gate::guessPolicyNamesUsing(function ($modelClass) {
// 动态返回模型对应的策略名称,如:// 'App\Model\User' => 'App\Policies\UserPolicy',
return 'App\Policies\\'.class_basename($modelClass).'Policy';
});
新增app/Policies/TopicPolicy.php
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use App\Models\Topic;
class TopicPolicy
{
use HandlesAuthorization;
public function destroy(User $user, Topic $topic)
{
return $user->isAuthorOf($topic);
}
}
app/Models/User.php增加isAuthorOf方法
public function isAuthorOf($model)
{
return $this->id == $model->user_id;
}
开启调试模式
composer require barryvdh/laravel-debugbar --dev
php artisan vendor:publish --provider="Barryvdh\Debugbar\ServiceProvider"
修改config/debugbar.php
.
.
.
'explain' => [ // Show EXPLAIN output on queries
'enabled' => true,
'types' => ['SELECT'], // Deprecated setting, is always only SELECT
],