从零开始搭建一个 demo 项目

简介

我之前遇到这样一个问题,平时都是对已有的项目进行维护或者添加一些新功能,偶尔有一天,需要我自己独立负责一个项目的时候,就有点“无从下手”,对,就是无从下手!零碎的知识点不知道该怎么把它们拼到一起,当最终完成时又会感慨:“哎呀,当时我为什么不那样做呢?我那时候没考虑到这个情况,我应该这样做的,等等”。

于是,我就想,为什么不做一个demo项目方便我或者他人使用呢? 因为需求是不同的,所以这个demo项目封装的只是一些通用的,常用到的内容,如有做的不对的地方,请点击右下方的纠正按钮,我们一起来完善它。

开发前的准备

适用项目范围

适用于小、中型项目,不涉及到高并发等情况,只是作为新手入门使用。

项目偏重点为api接口部分。

版本相关

- barryvdh/laravel-cors: "^0.11.4", - laravel/framework: "^6.2", - laravel/passport: "^8.0", - spatie/laravel-query-builder: "^2.3"

环境搭建

我们默认您已经安装好了所需的 lnmp环境,环境搭建请看Laravel 开发环境部署

如果您喜欢使用集成环境,推荐LaragonPHPStudy

创建项目

通过Laravel安装器

 composer global  require laravel/installer

确保将 Composer’s system-wide vendor 目录放置在你的系统环境变量 $PATH 中,如果ok,您可以使用laravel new 项目名 创建一个新的项目。

通过composer

 composer create-project --prefer-dist laravel/laravel 项目名

从github拉取项目

您可以通过下面命令直接拉取本项目:

 git clone https://github.com/Jouzeyu/api-demo.git

相关配置

您可以在.env文件中配置您的数据库连接,然后运行php artisan migrate命令迁移数据库。

API 登录

说明

我们这里使用官方给推荐的Passport OAuth 认证,当然你也可以使用Dingo API。如果你的项目只是几个人使用且不需要刷新令牌的话,也可以尝试使用Laravel的 API 认证

安装

composer require laravel/passport

运行迁移

php artisan migrate

运行迁移后将在数据库中自动创建相关表,如果提示错误,请优先查看您的.env文件中的数据库连接是否正确。

生成秘钥和客户端

接下来,运行 passport:install 命令来创建生成安全访问令牌时所需的加密密钥,同时,这条命令也会创建用于生成访问令牌的「个人访问」客户端和「密码授权」客户端:

php artisan passport:install

配置及使用

第一步:在User模型中引用 HasApiTokens

<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
    use Notifiable, HasApiTokens;
}

第二步:在 app/Providers/AuthServiceProvider 中的 boot 方法中调用 Passport::routes 方法。

<?php

namespace App\Providers;

use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        //'App\Model' => 'App\Policies\ModelPolicy'
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Passport::routes();
    }
}

最后一步:在 config/auth.php 配置文件中,将 api 的 driver 选项替换为 passport

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

推荐文章:使用 Laravel Passport 处理 API 认证

项目相关:完成API登录(其他部分)

1. 在app/Http/Controllers/Api下创建Controller.php,内容如下:

<?php

namespace App\Http\Controllers\Api;

use Illuminate\Routing\Controller as BaseController;

class Controller extends BaseController
{

}

2. 创建 Auth控制器并修改如下:

<?php

namespace App\Http\Controllers\Api;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use App\User;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    /**
     * Create user
     *
     * @param  [string] name
     * @param  [string] email
     * @param  [string] password
     * @return [string] message
     */
    public function register(Request $request)
    {
        $request->validate([
            'name' => 'required|string',
            'email' => 'required|string|email|unique:users',
            'password' => 'required|string'
        ]);

        $user = new User([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password)
        ]);

        $user->save();

        return response()->json([
            'message' => '用户创建成功'
        ], 201);
    }

    /**
     * Login user and create token
     *
     * @param  [string] email
     * @param  [string] password
     * @param  [boolean] remember_me
     * @return [string] access_token
     * @return [string] token_type
     * @return [string] expires_at
     */
    public function login(Request $request)
    {
        $request->validate([
            'email' => 'required|string|email',
            'password' => 'required|string',
            'remember_me' => 'boolean'
        ]);

        $credentials = request(['email', 'password']);

        if(!Auth::attempt($credentials))
            return response()->json([
                'message' => '用户名或者密码错误'
            ], 401);

        $user = $request->user();

        $tokenResult = $user->createToken('Personal Access Token');
        $token = $tokenResult->token;

        if ($request->remember_me)
            $token->expires_at = Carbon::now()->addWeeks(1);

        $token->save();

        return response()->json([
            'access_token' => $tokenResult->accessToken,
            'token_type' => 'Bearer',
            'expires_at' => Carbon::parse(
                $tokenResult->token->expires_at
            )->toDateTimeString()
        ]);
    }

    /**
     * Get the authenticated User
     *
     * @return [json] user object
     */
    public function userInfo(Request $request)
    {
        return response()->json($request->user());
    }
}

3. api路由部分:

Route::group([
    'prefix' => 'v1'
], function () {
    Route::post('login', 'Api\AuthController@login');
    Route::post('register', 'Api\AuthController@register');

    Route::group([
      'middleware' => 'auth:api'
    ], function() {
        Route::get('user_info', 'Api\AuthController@userInfo');
    });
});

项目相关:测试结果

注册结果:

从 0 开始搭建一个 demo 项目

登录结果:

从 0 开始搭建一个 demo 项目

置换详细信息结果:

从 0 开始搭建一个 demo 项目

统一 Restful API 响应处理

说明

私下和公司前端聊过,怎样的Restful API 响应对他们调用起来更加友好,前端说只要统一就好,但是经过尝试,对接,我发现后端捕获所有异常,并添加到返回数据中更加适合前后端分离项目。所以我们这里采用的也是这种方式。

封装统一返回信息

首先我们在app/Api文件夹下新建一个Helpers文件夹并创建一个名为ApiResponse.php的文件来存放我们要封装的内容:

<?php
namespace App\Api\Helpers;
use Symfony\Component\HttpFoundation\Response as FoundationResponse;
use Response;

trait ApiResponse
{
    /**
     * @var int
     */
    protected $statusCode = FoundationResponse::HTTP_OK;

    /**
     * @return mixed
     */
    public function getStatusCode()
    {
        return $this->statusCode;
    }

    /**
     * @param $statusCode
     * @return $this
     */
    public function setStatusCode($statusCode,$httpCode=null)
    {
        $httpCode = $httpCode ?? $statusCode;
        $this->statusCode = $statusCode;
        return $this;
    }

    /**
     * @param $data
     * @param array $header
     * @return mixed
     */
    public function respond($data, $header = [])
    {

        return Response::json($data,$this->getStatusCode(),$header);
    }

    /**
     * @param $status
     * @param array $data
     * @param null $code
     * @return mixed
     */
    public function status($status, array $data, $code = null){

        if ($code){
            $this->setStatusCode($code);
        }
        $status = [
            'status' => $status,
            'code' => $this->statusCode
        ];

        $data = array_merge($status,$data);
        return $this->respond($data);

    }

    /**
     * @param $message
     * @param int $code
     * @param string $status
     * @return mixed
     */
    /*
     * 格式
     * data:
     *  code:422
     *  message:xxx
     *  status:'error'
     */
    public function error($message, $code = FoundationResponse::HTTP_BAD_REQUEST,$status = 'error'){

        return $this->setStatusCode($code)->message($message,$status);
    }

    /**
     * @param $message
     * @param string $status
     * @return mixed
     */
    public function message($message, $status = "success"){

        return $this->status($status,[
            'message' => $message
        ]);
    }

    /**
     * @param string $message
     * @return mixed
     */
    public function internalError($message = "Internal Error!"){

        return $this->error($message,FoundationResponse::HTTP_INTERNAL_SERVER_ERROR);
    }

    /**
     * @param string $message
     * @return mixed
     */
    public function created($message = "created")
    {
        return $this->setStatusCode(FoundationResponse::HTTP_CREATED)
            ->message($message);

    }

    /**
     * @param $data
     * @param string $status
     * @return mixed
     */
    public function success($data, $status = "success"){

        return $this->status($status,compact('data'));
    }

    /**
     * @param string $message
     * @return mixed
     */
    public function notFond($message = 'Not Fond!')
    {
        return $this->error($message,Foundationresponse::HTTP_NOT_FOUND);
    }
}

如何使用

修改基类

<?php

namespace App\Http\Controllers\Api;

use App\Api\Helpers\ApiResponse;
use App\Http\Controllers\Controller as BaseController;

class Controller extends BaseController
{

    use ApiResponse;
    // 引用封装好的错误提示模板
}

其他控制器调用

1. 返回正确资源信息

return $this->success($users);

2. 返回自定义 http 状态码的正确信息

return $this->setStatusCode(201)->success($users);

3. 返回错误信息

return $this->error('用户注册失败');

5. 返回自定义 http 状态码的错误信息

return $this->error('用户登录失败',401);

声明:这块部分参考自手摸手教你让 Laravel 开发 API 更得心应手

项目相关:完善其他部分

1. 创建Users控制器,方便测试:

<?php

namespace App\Http\Controllers\Api;

use App\User;
use Illuminate\Http\Request;


class UsersController extends Controller
{
   public function test(Request $request){
       $user=User::where('id',1)->first();
       return $this->success($user);
   }
}

2. 注册路由

Route::group([
    'prefix' => 'v1'
], function () {
    Route::get('test', 'Api\UsersController@test');//这里,不需要登录认证

   ……

});

项目相关:测试结果

从 0 开始搭建一个 demo 项目

从 0 开始搭建一个 demo 项目

从 0 开始搭建一个 demo 项目

从 0 开始搭建一个 demo 项目

处理接口返回值

问题抛出

为了安全等原因,我们都会限制其返回值。如你所见,在上一块中返回User信息时,返回的是所有的数据,其中就包含了敏感信息password,常见的敏感信息还包括微信的openid等,如何避免就是这一块的主要内容。

API资源

简介

你往往需要一个转换层来联结你的 Eloquent 模型和实际返回给用户的 JSON 响应,也就是我们常说的ViewModel层。

创建用户资源

php artisan make:resource Api/UserResource

ok,我们可以看到在http目录下多了一个resource目录,展开后编辑UserResource.php如下:

<?php

namespace App\Http\Resources\Api;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * 这里指定了返回值
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id'=>$this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at'=>(string)$this->created_at,
            'updated_at'=>(string)$this->updated_at
        ];
    }
}

如何使用

打开我们的User测试控制器,将:

  return $this->success($user);

替换为:

return $this->success(new UserResource($user));

注意:该方法只适合返回单一用户,否则将无法使用分页。如果是用户列表,你可以使用return UserResource::collection($user);

项目相关:测试结果

从零开始搭建一个 demo 项目

实现枚举的三种方式

现在有一个需求,给用户添加状态,比如冻结,正常。这是很普遍的需求,我们一般是在数据表中多一个status字段,用数字代表身份,但是短时间内我们知道数字所代表的含义,时间一长就忘记了,而且前端人员也不清楚。怎么办呢,我们就用到了枚举。

单独的枚举目录

创建Enum目录

app下创建一个 Enum 目录,里面存放我们的枚举文件,例如UserEnum.php

<?php

namespace App\Enum;
class UserEnum
{
    // 状态类别
    const NORMAL = 1; //正常
    const FREEZE = 2; //冻结

    public static function getStatusName($status){
        switch ($status){
            case self::NORMAL:
                return '正常';
            case self::FREEZE:
                return '冻结';
            default:
                return '正常';
        }
    }
}

使用

修改 UserResource.php:

return [
            'id'=>$this->id,
            'name' => $this->name,
            'email' => $this->email,
            'status'  =>  UserEnum::getStatusName($this->status),
            'created_at'=>(string)$this->created_at,
            'updated_at'=>(string)$this->updated_at
        ];

提示:冻结状态就不应该能够登陆了,这里的判断逻辑请自行编写

配置文件

因为篇幅较长,大家可以看我之前的博客如何优雅地使用帮助类文件 helpers.php

嵌套太模型中

同理,大家可以看我之前的博客为 type 等字段「保驾护航」

由于每个人喜欢的方式不同,故枚举这块不计入demo项目中。

接口筛选

说明

前面我们已经完善的差不多了,但是还差一个重要的模块,那就是接口筛选。达到的目的就是前端通过不同的参数,来拿到相对应的筛选后的数据。我们这里用的是 laravel-query-builder拓展包。

安装

composer require spatie/laravel-query-builder

发布配置

php artisan vendor:publish --provider="Spatie\QueryBuilder\QueryBuilderServiceProvider" --tag="config"

如何使用

1. 首先确保模型中允许通过了所需字段,例如User模型中:

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

2. 在控制器中使用:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Resources\Api\UserResource;
use App\User;
use Illuminate\Http\Request;
use Spatie\QueryBuilder\QueryBuilder;


class UsersController extends Controller
{
   public function test(Request $request){
       $users = QueryBuilder::for(User::class)
           ->allowedFilters(['name'])
           ->get();
       return UserResource::collection($users);
   }
}

3. 前端筛选

http://api-demo.test/api/v1/test?filter[name]=jouzeyu

注意:引入的是use Spatie\QueryBuilder\QueryBuilder;,而不是其他。我们使用allowedFilters来声明允许筛选的字段。这里我们只是初步介绍一下,更多内容请看文档

其他配置

语言包安装

composer require caouecs/laravel-lang:~4.0

debug调试工具栏

composer require barryvdh/laravel-debugbar --dev

提示:--dev代表仅在本地安装

laravel-ide-helper

composer require --dev barryvdh/laravel-ide-helper

解决跨域问题

安装

composer require barryvdh/laravel-cors

配置

app/Http/Kernel.php中添加:

protected $middleware = [
    // ...
    \Barryvdh\Cors\HandleCors::class,
];

总结

demo 项目暂时就告一段落了,也许他并不完美,但还是希望对刚刚入门的朋友有所启发。如果你有更好的方案,欢迎在下方评论,感谢。

开源地址:https://github.com/Jouzeyu/api-demo

本作品采用《CC 协议》,转载必须注明作者和本文链接
空舟湖上~      ——Jouzeyu
本帖由 lochpure 于 4年前 加精
lochpure
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 10
Dennis_Ritchie

加油 :+1:

4年前 评论

很有帮助,很实用,点赞!

4年前 评论

点个赞,对于laravel 想要多学习的人来说,还是很有必要的

4年前 评论
Summer

总结的很棒

4年前 评论

超赞 正好在看 vue 做前端 前后端分离 API 这块 感谢楼主的分享

4年前 评论

经验+1 :+1:

4年前 评论

windows homestead 启动后,下一步的开发 laravel 应该装在哪
不知道 composer global require laravel/installer 在 homestead 中执行,还是在 windows 执行
file

4年前 评论

正好在做一个api项目前期准备工作 学习了!

4年前 评论

登录后接口为什么不用返回 refresh_token ,不用做刷新 token 机制吗?

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!