还记得之前我们的文章《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_atupdated_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,它就能自动避免期间的一些混乱。

你也可以用它来转换一些更复杂的类型,比如arraycollection,这经常用在将json字段的数据,转换成PHP里的array或者laravel里的collection

(二)自定义的属性转换

其实属性转换的逻辑,我们也可以在accessormutator中来做,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的关系里,还有togglesync方法,它们都可以帮你避免手动写一些杂乱的逻辑。

(四)中间表

另一个跟many-to-many关系相关的是中间表的数据,因为这个时候我们有个中间表(pivot table),所以常见的需求是,使用这个中间表来存储一些额外的信息。

比如说,假设UserTeam这两个是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下所有成员的额外信息,假设是在后台页面上,我们想展示成员的审核状态,同时带上一个成员加入这个团队的时间信息。

我们就可以使用withPivotwithTimestamps方法来获取这些额外信息,但是我也可以使用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你可以调用其countiterationfirstlastevenodddepth已经更多类似属性,这对于我们掌控遍历逻辑很有用。

(七)通配符形式的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次。

file

所以,尽量在只是真正用到这个数据的视图上分享数据,或者将数据分享给更高层级的视图上,比如分享给layout视图。如果你确实需要针对很多个视图分享数据,可以尝试使用单例模式来调取数据:

file

当然,也可以利用上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

最后修改:2023 年 03 月 07 日
如果觉得我的文章对你有用,请随意赞赏