Laravel tips

The Laravel major release schedule will slow down this year, as it moves from bi-annual to a single yearly update. There are still weekly patch releases though, so I thought I'd capture some of the newer features alongside some older-but-helpful features you may not know about.

1. Extracting data with only()

Use only() to retrieve an associative array of values with the specified keys. This method is available on a range of different classes:

// on models:
$data = $author->only(['name', 'age']);

// on Eloquent collections:
$data = $authors->only([1, 2, 3]);

// on Support collections:
$data = collect($authors->groupBy('city'))->only(['Edinburgh', 'London']);

// on requests:
$data = $request->only(['name', 'age']);

// on plain arrays:
$data = Arr::only($arr, ['name', 'age']);

2. The terminating() callback

Sometimes you may need to perform an action after your response has been sent to the browser. One option is a terminating middleware, but for a single use-case you may pass a callback to the app’s terminating() method:

app()->terminating(function () {
    // Do something before the app terminates.
});

3. Bootable traits

Want to register events or scopes on all classes that use your trait? Create a static boot{TraitName} method on the trait, and it will be called whenever a class using that trait is instantiated:

public static function bootMyTrait()
{
    static::addGlobalScope(new MyTraitScope);
}

4. The QueryExecuted event

If your query isn't working as expected, try using DB::listen() to listen for QueryExecuted events and inspect the SQL and bindings:

use Illuminate\Database\Events\QueryExecuted;
use DB;

DB::listen(function(QueryExecuted $event) {
    dump($event->sql);       // 'SELECT * FROM books WHERE isbn = ?'
    dump($event->bindings);  // ['12345']
});

$author->books()->where('isbn', '12345');

5. The $touches property

If you want to update a model's updated_at timestamp without changing any other attributes, you might already know you can call the touch() method on the model:

$author->touch();

What if you also want to update all the books related to that author? Rather than manually iterating through the relations, you can use the $touches property of the model:

class Author
{

    protected $touches = ['books'];
}

Now when you call touch() on the author, Eloquent will automatically change the updated_at timestamp on all the related books as well.

6. The hasOne...ofMany relation

A common relationship in many applications is the hasMany relation, where one model can be related to many other models. For example, an author model may have a hasMany relation to their blog posts:

class Author
{

    public function posts()
    {
        return $this->hasMany(Post::class);
    }

}

Let's say your task was to display a list of authors and their most recent post. One way would be to do a subquery using the MAX() aggregrate function, and join it to your main query, but this can be a complex query to write.

From Laravel 8.42 onwards, you can use the following syntax:

class Author
{

    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    public function latestPost()
    {
        return $this->hasOne(Post::class)->ofMany('id', 'max');
    }

}

The first argument to the toMany method is the column of the Posts table to group by, and the second being the SQL function that will be used. This makes it easy to find the latest post, most expensive product, and so on.

7. Save models without triggering events.

If you listent to Eloquent events like saving, creating or updating, sometimes you may find yourself wanting to save a model without triggering those events. The saveQuietly() or updateQuietly() methods offer a simple way to do this. (Before 8.41, this was possible with the more awkward Model::withoutEvents(function...); syntax.)

From 8.61 onwards, you can also call createQuietly() on your model factories during tests.

8. Cancelling notifications

If you dispatch notifications via the queue, sometimes you might find that your underlying data has changed between when you dispatched and when the system is processing your notification. For example, you may dispatch an “OrderShipped” notification but then cancel the order before the notification is processed.

From 8.4 onwards, if you define a shouldSend() method on your notification class, it will be called immediately prior to sending. Returing false from this method will prevent your notification from being sent to the channel:

class OrderShipped
{

    public $order;

    ...

    public function shouldSend($notifiable, $channel)
    {
        return $this->order->isShipped();
    }

}

9. Normalising user input

Sometimes you may want to normalise user input before it is passed into your application. For example, your may want to allow users to enter phone numbers in a variety of formats, but store them constistently in international “E.164” format.

You can achieve this by extending the TransformsRequest middleware. Its transform() method will be called for each key/value pair in the request’s input, and should return the transformed value.

use Illuminate\Foundation\Http\Middleware\TransformsRequest;
use \Exception;

class NormalisePhoneNumbers extends TransformsRequest
{

    public function __construct(
        protected Formatter $formatter
    ){
    }    

    protected function transform($key, $value)
    {
        if ($key === 'phone_number') {
            try {
                $value = $this->formatter->toE164($value);
            } catch (Exception $e) {}
        }
        return $value;
    }

}

Here we type-hint the formatter dependency in the middleware constructor, and only transform the value if the formatter can successfully format it. This ensures that invalid data will be passed untouched through to your validation rules.

This technique also works for nested data, with the $key being provided using “dot” syntax.

10. *OrFail() Eloquent methods

There are many scenarios in which you’d like to throw an exception when an Eloquent operation can’t be completed. Laravel makes this very easy with a range of methods:

  • findOrFail($id) - return a single Eloquent model, or throw an exception if not found.
  • firstOrFail() - return the first matching Eloquent model, or throw an exception if no models are found.
  • valueOrFail($attribute) - return the value of the named attribute on the first matching model.
  • saveOrFail() - save the model inside a transaction, or throw an exception if the transaction fails.
  • updateOrFail() - update te model inside a transaction, or throw an exception if the transaction fails.
  • deleteOrFail() - update the model inside a transaction, or throw an exception if the transaction fails.