Laravel 2019优雅技巧和最佳实践(高级篇)

想要更好的阅读体验和更多的干货分享,请直接移步个人原文出处吧:www.pilishen.com/posts/some-…

还记得以前咱们的文章《Laravel 2018使用数据分析——Laravel你用对了吗?学对了吗?》吗?那个是到2018年7月,基于你们的laravel使用数据,所作的分析和建议,若是你尚未看过那个,而且尚未遵循其中的最佳实践,那么首先落实好它。无论你laravel用了多久,PHP用了多久,若是那里面的数据分析和建议对你来讲还很陌生,还都跟你本身实际用的不是一回事,那么基本说明你的laravel用的就是不规范、不优雅的,你就可能真的须要好好系统学习一下了。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属性转换(映射)

{id="AttributeCasting"}json

laravel里能够转换model属性的数据类型,经过使用$casts,默认的,created_atupdated_at这两个时间属性就会被转换成Carbon实例。sass

咱们也能够设置额外的,须要转换的属性。好比说,有个Setting model,它从属于User model,咱们想着将它的外键(foreign key)自动转换成integer类型,假设还有个active字段,也想着转换成boolean类型。bash

class Setting extends Model
{
    protected $casts = [
        'user_id' => 'integer',
        'active' => 'boolean',
    ];
}
复制代码

这样当咱们获取相应属性的时候,它们就会自动被转换或映射成相应的类型,这不仅是发生在属性读取阶段,更重要的是,在咱们设置它们的值的时候,在写入的时候,也会进行相应的类型转换。session

$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,它就能自动避免期间的一些混乱。app

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

(二)自定义的属性转换

{id="CustomCasting"}

其实属性转换的逻辑,咱们也能够在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();
复制代码

(三)模型关系

{id="ModelRelationships"}

不少人喜欢直接修改外键的方式,来设置两个模型之间的关系,相似这样:

$setting->user_id = $user->id;
复制代码

更优雅的方式,是利用上laravel提供的一些模型关系关联的方法,好比belongsTo关系里,可使用associatedisassociate方法:

// 关联
$setting->user()->associate($user);

// 取消关联
$setting->user()->disassociate($user);
复制代码

在many-to-many的关系里,你可使用attachdetach方法:

// 关联
$user->settings()->attach($setting);

// 取消关联
$user->settings()->detach($setting);
复制代码

在many-to-many的关系里,还有togglesync方法,它们均可以帮你避免手动写一些杂乱的逻辑。

(四)中间表

{id="Pivots"}

另外一个跟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视图里的简便命令

{id="BladeDirectives"}

不少人在应用里,依然只是用最基本的blade命令,好比,只是用@if标签,其实还有不少更简单、更优雅的命令:

@if(isset($records))
@isset($records)     // 更优雅

@if(empty($records))
@empty($records)     // 更优雅

@if(Auth::check())   
@auth                // 更优雅

@if(!Auth::check())
@guest               // 更优雅
复制代码

此外,还有两个常常在Form表单里用的:

@method('PUT')

@csrf
复制代码

(六)遍历指针追踪

{id="LoopIterationTracking"}

常常咱们会在视图里用@foreach来遍历数据,当你须要对其中的数据进行更多的掌控时,能够其中的$loop变量,它是@foreach内的一个内置的变量,经过$loop你能够调用其countiterationfirstlastevenodddepth已经更多相似属性,这对于咱们掌控遍历逻辑颇有用。

(七)通配符形式的View Composers

{id="Wildcard"}

在使用 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,一个道理。

(八)异常渲染

{id="RenderingExceptions"}

我常常见人们把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

{id="Responses"}

对返回的响应进行必定的格式处理,也是很常见的需求,尤为是在API里。

Shift的数据分析显示,像Fractal这种专门格式响应的组件,是比较受欢迎的。但,其实laravel默认有提供这些功能的。

好比,你能够建立个Resource类,在这个类里,你能够具体定义或格式化你的某个model的各类属性,甚至也能够定义响应返回的header信息等。

定义好格式后,须要的时候就能够将model实例传给它,再将它直接返回。不仅是单个的model实例能够变动格式,laravel也提供多个model的collection集合返回格式。

具体的能够参考文档上的示例://laravel.com/docs/5.8/eloquent-resources#concept-overview

(十)用户认证逻辑

{id="AuthenticationBehavior"}

若是你应用里改了不少框架自己的核心源码,那么着就会致使你后期的版本升级很困难,或者说你彻底不敢升级。十有八九,你都不须要非得改动源码的,laravel每每都提供了无缝扩展的方式或地方,你只须要找到它,作相应改动,就不会影响到核心逻辑了。

好比,你想要有额外的用户认证逻辑,想更改默认的用户认证响应,这个时候不要直接改写AuthenticatesUser这个trait里的sendLoginResponse()方法,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()方法里的逻辑了。

因此老是要留意相似的预留的接口、方法或事件等,而不是上来就去覆写核心逻辑。

(十一)权限验证逻辑

{id="AuthorizationLogic"}

常常,权限的验证,也会贯穿你的整个应用里。好比说,我有个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停止异常。

(十二)请求签名

{id="SignedRequests"}

另外一个认证相关的,是在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'));
    }
}
复制代码

(十三)响应和路由辅助函数

{id="ResponseandRoute Helpers"}

有一些很优雅的响应和路由相关的辅助函数,咱们一块儿来看一下,看看它们的先后对比效果:

// 以前
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

file
file
相关文章
相关标签/搜索