本文主要总结使用 Laravel 5.5,及配套的一些组件在进行开发遇到的一些值得记录的东西,以及坑。
Laravel 5.5 - Dingo API - Laravel admin
Laravel 5.5 重拾 Tinker 这货真的很好用,Laravel 引入的所有类有很多方法名我们其实是不知道的,但是在 tinker 中可以被全部补全出来。
当然除了补全 Laravel 中的类方法和全局函数,也可以和 php -a 一样补全 PHP 内置的函数,且能少些不少语法。
总之,对开发期间帮助性很强,十分推荐。
路由 自定义 API 路由文件 为了方便管理 API,希望把 API 的路由文件分离到不同的文件,期望 Laravel 自动载入 routes/api/ 目录下的所有路由文件。
修改 App\Providers\RouteServiceProvider ,新增 $apiRoutesPath
属性并修改 mapApiRoutes()
方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected $apiRoutesPath = 'api' ; protected function mapApiRoutes() { $routeRegistrar = Route::prefix('api' ) ->middleware('api' ) ->namespace ($this- >namespace ); load_phps( route_path($this- >apiRoutesPath), function ($file ) use ($routeRegistrar ) { $routeRegistrar- >group ($file ); } ); }
其中 load_phps
是我自定义的全局辅助函数,在 Laravel 启动时已经载入,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function load_phps (string $path , \Closure $callable ) { if (! file_exists ($path )) { excp ("PHP files path not exists: {$path} " ); } $result = []; $fsi = new \FilesystemIterator ($path ); foreach ($fsi as $file ) { if ($file ->isFile ()) { if ('php' == $file ->getExtension ()) { $result [$file ->getPathname ()] = $callable ($file ); } } elseif ($file ->isDir ()) { $_path = $path .'/' .$file ->getBasename (); load_phps ($_path , $callable ); } } unset ($fsi ); return $result ; }
模型 自定义模型属性 在一个模型控制器获取一个主要的模型数据后,往往还需要和其他接口或模型或计算结果进行聚合,为了方便直接返回一个模型,可以考虑动态地往模型现有数据中添加其他数据。
创建一个 AttrCustomable
trait
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php namespace App \Traits ;trait AttrCustomable { private $customAttributes = []; public function addCustomAttribute (string $key , $value ) { $this ->customAttributes[$key ] = $value ; return $this ; } public function toArray ( ) { $data = parent ::toArray (); $data = array_filter (array_merge ($data , $this ->customAttributes)); return $data ; } }
1 2 3 4 5 6 7 <?php namespace App \Models ; class User extends Model { use AttrCustomable ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php namespace App \Http \Controllers \Api \V1 ;class User { public function show ( ) { return $this ->user () ->addCustomAttribute ('pionts' , 10000 ) ->addCustomAttribute ('foo' , 'bar' ); } }
获取隐藏属性 假设在 User 模型的某个方法内,要访问 hidden
过的属性 password
,有如下几种方式:
通过模型的 makeVisible
方法 (5.1 是 withHidden
)
1 2 3 $this ->makeVisible('password' ); // 使 password 属性可见echo $this ->password; // 输出用户密码 $this ->makeHidden('password' ); // 使 password 属性隐藏
源码入下:
1 2 3 4 5 6 7 8 9 10 public function makeVisible ($attributes ) { $this ->hidden = array_diff ($this ->hidden, (array ) $attributes ); if (! empty ($this ->visible)) { $this ->addVisible ($attributes ); } return $this ; }
1 2 3 4 5 6 7 8 9 10 public function makeHidden ($attributes ) { $attributes = (array ) $attributes ; $this ->visible = array_diff ($this ->visible, $attributes ); $this ->hidden = array_unique (array_merge ($this ->hidden, $attributes )); return $this ; }
1 2 $allAttrs = $this ->getAttributes(); // 直接获得模型所有属性(无视可见性设置)echo $allAttrs ['password' ]; // 输出用户密码
如果模型实现了 Illuminate\Contracts\Auth\Authenticatable
接口,则还可以通过该接口提供的 getAuthPassword
方法只获取密码属性
1 2 3 4 5 6 7 class User extends Model implements \Illuminate\Contracts\Auth\Authenticatable { public function index() { echo $this ->getAuthPassword(); } }
如何判断模型是否存在 1 $mode->exists; // !!! 属性调用而非方法
定义资源路由后控制器模型注入失效?
1 2 3 4 5 6 7 8 $router->group([ 'prefix' => 'members' , 'middleware' => [ 'api.auth' , ], ], function ($router) { $router->resource('address' , 'MemberAddress' ); });
生成路由定义如下:
1 2 3 4 5 - GET|HEAD /members/ address MemberAddress@index - POST /members/ address MemberAddress@store - GET|HEAD /members/ address/{address} MemberAddress@show - PUT|PATCH /members/ address/{address} MemberAddress@update - DELETE /members/ address/{address} MemberAddress@destroy
以对应的 MemberAddress
控制器的 show
方法举例:
请求 GET
/members/address/1
,其中 1 号的记录在数据库是实际存在的,可在方法中不是期望的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 use App \Models \MemberAddress as Address ;public function show (Address $address ) { dd ($address ->exists); dd (func_get_args ()); array :2 [▼ 0 => MemberAddress { 1 => "1" ] }
结果显示,这里在向控制器方法自动注入模型的时候,参数和模型没有对应上,导致 $address 只是个空模型,这当然不是我们想要的。
在 dingo api GitHub 的这个 issue
的提示下,去翻了 bindings
这个中间件的源码后找到了我想要的解决办法:
参见:Illuminate\Routing\ImplicitRouteBinding@resolveForRoute
。
修改 MemberAddress
控制器的构造方法如下:
1 2 3 4 5 6 7 8 public function __construct() { $this ->middleware('id_filter:address,\App\Models\MemberAddress' ) -> only([ 'show' , 'update' , 'destroy' ]); }
新增中间件并注册到 App\Http\Kernel
:
1 2 3 4 protected $routeMiddleware = [ // ... 'id_filter' => \App \Http \Middleware \IDFilter : :class , ];
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 <?php namespace App \Http \Middleware ;class IDFilter { public function handle ( $request , \Closure $next , string $idkey , string $class = null , string $abortOn404 = 'no' , string $forget = 'no' ) { $route = $request ->route (); $id = ($route ->parameters[$idkey ] ?? false ); if (empty_safe ($id ) || (! ispint ($id )) || ($id < 1 )) { abort (403 , "Bad id value of `{$idkey} `: {$id} " ); } if ($class ) { if (! class_exists ($class )) { abort (503 , 'Model class not exists: ' .$class ); } $model = app ($class ); $_model = $model ->where ($model ->getRouteKeyName (), $id )->first (); if (ci_equal ('yes' , $abortOn404 ) && (! $_model )) { abort (404 , 'Model object not found: ' .get_class ($model )); } if (ci_equal ('yes' , $forget )) { $route ->forgetParameter ($idkey ); } else { $route ->setParameter ($idkey , $_model ?? $model ); } } return $next ($request ); } }
其中,getRouteKeyName
默认返回 id
可以在相应模型中重写这个方法实现自定义。
之所以不直接用 Laravel 提供的默认 bindings
中间件,原因有两点:
使用锁的场景 通常在并发场景下,为了防止某个资源被重复创建/更新,在同时存在查询-更新/创建操作的时候 必要使用悲观锁:
1 2 3 4 5 6 7 8 9 10 11 $amount = 100 ;\DB : :beginTransction (); $user = \App \Models \User : :lockForUpdate ()->find(1 ); // 锁住该用户对应的行记录,防止并发中的其他请求修改该用户状态$log = \App \Models \BalanceLog : :insert ([ 'before' => $user ->balance, 'create_at' => time(), 'amount' => $amount , ]); $user ->balance += $amount ;$user ->save();\DB : :commit ();
如果不使用 lockForUpdate()
来锁住该用户对应的记录,那么在多个相同请求到来时,该用户的余额和日志会被更新/创建多条,这明显不是我们想要的。
lockForUpdate()
创建的锁将在本次事务结束后释放,且如果记录不存在,lockForUpdate()
则不会起到什么作用。
不需要前端、UI, 后端就可以开发整个后台
这是个专门写后台管理系统的轮子。简而言之,是用 PHP 同时写后台页面和后端逻辑。
优点是将 Web 前端组件很好地封装到后端,使后端人员可以不用操心页面上的东西,要什么页面元素按照文档照做就行了,这么一来,写一个后台只需要后端就够了。
虽然对于 Laravel 5.5 来说,要实现 Dingo API 的所有功能易如反掌,但之所以选择 Dingo API,主要是因为已经有很多的人用它,相信它封装得更标准,更简便,更稳定,不想花时间重复造轮子罢了。
如果使用过程发现并不好用,或者不太适合我们的项目,这样改造起来也有针对性。
优缺点
Dingo API 使用提供了 api:docs 这个 artisan 命令,可以自动为 API 生成版本文档。
虽然在定义 API 端点的时候指已经明了版本号和路由,但是为了生成更好看的 API 文档,仍然需要在类和方法前面重新定义路由的类型和路径,已经版本号。这样一来,代码中要添加很多额外的注释。
缺点:api:cache
会缓存除了 API 以外的所有路由。
期望:只缓存通过 Dingo API 定义好的路由。
FAQ
如何访问指定版本的接口? 在配置 Dingo API 的时候,有个配置项叫做 API_VERSION
的配置,这个配置的作用是作为默认版本号,即客户端当没有指定。
在 HTTP 请求头中添加 Accept
字段,举例说明:
1 2 3 curl -X GET http:// api.example.com/user \ -H 'Authorization: Bearer ??TOKEN??' \ -H 'Accept: application/vnd.{API_SUBTYPE}.{VERSION_NUMBER}+json' \
其中,API_SUBTYPE
就是 .env
中配置的 API_SUBTYPE
,VERSION_NUMBER
就是使用 version()
方法定义过的版本号,vnd
是 API_STANDARDS_TREE
推荐的值,json
代表客户端期望服务器返回的数据格式。
如何正确使用自动刷新 Token 机制? 需要前后端协作好:后端要根据当此请求携带的 Token 自动生成一条新 Token,并返回给前端;前端要做好自动使用后端每次返回的更新 Token 来作为下次 API 请求的 Token。
以 tymon/jwt-auth 认证驱动为例,在 Tymon\JWTAuth\Providers\AbstractServiceProvider
中已经定义好 $middlewareAliases
中间件别名如下:
1 2 3 4 5 6 protected $middlewareAliases = [ 'jwt.auth' => Authenticate::class, 'jwt.check' => Check::class, 'jwt.refresh' => RefreshToken::class, 'jwt.renew' => AuthenticateAndRenew::class, ];
可以直接在路由定义或控制器构造函数中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 dingo()->version('v1' , [ 'namespace' => 'App\Http\Controllers\Api\V1' , 'middleware' => [ 'jwt.refresh' , ], ], function ($router) { }); public function __construct () { $this->middleware('jwt.refresh' ); }
每次请求 API 成功后,均会在响应头中的 Authorization
字段,其值便是刷新后的 Token
。
如何使用 dingo Router 中 name()
定义过的 API 路由? Dingo 定义过的的命名路由是不能用 Laravel 自带的 route 方法获取完整 URL 的,只能用 Dingo 的 URL 生成器。
举例说明:
1 2 3 4 5 6 7 8 9 10 11 12 // 定义微信授权登录回调路由$router ->post('{user}/wechat' , 'Passport@oauthloginFromWechat' ) ->name('callback.oauth.wechat' ); // 错误写法route('callback.oauth.wechat' ); // 提示找不到路由 // 正确写法app('Dingo\Api\Routing\UrlGenerator' ) ->version('v1' ) ->route('callback.oauth.wechat' , ['user' => 1 ]);
参考