Laravel - Some Shifty Bits
Main Thread Talks • 10 min read
This post is a write-up my talk at Laracon US 2019 - Some Shifty Bits. The talk title is a nod to Shift and not a pun for shitty bits.
It was influenced by my talk from last year's Laracon - Laravel by the Numbers - and my talk at Laracon Online - 10 practices for writing less complex, more readable code.
I received a lot of valuable feedback from these talks. So I combined them by using analytics from Shift to identify underutilized features of Laravel and demonstrate them with code.
As Laravel is an MVC framework, I'll start with model features and progress to views and controllers.
Attribute Casting
Laravel provides a way to force a particular data type for model attributes. You may do so with the $casts
property.
By default the created_at
an updated_at
attributes are cast to Carbon objects.
We can cast additional attributes as well.
For example, take a Setting
model which belongs to a User
. I may want to cast the foreign key to an integer
. Maybe there's also a bit flag called active
I want to cast to a boolean
.
1class Setting extends Model2{3 protected $casts = [4 'user_id' => 'integer',5 'active' => 'boolean',6 ];7}
Seeing this in action with a simple script, upon retrieving the data it is cast to the defined data types. But, more importantly, when we assign values to these attributes they are also cast.
1$setting = Setting::first(); 2 3dump($setting->user_id); // 1 4
dump($setting->active); // false
5 6$setting->user_id = request()->input('user_id'); 7
$setting->active = 1; 8 9
dump($setting->user_id); // 5, not "5"10dump($setting->active); // true, not 1
This is nice because when using request data (which is a string
by default) casting avoids any type issues which may arise.
You can also use more complex cast types like array
or collection
. This will automatically deserialize a JSON encoded string into a PHP array or Laravel collection.
Custom Casting
We can also cast data for more complex scenarios by creating the accessor and mutator methods for the attribute. These are getters and setters with a naming convention of the attribute name with a suffix of Attribute and prefixed with either get
or set
.
The accessor method accepts the original data and returns the data type we want. The mutator accepts this data type and sets the underlying value to the original format.
Let's see this in action with a quick snippet for casting a pipe-delimited string into an array.
1class Setting extends Model 2{ 3 public function getDataAttribute($value) 4 { 5 return explode('|', $value); 6 } 7 8 public function setDataAttribute($value) 9 {10 $this->attributes['data'] = implode('|', $value);11 }12}
Now keep in mind due to the magic nature of these methods you may not be able to perform actions on them as we would with these PHP data types.
For example, if I were to try to use the array concatenation operator it actually would not update the underlying attribute.
1$setting = Setting::first();2 3dump($setting->data); // [1, 2, 3]4 5$setting->data += [4];6 7dump($setting->data); // still [4, 5, 6]8 9$setting->save(); // 4|5|6
While the documentation doesn't explicitly discuss this, you can see it avoided in the JSON examples by setting a temporary variable and then reassigning this to attribute.
Model Relationships
Many applications directly map the relationship between two models by setting the foreign key to the key of another model.
1$setting->user_id = $user->id;
However, there is a bit more of an expressive way to define these relationships and allow the framework to do the mapping for you.
For belongs to relationships you can do this with the associate
and disassociate
methods:
1// map2$setting->user()->associate($user);3 4// unmap5$setting->user()->disassociate($user);
For many-to-many relationships you can do this with the attach
and detach
methods:
1// map2$user->settings()->attach($setting);3 4// unmap5$user->settings()->detach($setting);
For many-to-many relationships there are also toggle
and sync
methods. These help you manage the associations in bulk and avoids writing nasty logic.
Pivots
Also related to many-to-many relationships is Pivot data.
For many-to-many relationships there is an intermediate table (or pivot table). A common requirement is to leverage this table to store additional information.
For example, consider a User and Team many-to-many relationship. A user can be on many teams, and a team can have many users.
However, we may want an additional bit of data to mark the user as approved to be on a team. Where is this data stored?
Well, we can store this on the pivot table. And in the relationship, we can reference this pivot data.
So for the User
model, we want to restrict the relationship to only the teams where the user has been approved
.
1class User extends Authenticatable2{3 public function teams()4 {5 return $this->belongsToMany(Team::class)6 ->wherePivot('approved', 1);7 }8}
On the other side of this relationship we may want to get the additional information for the members of that team.
This data might be used for a dashboard display with the approved
status as well as timestamps of when they joined the team.
I can grab this additional data using the withPivot
and withTimestamps
methods. But I can leverage the using
method to specify a class to represent this data. You can think of this like a cast.
1class Team extends Model 2{ 3 public function members() 4 { 5 return $this->belongsToMany(User::class) 6 ->using(Membership::class) 7 ->withPivot(['id', 'approved']) 8 ->withTimestamps(); 9 }10}
Taking a look at this Membership
class it's actually a superset of the Model
class called Pivot
.
It has similar properties where we set the table
. In this case, I'll follow the convention of the two model names in alphabetical order.
I've also enabled the incrementing state for this pivot table since it has an incrementing, primary key column.
And I've also defined the relationships for the user
and team
to also load this data as well.
1class Membership extends Pivot 2{ 3 protected $table = 'team_user'; 4 5 public $incrementing = true;
6 7 protected $with = ['user', 'team']; 8 9 public function user()10 {11 return $this->belongsTo(User::class);12 }13 14 public function team()15 {16 return $this->belongsTo(Team::class);17 }18}
Leveraging pivot methods allows me to create a pretty complex relationship using the basic relationships of belongsTo
and belongsToMany
.
Blade Directives
Many applications still use the basic Blade directives. For example, only the @if
directive. Blade offers more expressive directives which can help you streamline your templates.
1@if(isset($records)) 2@isset($records) // expressive alternative 3 4@if(empty($records)) 5@empty($records) // expressive alternative 6 7@if(Auth::check()) 8@auth // expressive alternative 9 10 11@if(!Auth::check())12@guest // expressive alternative
In addition, there are two blade directives for method spoofing and CSRF form fields.
So instead of hard coding HTML and having to remember the proper names and values, you can use these directives instead:
1@method('PUT')2 3@csrf
Loop Iteration Tracking
Finally if you're doing any kind of iteration tracking with @foreach
directives, take a look at the loop variable.
This is a built-in option available within the foreach
block and has properties like count
, iteration
, first
, last
, even
, odd
and more which help satisfy all your iteration logic needs.
Wildcard View Composers
Finally, a performance gotcha with view composers. While these are a great way to share data, you might lazily share the the data with all views using the *
wildcard view.
1View::composer('*', function ($view) {2 $settings = Setting::where('user_id', request()->user()->id)->get()3
$view->with('settings', $settings);4});
When you do this with a closure, the containing logic will be executed for every view your template uses. This includes a layouts, partials, components, etc.
So if your template uses 7 other views this will be executed 7 times.
Instead, try to isolate sharing the data with the particular view which uses it, or share it with the highest level view (for example the layout). You can also adopt the singleton pattern to overcome this if you truly need to target many views.
Rendering Exceptions
I commonly see try
/catch
blocks within controllers. I have never been very fond of try
/catch
blocks. They have a dense and noisy syntax.
1try {2 if ($connection->isGitLab()) {3 GitLabClient::addCollaborator($connection->access_token, $repository);4 }5} catch (GitLabClientException $exception) {6 Log::error(Connection::GITLAB . ' failed to connect to: ' . $request->input('repository') . ' with code: ' . $exception->getCode());7
return redirect()->back()->withInput($request->input());8}
We can remove the need for this by leveraging the framework and custom exceptions. Instead you can define a render()
method on this custom exception and Laravel will automatically call it.
This means you can move the exception response code within this method. Then allow the code to bubble up the exception and let the framework handle it was still performing the same behavior.
API Responses
Similar to managing responses, formatting responses is something frequently performed in applications. Especially API applications.
Shift analytics confirm packages like Fractal are among the most popular. However, Laravel provides some built-in ways to do so.
For example, you may create a Resource object. Within this class you map and format your model attributes for output as well as specify any header values you might want on the response.
You pass this resource object your model and can return it directly to Laravel. It also supports collections of models if you'd like to format those responses differently.
Authentication Behavior
Something else common in applications which creates a headache when upgrading is overwriting core behavior.
I mentioned this in Laravel by the Numbers, but there are ways to hook in the various behavior Laravel provides.
For example, if you have additional authentication behavior you want to perform or to change the response, instead of overriding the sendLoginResponse()
method provided by the AuthenticatesUser
trait, Laravel provides a hook through the authenticated()
method.
1protected function sendLoginResponse(Request $request)2{3 $request->session()->regenerate();4 5 $this->clearLoginAttempts($request);6 7 return $this->authenticated($request, $this->guard()->user())8 ?: redirect()->intended($this->redirectPath());9}
Not only is the authenticated()
method called, but if it returns anything other than null
, it will be used for the response instead of redirecting the user.
Always look for these types or hooks or events instead of overwriting core code.
Authorization Logic
Finally for applications using authentication, you will likely have authorization logic as well. Unfortunately this logic often gets littered across the layers of your application.
For example, I have a video controller which contains logic to ensure the user can watch that particular video.
1class VideosController extends Controller 2{ 3 public function show(Request $request, Video $video) 4 { 5 $user = $request->user(); 6 7 $this->ensureUserCanViewVideo($user, $video); 8 9 $user->last_viewed_video_id = $id;10 $user->save();11 12 return view('videos.show', compact('video'));13 }14 15 private function ensureUserCanViewVideo($user, $video)16 {17 if ($video->lesson->isFree() || $video->lesson->product_id <= $user->order->product_id) {18 return;19 }20 21 abort(403);22 }23}
This codes checks if the lesson is free or the user has purchased a package which includes this lesson. Otherwise the application aborts with a 403
.
Now this isn't the only place in the application which performs this type of authorization check. This is done in middleware and views.
Laravel provides a way to encapsulate authorization logic using Gates and Policies.
Gates are the generic checks and policies map nicely to the CRUD operations of a model.
I'll demonstrate using a gate. It uses a callback which returns true
if they are authorized to perform a particular action and false
otherwise.
I can also name this gate for simple reference as well as pass it additional data it might need to perform the authorization check.
1Gate::define('watch-video', function ($user, \App\Lesson $lesson) {2 return $lesson->isFree() || $lesson->product_id <= optional($user->order)->product_id;3});
Now anywhere I performed this check before, I can replace using the Gate
facade and the authorization I defined. And, of course, there are also Blade directives I can use in my views.
Since Laravel encapsulates this for me, I can remove the need for my own additional encapsulation and do this directly as a guard clause within the show
action.
1class VideosController extends Controller 2{ 3 public function show(Request $request, Video $video) 4 { 5 abort_unless(Gate::allows('watch-video', $video), 403); 6 7 $user = $request->user(); 8 $user->last_viewed_video_id = $video->id; 9 $user->save();10 11 return view('videos.show', compact('video'));12 }13}
If you want to learn more about guard clauses and reducing big blocks I talk about these and other practices in the BaseCode Field Guide.
Signed Requests
One final bit dealing with authorization is the ability to create signed URL in Laravel. These URLs to have data within them and are signed using an HMAC to avoid them being tampered with.
These can also be short-lived by setting an expiration time.
Laravel not only automatically can generate these temporary URLs signed URLs, but also manages verifying them and provides middleware to attach them to URLs.
So for example, I use these signed URLs to allow members to join teams I mentioned earlier.
1class TeamController extends Controller { 2 public function __construct() { 3 $this->middleware('signed')->only('show'); 4 } 5 6 public function edit(Request $request) { 7 $team = Team::firstOrCreate([ 8 'user_id' => $request->user()->id 9 ]);10 11 $signed_url = URL::temporarySignedRoute('team.show', now()->addHours(24), [$team->id]);12 13 return view('team.edit', compact('team', 'signed_url'));14 }15}
Response and Route Helpers
Had an extra minute left on my talk, so I crammed in a bonus sample.
One of my favorite expressive features of Laravel are the fluent response and route methods.
So let's see these in action by looking a snippet with some before and after code samples.
1// before 2Route::get('/', ['uses' => 'HomeController@index', 'middleware' => ['auth'], 'as' => 'home']); 3Route::resource('user', 'UserController', ['only' => ['index']]); 4 5// after 6Route::get('/', 'HomeController@index')->middleware('auth')->name('home'); 7Route::resource('user', 'UserController')->only('index'); 8 9 10// before11response(null, 204);12response('', 200, ['X-Header' => 'whatever'])13 14// after15response()->noContent();16response()->withHeaders(['X-Header' => 'whatever']);
Want to make these changes in your code? Several of the Laravel features mentioned here including the Blade directives, fluent method chaining, and form requests are all conversions automated by the Laravel Fixer Shift.