了解底层源码Laravel框架 RESTful API 风格进行设计开发(适合强迫症)

timg (3).jpg

如上图,本文讲述Laravel5.5 LTS为例子,怎么合理的开发 RESTful API 接口(真正意义上的前后端分离),因为博客正在开发,所以文章是持续更新的....
*注: 本文全部是自己手打的原创,基本上是文字比较多,所以大多都是要理解,有看不懂留言详解(另外大神们可以随意指点,我都会虚心接受)

1.Laravel底层核心架构Contracts契约的理解和使用

理解契约

契约用在哪里?契约哪里好?为什么要契约?这也是当初我带着这些疑问去尝试使用的问题
契约用官方解释是:Laravel 的契约是一组定义框架提供的核心服务的接口

其实就是:通过服务提供者去绑定接口类,然后在Services层实现方法,用Trait多继承特性,注入到控制器继承方法里使用
关键字服务提供者延迟提供器Trait依赖注入(DI)Io容器,请读者先了解一下这几个关键字再使用契约,已知的大神们请忽略这句话。

使用契约
  • 由于框架没有对契约的php artisan命令的支持,我们需要手动生成Contracts文件夹并新建.php文件

    Contracts文件目录.png

  • App\Contracts\Response.php

<?php
namespace App\Contracts;

interface Response
{
    /**
     * SUCCESS - 操作成功
     */
    public function success(string $message = '', array $data = []);

    /**
     * Error - 失败
     */
    public function error(string $message = '', array $data = []);
}
  • 聪明的人到这里大概知道我们契约用在了什么地方了吧~
    创建完文件夹和定义完方法后,我们要对此接口进行实现类方法就需要手动创建一个Services
Services文件目录:app\Services\ResponseService.php  (这里有个坑要注意类名和方法名一定要一致)

<?php
namespace App\Services;

use App\Contracts\Response;

class ResponseService implements Response
{
    /**
     * SUCCESS - 操作成功
     * @param $message string
     * @return json
     */
    public function success(string $message = '', array $data = [])
    {
        return response()->json([
            'status'  => true,
            'code'    => 200,
            'message' => empty($message) ? config('response.success') : $message,
            'data'    => $data,
        ]);
    }

    /**
     * Error - 失败
     * @param $message string
     * @return json
     */
    public function error(string $message = '', array $data = [])
    {
        return response()->json([
            'status'  => false,
            'code'    => 404,
            'message' => empty($message) ? config('response.error') : $message,
            'data'    => $data,
        ]);
    }
}
  • 然后用Laravel的核心架构服务提供者,去绑定这两者的关系,并且在config\appproviders数组中注册
    服务提供者支持artisan命令:php artisan make:provider ResponseServiceProvider
'providers' => [
...
        //注册Contracts契约接口
        App\Providers\ResponseServiceProvider::class,
],
  • 进入到刚才创建的ResponseServiceProvider.php文件中写入以下内容:
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Contracts\Response as ResponseContracts; // 引入契约
use App\Services\ResponseService; // 引入实现接口类

/**
 * Response - 延迟提供器
 * (推迟加载这种提供器会提高应用程序的性能,因为它不会在每次请求时都从文件系统中加载)
 *
 * Class ResponseServiceProvider
 * @package App\Providers
 * @author SuperHao - 619596123@qq.com
 */
class ResponseServiceProvider extends ServiceProvider
{
    /**
     * 延迟提供器 - 是否延时加载提供器。
     *
     * @var bool
     */
    protected $defer = true;

    /**
     * 引导任何应用程序服务
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * 在容器中注册绑定
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        // ======================绑定方式①====================== //
        // 给这个接口一个别名
        // $this->app->bind('MResponse','App\Contracts\Response');
        // 将Contracts接口和它的实现类绑定
        // $this->app->bind('App\Contracts\Response','App\Services\ResponseService');

        // ======================绑定方式②====================== //
        /**
         * singleton 单例绑定
         *
         * @param  \App\Contracts\Response        契约接口类
         * @param  \App\Services\ResponseService  接口实现类
         */
        $this->app->singleton(ResponseContracts::class, ResponseService::class);
        /**
         * 起别名
         *
         * @param  \App\Contracts\Response  契约接口类
         * @param  string $name
         */
        $this->app->alias(ResponseContracts::class, 'MResponse');
    }
    
    /**
     * 延迟提供器 - 获取提供器提供的服务。
     *
     * @return array
     */
    public function provides()
    {
        return ['MResponse'];
    }
}
  • 创建Trait继承类,实现接口类方法
/**
 * Response继承类
 *
 * Trait ResponseTrait
 * @package App\Traits
 * @property \App\Contracts\Response $response
 */
trait ResponseTrait
{
    /**
     * @var array
     */
    protected $method = ['response'];

    /**
     * 读取不可访问属性的值时
     * (当访问私有的或者受保护的属性时触发魔术方法)
     *
     * @param  string $name
     * @return mixed
     * @author SuperHao - 619596123@qq.com
     */
    public function __get($name)
    {
        if (in_array($name, $this->method)) {
            $name = "_" . $name;
            return $this->$name();
        }

        abort(422, trans('response.contractsError'));
    }

    /**
     * app获取可用的容器实例 - 响应
     *
     * @return \Illuminate\Foundation\Application|mixed
     * @author SuperHao - 619596123@qq.com
     */
    private function _response()
    {
        return app('MResponse');
    }
}
  • 到这里就差我们在控制器中,继承方法里use引入使用就可以了
namespace App\Http\Controllers;

use App\Traits\ResponseTrait;

/**
 * Api有状态 - 基类
 *
 * Class ApiController
 * @package App\Http\Controllers
 */
class ApiController extends Controller
{
    /**
     * 继承实现契约方法
     */
    use ResponseTrait;
}
  • 下面我们直接在有需要的时候调用自己定义的方法就阔以了
public function store(RegisterStoreRequest $request)
{
    $data = $request->only([
        'name',
        'nickname',
        'email',
        'mobile',
        'password',
    ]);

    $data['password'] = bcrypt($data['password']);
    User::create($data);

    // 调用契约类给前端返回json格式数据
    return $this->response->success(trans('response.success'));
 }
create.png

2.Eloquent 【fill】方法

fill方法,在Laravel5.5的官方文档里粗略的介绍了一丢丢,容易被人忽略使用的一个检测后批量赋值的方法,大白话来讲这个方法就是在save修改之前调用,来检测批量赋值Model层的protected $fillable白名单或protected $guarded黑名单
深入fill源码【思否】大牛讲解地址: segmentfault.com 下面这两句借鉴大神总结

当你设置了 fillable 数组,没有设置 guarded 数组时,那么此 Model 会处于 仅可批量赋值指定属性 的状态
当你没有设置 fillable 数组,却设置了 guarded 数组时,那么此 Model 会处于 可批量赋值任何属性 的状态
(注意:不要同时使用$fillable$guarded亲测目前Laravel还不允许这种骚操作~)

Controller:
public function User()
{      
       ...
       $this->user->fill($this->user)->save();
}
********************************************************************
Model:
class User extends BaseModel
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'nickname',
        'email',
        'mobile',
        'password',
        'nickname_at',
    ];
}

3.Trait 特性『合理』灵活地『应用』(标题Trait的解释来自大神 ---- Summer)Summer 站长第一条评论

Trait 是用来实现多继承的关系,弥补PHP这个单继承语音的缺点,Trait让PHP实现支持多继承,直接use进来直接使用。
最大的作用就是提高代码的复用性,和Interface继承类似但是区别在于,可以继承多个类,
Trait 更灵活方便了,可以用来写业务逻辑层封装公共方法提升复用性,但是要注意耦合性,下面是我博客中实战运用其中之一代码:

真实代码结合容器契约实现全局自定义Response返回数据格式:

namespace App\Traits\Applets;

use Illuminate\Support\Facades\Cache;

/**
 * 小程序 - 用户接口类
 *
 * Trait AppletsTrait
 * @package App\Traits\Applets
 */
trait AppletsTrait
{
    /**
     * 获取当前微信登录的用户信息
     *
     * @param $user
     * @return bool
     * @author SuperHao - 619596123@qq.com
     */
    public function getRedisUser(&$user)
    {
        /**
         * 由于优先级问题:先走继承再走中间件原因
         * 这里验证一下是否存在
         */
        $result = array_key_exists('HTTP_AUTHORIZATION', $_SERVER);
        if (!$result) return false;
        $session3rd = session3rd($_SERVER['HTTP_AUTHORIZATION']);
        if (!Cache::has($session3rd)) return false;

        /**
         * 获取用户 Redis Key值
         */
        $user = Cache::get($session3rd);
        if (empty($user)) return false;

        return true;
    }
}

4.Query Builder 叠加条件

Query Builder我认为是一个积木,创建了积木地基,上面你可以拼接任何的条件,运用很广:全局作用域 DB闭包外加条件 闭包函数 前两个官方文档都有展示,当你解锁了 Query 新姿势就可以各种变形的查询条件拼接叠加

  • 两种“地基”方式:
    DI注入形式:$query = $this->user->query()
    直接实例形式:$query = User::query()
  • 然后就可以随意的操作自己的条件代码Demo:
// 带name查询
if (request()->filled('type')) $query->where('type_id',$type);
// 闭包模糊查询
$query->where(function ($query) use ($value) {
     $query->orWhere('name', 'like','%' . $value['name'] . '%');
     $query->orWhere('keyword', 'like','%' . $value['keyword'] . '%');
     $query->orWhere('is_desable',"$value['is_desable']");
});
// 拼接倒序
$query->orderBy('id', 'desc');
// 最后再拼接获取方式
$data = $query->get(['id', 'name', 'email', 'status', 'is_disable']);
  • 实战打印出来看
$query = $this->hospital->query();
/**
 * 首字母筛选
 */  
if($request->filled('initials')) $query->where('initials',$request->input('initials'));   
/**
 * 地区筛选
 */
if($request->filled('area')) $query->where('area',$request->input('area'));
dd($query);  或者  dd($query->toSql());
bindings.png

5.Exceptions 全局捕获异常

前置条件:需要使用过表单验证 - 表单请求验证,为什么我说先需要使用这个,我们为了让代码更清晰简约剥离代替以前老的Illuminate\Support\Facades\Validator类去验证,不再把验证写在控制器当中,而是依赖注入到我们的控制当中,在这时候就特别...特别...特别...(重要的事说3遍)需要用到全局捕获异常(至于为什么,请认真看下面的解释)

当你在使用php artisan make:request StoreBlogPost去生成表单验证层的时候,用postman你会发现并不是抛出异常,而是跳转到上一个操作请求上了(超难受)。
跟进打开use Illuminate\Foundation\Http\FormRequest验证的继承类源码,你会发现有一个failedValidation方法,这个Laravel自带的源码中能看出方法是处理失败的验证里面有个->redirectTo()。找到祸源就解决它,不让它跳转,我尝试过重写父类这个方法,是可行的。但是太low,我们用这个全局捕获异常去捕获返回json更舒服一些,代码看下面:

  • App\Exceptions\Handler的重写方法代码:
/**
 * 重写 - 将异常呈现为HTTP响应 (Render an exception into an HTTP response)
 *
 * @param \Illuminate\Http\Request $request
 * @param Exception $exception
 * @return \Symfony\Component\HttpFoundation\Response
 * @author SuperHao - 619596123@qq.com
 */
public function render($request, Exception $exception)
{
    if ($exception instanceof ValidationException) {
        // 捕获 request
        return $this->response->error($exception->validator->errors()->first());
    } elseif ($exception instanceof AuthenticationException) {
        // 捕获 Auth
        dd('AuthenticationException - 身份不正确');
    } elseif ($exception instanceof ModelNotFoundException) {
        // 捕获 模型 (只限:findOrFail 和 firstOrFail 方法会抛出异常)
        return $this->response->error(trans('response.errorOrFail'));
    } elseif ($exception instanceof NotFoundHttpException) {
        // 捕获 404
        return $this->response->wrong(404 ,'您的页面 - 走丢了');
    } elseif ($exception instanceof MethodNotAllowedHttpException) {
        // 捕获 Route
        dd('MethodNotAllowedHttpException - 路由方法不对应');
    } elseif ($exception instanceof BroadcastException) {
        // 捕获 Route
        dd('BroadcastException - 广播异常');
    }

    /**
     * 没有捕获到走源码
     */
    return parent::render($request, $exception);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容