接口开发分享

介绍

大家好!本篇文章主要用来分享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
            ],
         

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值