A Use Case for Pivot Models, Updating the Parent via Model Events

Don’t overlook Laravel Pivot Models; couple them with model events for powerful features.

Pivot Models, what the heck are those? You know about models in Laravel already, they represent all the stuff in your app. All the CRUDdy things. You know about pivoting, of course, especially if you grew up a millennial:

So… Pivot Models? They are actually pretty simple to understand, especially if you know a bit about database structures. Consider a common example of CRUDdy things in an app, such as posts and categories. Posts are a model, and categories are a model. They have their tables in the database. A post can belong to many categories, and categories can belong to many posts. Many-to-many. Almost every relational database represents this by a “join” table, an intermediary table representing connections between a post and a category. Sometimes, this intermediary is called a “pivot” table. Woah, there’s the word! Is it starting to become clear now? A Pivot Model is a representation of this intermediary table.

In our example above, we have our models for post and category. Often, this is enough for us to work with, especially with how easy the Eloquent ORM makes handling relationships. However, we could use a Pivot Model to represent this relationship and give us more functionality. Sticking to the Laravel naming convention (alphabetical and singular names) the table would be named “category_post” and the model would be named “CategoryPost”.

You can look at the official documentation to dive deeper, but if I may be so bold, I don’t think the documentation touches enough on why Pivot Models are so useful. Their greatest use comes with leveraging model events to supercharge your app. 

I’ve talked before about the power of model events. Thankfully, this same power extends to Pivot Models. Let’s look at a more concrete use case, and one I recently encountered in the wild: updating an invoice when adding a payment.

We have invoices, and we have payments that apply to those invoices. A payment can actually cover many invoices, and an invoice can be covered by many payments. Woah, this sure sounds like we need a pivot table, huh? Enter the Invoice Payment!

namespace App\Models;

class InvoicePayment extends Pivot // we extend the Pivot class
{
   public $incrementing = true;
}

Notice two things:

  1. We extend the Pivot class, not the Model class.

  2. We explicitly state that our table has an incrementing ID (yours might not, but if it does, you need to state it).

Now in our related Invoice and Payment models, we can explicitly state they use this intermediate pivot model with the using method:

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;

class Invoice extends Model
{
   public function payments(): BelongsToMany
   {
       return $this->belongsToMany(Payment::class)
           ->using(InvoicePayment::class)->withTimestamps();
   }

   public function paymentAmounts(): HasMany
   {
       return $this->hasMany(InvoicePayment::class);
   }

   public function amountLeft(): Attribute
   {
       return Attribute::make(
           get: function () {
               return $this->amount - $this->paymentAmounts->sum('amount');
           }
       );
   }
}

Now, an invoice has a status. To simplify, let’s say it can be one of three things: unpaid, partial, or paid. Whenever a new InvoicePayment is added, I want to update the invoice status. This is incredibly simple to do using the pivot model and the “saved” event:

// Inside the InvoicePayment class
protected static function booted(): void
{
   static::saved(function ($invoicePayment) {
       $difference = $invoicePayment->invoice->amountLeft;

       if ($difference < 0) {
           $invoicePayment->invoice->update(['status' => 'paid']);
       } else {
           $invoicePayment->invoice->update(['status' => 'partial']);
       }
   });
}

Hooking into the “saved” event, the corresponding Invoice is updated whenever an InvoicePayment is created or updated.

What do we gain from this functionality? The biggest gain, in my opinion, is not having to replicate this functionality across our app wherever we might update or create an invoice payment. We are utilizing Laravel's innate event-driven architecture to essentially automate this functionality for us. That is a huge win!

A small gotcha to this approach. When you use methods like sync and attach to create these relationships, you bypass the InvoicePayment model events, so your listeners won’t fire. There are solutions to this, including this Github package with ongoing support as of the writing of this post and a decent amount of stars. However, this is also easily dealt with by instantiating your Pivot Models directly:

InvoicePayment::create([
   'invoice_id' => $invoice,
   // ...
]);

There you have it, a great use case for using Pivot Models. I hope this was helpful. Happy developing out there!

Categories

  • Development

Tags:

  • Laravel
  • Code
  • Tutorial