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 Model
2{
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// map
2$setting->user()->associate($user);
3 
4// unmap
5$setting->user()->disassociate($user);

For many-to-many relationships you can do this with the attach and detach methods:

1// map
2$user->settings()->attach($setting);
3 
4// unmap
5$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 Authenticatable
2{
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.

Duplicate queries show in DebugBar

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());
7return 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// before
11response(null, 204);
12response('', 200, ['X-Header' => 'whatever'])
13 
14// after
15response()->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.