还记得之前我们的文章《Laravel 2018使用数据分析——Laravel你用对了吗?学对了吗?》吗?那个是到2018年7月,基于大家的laravel使用数据,所做的分析和建议,如果你还没有看过那个,并且还没有遵循其中的最佳实践,那么首先落实好它。不管你laravel用了多久,PHP用了多久,如果那里面的数据分析和建议对你来说还很陌生,还都跟你自己实际用的不是一回事,那么基本说明你的laravel用的就是不规范、不优雅的,你就可能真的需要好好系统学习一下了。
那么一年过去了,除去那些基本的,我们Laravel Shift
的作者Jason McCreary
在今年的laravel国际会议Laracon US 2019上,又作了题为《Laracon US 2019 - Some Shifty Bits》的主题分享,分享了这一年来更高级的一些laravel优雅建议和最佳实践,一起来看看你都用上了没。
需要注意的是,可能下面的一些条目,看上去缺乏细节,这是因为这只是一个文字稿,具体详细的代码展开过程是在他《Laracon US 2019 - Some Shifty Bits》视频里演示的,所以这里有些地方就没有再额外赘述了,单纯的文字并不能很好展示代码的展开和重构细节。
想真正跟着这些国际大神学习,学习他们的编程思维,学习他们的代码编写细节和习惯,还是建议要看他们的活生生的、直观的视频,这样你得到的才不只是一些“死的”“下脚料”,而是能够用到你整个职业生涯的习惯、思维、模式等更有用的东西,更容易让你成为真正优秀的程序员的东西。
当然了,这就需要极好的英语和较扎实的基础,才能从现场视频中听懂他们到底在说啥,才能真正领会他们的演讲主旨,可能这对大部分人来说都太难。不过不要紧,我们pilishen.com已经推出了【国际IT专场】这个频道,收集了精彩的国际IT会议视频,并为大家做了精心翻译和译制。这样,只要你稍微有点基础,稍微有点想当大神的上进心,你也可以实际接触第一手的国际大牛演讲,也可以跟着最棒的大神进行学习,也可以因此在最短的时间里突破自我,日后成长为真正的国际大神。
(一)Model属性转换(映射)
laravel里可以转换model属性的数据类型,通过使用$casts,默认的,created_at
和updated_at
这两个时间属性就会被转换成Carbon实例。
我们也可以设置额外的,需要转换的属性。比如说,有个Setting
model,它从属于User
model,我们想着将它的外键(foreign key)自动转换成integer类型,假设还有个active
字段,也想着转换成boolean类型。
class Setting extends Model
{
protected $casts = [
'user_id' => 'integer',
'active' => 'boolean',
];
}
这样当我们获取相应属性的时候,它们就会自动被转换或映射成相应的类型,这不只是发生在属性读取阶段,更重要的是,在我们设置它们的值的时候,在写入的时候,也会进行相应的类型转换。
$setting = Setting::first();
// 读取属性时
dump($setting->user_id); // 1
dump($setting->active); // false
// 设置属性时
$setting->user_id = request()->input('user_id');
$setting->active = 1;
dump($setting->user_id); // 5, not "5"
dump($setting->active); // true, not 1
它的好处是,当我们request当中的数值,不是理想的类型时,比如request传过来的很可能是一个字符“5”
,因为我们设置了cast,它就能自动避免期间的一些混乱。
你也可以用它来转换一些更复杂的类型,比如array
和collection
,这经常用在将json
字段的数据,转换成PHP里的array
或者laravel里的collection
。
(二)自定义的属性转换
其实属性转换的逻辑,我们也可以在accessor
和mutator
中来做,accessor
是当我们读取一个属性时做些什么,mutator
是当我们写入一个属性时做什么,当然这期间我们可以进行数据转换。
假设我们data
字段的数据,默认存的是用|
号拼接起来的字符,我们想着在获取和设置时能转换成array来操作:
class Setting extends Model
{
public function getDataAttribute($value)
{
return explode('|', $value);
}
public function setDataAttribute($value)
{
$this->attributes['data'] = implode('|', $value);
}
}
需要注意的是,由于自身的一些限制,你并不能在它们上面直接执行一些PHP相应数据类型的操作,比如这里,既然data
读出来的是array,那么假设我想进行一个合并array的操作,下面的这种方式就并不会更新背后的数值:
$setting = Setting::first();
dump($setting->data); // [1, 2, 3]
$setting->data += [4]; //不会更改数据库中的内容
dump($setting->data); // 仍然是 [1, 2, 3]
$setting->save(); // 1|2|3
当然了,这种情况下,你可以设置个变量,对其进行更改了以后,再将这个变量整个赋值给相应的字段,类似这样:
$user = App\User::find(1);
$options = $user->options; // 设置变量
$options['key'] = 'value'; // 更改变量
$user->options = $options; // 整个赋值回去
$user->save();
(三)模型关系
很多人喜欢直接修改外键的方式,来设置两个模型之间的关系,类似这样:
$setting->user_id = $user->id;
更优雅的方式,是利用上laravel提供的一些模型关系关联的方法,比如belongsTo
关系里,可以使用associate<span> </span>
和disassociate<span> </span>
方法:
// 关联
$setting->user()->associate($user);
// 取消关联
$setting->user()->disassociate($user);
在many-to-many的关系里,你可以使用attach<span> </span>
和detach<span> </span>
方法:
// 关联
$user->settings()->attach($setting);
// 取消关联
$user->settings()->detach($setting);
在many-to-many的关系里,还有toggle
和sync
方法,它们都可以帮你避免手动写一些杂乱的逻辑。
(四)中间表
另一个跟many-to-many关系相关的是中间表的数据,因为这个时候我们有个中间表(pivot table),所以常见的需求是,使用这个中间表来存储一些额外的信息。
比如说,假设User
和Team
这两个是many-to-many关系,一个user可以在多个team中,一个team可以拥有很多个user。
但是呢,我们想着有些额外信息,比如记录一个user是否被批准加入某个team,这块信息存到哪里呢?当然,我们可以将其存到中间表里,然后在关系调取时,我们可以获取相应中间表的信息。
比如User模型里,我们想获取只有那些被批准加入了的Team:
class User extends Authenticatable
{
public function teams()
{
return $this->belongsToMany(Team::class)
->wherePivot('approved', 1);
}
}
在关系的另一端,我们想着获取一个team下所有成员的额外信息,假设是在后台页面上,我们想展示成员的审核状态,同时带上一个成员加入这个团队的时间信息。
我们就可以使用withPivot
和withTimestamps
方法来获取这些额外信息,但是我也可以使用using
方法来声明一个类,用这个类来代表这块数据,可以把这个想象成一个数据映射(cast)。
class Team extends Model
{
public function members()
{
return $this->belongsToMany(User::class)
->using(Membership::class)
->withPivot(['id', 'approved'])
->withTimestamps();
}
}
来看看这个Membership
类,它扩展的是Pivot
类,而不是我们通常的Model
类,Pivot
类背后也扩展了Model
类,所以我们可以说Membership
也是一个Model,只不过是一个Pivot Model,你可以说它是中间表模型。
它其中跟我们普通的model一样,也可以设置table
属性,也可以设置自增属性incrementing
,等等。
而且这里我还定义了它跟user和team的关系,并且让它们默认加载了(with
)。
class Membership extends Pivot
{
protected $table = 'team_user';
public $incrementing = true;
protected $with = ['user', 'team'];
public function user()
{
return $this->belongsTo(User::class);
}
public function team()
{
return $this->belongsTo(Team::class);
}
}
想象一下,多了一个Model出来,就能多出来多少便利呢,我们可以在这个中间表模型上,加上accessor,加上mutator,加上events,加上独特的方法,等等等等,也就会给你增加很多意想不到的便利。
(五)Blade视图里的简便命令
很多人在应用里,依然只是用最基本的blade命令,比如,只是用@if
标签,其实还有很多更简单、更优雅的命令:
@if(isset($records))
@isset($records) // 更优雅
@if(empty($records))
@empty($records) // 更优雅
@if(Auth::check())
@auth // 更优雅
@if(!Auth::check())
@guest // 更优雅
此外,还有两个经常在Form表单里用的:
@method('PUT')
@csrf
(六)遍历指针追踪
经常我们会在视图里用@foreach
来遍历数据,当你需要对其中的数据进行更多的掌控时,可以其中的$loop
变量,它是@foreach
内的一个内置的变量,通过$loop
你可以调用其count
、iteration
、first
、last
、even
、odd
、depth
已经更多类似属性,这对于我们掌控遍历逻辑很有用。
(七)通配符形式的View Composers
在使用 view composers时,要注意一个可能的性能影响。可能你会为了图省事,简单地在view composer里使用*
号来匹配所有的页面,尽管这是一种很好的共享数据的方式,但是也要注意,这样可能影响到你的性能。
View::composer('*', function ($view) {
$settings = Setting::where('user_id', request()->user()->id)->get()
$view->with('settings', $settings);
});
当你这样通过回调函数来分享数据时,其中的逻辑,就会在每一个视图里都被调用一次,这包括layouts,partials,component等。
所以如果你的模板调用了7个其他的视图,那么这个也会被执行7次。
所以,尽量在只是真正用到这个数据的视图上分享数据,或者将数据分享给更高层级的视图上,比如分享给layout视图。如果你确实需要针对很多个视图分享数据,可以尝试使用单例模式来调取数据:
当然,也可以利用上cache,一个道理。
(八)异常渲染
我经常见人们把try/catch
代码块放到controller里,我个人很不喜欢这样,它们总是看上去很繁重、乱糟糟的:
try {
if ($connection->isGitLab()) {
GitLabClient::addCollaborator($connection->access_token, $repository);
}
} catch (GitLabClientException $exception) {
Log::error(Connection::GITLAB . ' failed to connect to: ' . $request->input('repository') . ' with code: ' . $exception->getCode());
return redirect()->back()->withInput($request->input());
}
我们可以通过利用laravel框架提供的自定义exception渲染来移除这种需求,你可以在那个exception类里定义个render()
方法,当发生了这个异常时,laravel默认就会调用它。
这意味着你可以移除掉上面异常catch里的那些逻辑,将它们放到GitLabClientException里,让laravel自动去处理相应逻辑。
(九)API Responses
对返回的响应进行一定的格式处理,也是很常见的需求,尤其是在API里。
Shift的数据分析显示,像Fractal
这种专门格式响应的组件,是比较受欢迎的。但,其实laravel默认有提供这些功能的。
比如,你可以创建个Resource<span> </span>
类,在这个类里,你可以具体定义或格式化你的某个model的各种属性,甚至也可以定义响应返回的header信息等。
定义好格式后,需要的时候就可以将model实例传给它,再将它直接返回。不只是单个的model实例可以变更格式,laravel也提供多个model的collection集合返回格式。
具体的可以参考文档上的示例://laravel.com/docs/5.8/eloquent-resources#concept-overview
(十)用户认证逻辑
如果你应用里改了很多框架本身的核心源码,那么着就会导致你后期的版本升级很困难,或者说你完全不敢升级。十有八九,你都不需要非得改动源码的,laravel往往都提供了无缝扩展的方式或地方,你只需要找到它,做相应改动,就不会影响到核心逻辑了。
比如,你想要有额外的用户认证逻辑,想更改默认的用户认证响应,这个时候不要直接改写AuthenticatesUser
这个trait里的sendLoginResponse()<span> </span>
方法,laravel其实提供了authenticated()
方法,专门是用来让你写自己的验证成功返回逻辑的:
protected function sendLoginResponse(Request $request)
{
$request->session()->regenerate();
$this->clearLoginAttempts($request);
return $this->authenticated($request, $this->guard()->user())
?: redirect()->intended($this->redirectPath());
}
可以看到,只有当authenticated()
为空,或者返回为null
的时候,就会执行默认的重定向跳转,但如果你修改了authenticated()
方法,它本身里面没有任何逻辑,那么这时候返回不为空了,就会在认证成功后,直接调用你修改了的authenticated()
方法里的逻辑了。
所以总是要留意类似的预留的接口、方法或事件等,而不是上来就去覆写核心逻辑。
(十一)权限验证逻辑
经常,权限的验证,也会贯穿你的整个应用里。比如说,我有个video controller,它里面有相应逻辑,来确保特定的用户能观看到特定的视频。
class VideosController extends Controller
{
public function show(Request $request, Video $video)
{
$user = $request->user();
$this->ensureUserCanViewVideo($user, $video);
$user->last_viewed_video_id = $id;
$user->save();
return view('videos.show', compact('video'));
}
private function ensureUserCanViewVideo($user, $video)
{
if ($video->lesson->isFree() || $video->lesson->product_id <= $user->order->product_id) {
return;
}
abort(403);
}
}
这里我们检查lesson是否是免费的,或者当前用户是否购买了包含这个lesson的课程,否则的话,我们就抛出一个403中止的异常。
问题是,我们应用里,并不只是这一个地方需要这种检查逻辑,我们可能middleware和视图里都需要一模一样的逻辑,如何避免重复呢?
laravel提供了一种封装权限验证逻辑的方式,通过 Gates 和 Policies,Gates是一般意义上的权限检查,policies管的更多是一个model的CURD逻辑是否有权限。
这里我用gate来做演示,它通过使用一个回调函数,当验证通过时就返回true,验证失败了就返回false。
我也可以给这个gate起个简单的名字,便于后期调用它,同时也可以给它声明需要的参数,方便具体验证逻辑的执行。
Gate::define('watch-video', function ($user, \App\Lesson $lesson) {
return $lesson->isFree() || $lesson->product_id <= optional($user->order)->product_id;
});
这样,每个需要类似检查的地方,我就可以通过Gate
facade来调用我定义的这块验证逻辑。当然了,如果你是在视图里,也会有一系列简单的blade标签,比如@can
。
class VideosController extends Controller
{
public function show(Request $request, Video $video)
{
abort_unless(Gate::allows('watch-video', $video), 403);
$user = $request->user();
$user->last_viewed_video_id = $video->id;
$user->save();
return view('videos.show', compact('video'));
}
}
这样的话,我就可以移除自己的权限封装方法,同时借助abort_unless()
这个辅助函数,也即除非Gate::allows('watch-video', $video)
验证失败,返回为false时,就403中止异常。
(十二)请求签名
另一个认证相关的,是在laravel里创建一个签名了的url,这些url里包含相应数据,同时使用了HMAC
算法,来确保它们没有被更改掉。它也可以有生效时间,在一定时间内点击才能有效。
laravel不仅可以自动生成临时的url和签名的url,而且能够验证它们的有效性,提供了用于检查的middleware。
比如这里,我使用签名url来允许成员加入特定团队组。
class TeamController extends Controller {
public function __construct() {
$this->middleware('signed')->only('show');
}
public function edit(Request $request) {
$team = Team::firstOrCreate([
'user_id' => $request->user()->id
]);
$signed_url = URL::temporarySignedRoute('team.show', now()->addHours(24), [$team->id]);
return view('team.edit', compact('team', 'signed_url'));
}
}
(十三)响应和路由辅助函数
有一些很优雅的响应和路由相关的辅助函数,我们一起来看一下,看看它们的前后对比效果:
// 之前
Route::get('/', ['uses' => 'HomeController@index', 'middleware' => ['auth'], 'as' => 'home']);
Route::resource('user', 'UserController', ['only' => ['index']]);
// 之后
Route::get('/', 'HomeController@index')->middleware('auth')->name('home');
Route::resource('user', 'UserController')->only('index');
// 之前
response(null, 204);
response('', 200, ['X-Header' => 'whatever'])
// 之后
response()->noContent();
response()->withHeaders(['X-Header' => 'whatever']);
此篇是我们laravel高级课程《Laravel底层实战兼核心源码解析》的扩展文章,该篇中的很多要点,其实在我们这个课程里早就有相关讲解了。
更进一步的,如果你更厉害,或者更愿意学习,将来想成为行业大神,那么我们还给你准备了更高级的【国际IT专场会议】,在这里我们为你翻译整理了IT界的各大国际会议,PHP的作者、laravel的作者、symfony的作者等等国际顶尖大牛亲自给你讲解IT技术,多学习几个以后,你觉得自己离大神还远吗?专场链接://www.pilishen.com/casts