How to configure Laravel Cashier with multiple models

How to configure Laravel Cashier with multiple models

I recently worked on a Laravel project which had the requirement of two authenticable models along with separate subscriptions. The project, of course, was using Laravel Cashier to manage user subscriptions.

By default, Laravel Cashier assumes the App\Model\User class as a Billable model. We can configure it to use a different model, but in our case, there were two different models. So, I had to follow a different approach.

CASHIER_MODEL=App\Models\User

PS: This will be a long tutorial! I will explain everything from creating models, updating migrations, configuring webhooks, etc.

But if you are rushing, here is your solution, the trick is to set Cashier's billable model at the runtime using the config helper function.

config(['cashier.model' => 'App\Models\Seller']);

Initial Setup

Let's start by assuming that our application has two billable models, a User, and Seller. Both models will have subscriptions. There can be multiple ways to use cashier with multiple models, but for simplicity, we are going to store the details of their subscription in separate tables.

Let's start by installing the Cashier package for Stripe first.

composer require laravel/cashier

The cashier will use subscriptions and subscription_items tables to store information about the user's subscriptions. Let's publish Cashier's default migrations so that we can take over a look at the table structure.

php artisan vendor:publish --tag="cashier-migrations"

Now we should have the following files in our database/migrations directory.

  1. 2019_05_03_000001_create_customer_columns.php

  2. 2019_05_03_000002_create_subscriptions_table.php

  3. 2019_05_03_000003_create_subscription_items_table.php

These files contain schema information about the subscriptions table. Don't worry about them, we will come to these files later.

The User model setup

First, let's set up our first billable model, User with Cashier. Add Billable trait to our first billable model which at App\Models\User.

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

This user model is going to have its subscription information stored in the subscriptions table.

Next, let's create our second billable model & add migrations for it.

The Seller model setup

Our Seller model will have subscriptions like the User model. But, we need to set up a few more things than just adding a Billable trait to our model. We will need to add migrations, configure auth guard, etc. for the Seller model.

Along with the Seller model, we will create two more models, SellerSubscription & SellerSubscriptionItem. The SellerSubscription will hold the subscription information for the Seller model, and the SellerSubscriptionItem model will be responsible for holding the Multiplan Subscriptions.

In short, we are going to need the following models & tables for our Seller model.

  1. The Seller model with sellers table.

  2. The SellerSubscription model with seller_subscriptions table.

  3. The SellerSubscriptionItem model with seller_subscription_items table.

Let's start by generating our model using the following artisan command. Also, generate model & migrations files by adding the -m flag to our command.

php artisan make:model Seller -m

It should generate these two files at the following locations.

  1. Seller.php (In /app/models directory) - The Seller model

  2. 2021_XX_XX_XXXXXX_create_sellers_table.php (In /database/migrations directory) - The migration file

Now, let's set up our Seller model. Just like the User model we need to add the Billable trait to our Seller model sitting at App\Models\Seller.

use Laravel\Cashier\Billable;

class Seller extends Authenticatable
{
    use Billable;
}

In the migration file (2021_XX_XX_XXXXXX_create_sellers_table.php) for creating the sellers table, add the following schema content. We will also bring columns we got from 2019_05_03_000001_create_customer_columns.php after publishing Cashier's default migrations.

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateSellersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('sellers', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();

            // Stripe Cashier's columns
            $table->string('stripe_id')->nullable()->index();
            $table->string('card_brand')->nullable();
            $table->string('card_last_four', 4)->nullable();
            $table->timestamp('trial_ends_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('sellers');
    }
}

We are almost finished with our Seller model. But we still need to add seller specific subscriptions model and migration.

The SellerSubscription model setup

By default, subscription information for model App\Models\User will be stored in the subscriptions table. By using the Billable trait we are instructing Laravel, the User model will have a hasMany relation with the Laravel\Cashier\Subscription model. We can confirm that by the ManagesSubscriptions trait.

Here in the Laravel\Cashier\Concerns\ManagesSubscriptions trait, we can see the subscriptions method, which defines hasMany relation with the Laravel\Cashier\Subscription model.

use Laravel\Cashier\Subscription;

public function subscriptions()
{
    return $this->hasMany(Subscription::class, $this->getForeignKey())->orderBy('created_at', 'desc');
}

So, we are going to create a class called SellerSubscription which extends the Laravel\Cashier\Subscription model & inherits its properties.

In your console run the following command to generate the SellerSubscription model & migration.

php artisan make:model SellerSubscription -m

This will generate the following files.

  1. SellerSubscription.php (In /app/models directory)

  2. 2021_XX_XX_XXXXXX_create_seller_subscriptions_table.php (In /database/migrations directory)

The SellerSubscriptionItem model setup

Our User model has Multiplan Subscriptions stored in the subscription_items table. Then why should we leave the Seller model behind? Let's add multi-plan subscriptions functionality to the Seller model by defining a new model called SellerSubscriptionItem.

Let's generate the SellerSubscriptionItem model along with migration by running the following command in the terminal.

php artisan make:model SellerSubscriptionItem -m

This command should generate the following files.

  1. SellerSubscriptionItem.php (In /app/models directory)

  2. 2021_XX_XX_XXXXXX_create_seller_subscription_items_table.php (In /database/migrations directory)

Next, modify the SellerSubscription class slightly to extend Laravel\Cashier\Subscription class. And also define belongsTo relation with the Seller class as well as hasMany relation with the SellerSubscriptionItem class.

<?php

namespace App\Models;

use Laravel\Cashier\Subscription;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class SellerSubscription extends Subscription
{
    use HasFactory;

    public function owner()
    {
        return $this->belongsTo(Seller::class);
    }

    public function items()
    {
        return $this->hasMany(SellerSubscriptionItem::class);
    }
}

Next, also modify SellerSubscriptionItem class to extend Laravel\Cashier\SubscriptionItem class. And define belongsTo relation with the SellerSubscription class like this.

<?php

namespace App\Models;

use Laravel\Cashier\SubscriptionItem;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class SellerSubscriptionItem extends SubscriptionItem
{
    use HasFactory;

    public function subscription()
    {
        return $this->belongsTo(SellerSubscription::class);
    }
}

Now, it's time to take inspiration from Cashier's default migration, and update migrations for seller_subscriptions and seller_subscription_items accordingly.

Update migration 2021_XX_XX_XXXXXX_create_seller_subscriptions_table.php for seller_subscriptions table with the following schema structure. Pay attention to referencing key seller_id & modify it according to your custom model.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateSellerSubscriptionsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('seller_subscriptions', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('seller_id');
            $table->string('name');
            $table->string('stripe_id');
            $table->string('stripe_status');
            $table->string('stripe_plan')->nullable();
            $table->integer('quantity')->nullable();
            $table->timestamp('trial_ends_at')->nullable();
            $table->timestamp('ends_at')->nullable();
            $table->timestamps();

            $table->index(['seller_id', 'stripe_status']);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('seller_subscriptions');
    }
}

Next, also update migration 2021_XX_XX_XXXXXX_create_seller_subscription_items_table.php for seller_subscription_items with the following schema structure. And also pay attention to referencing key seller_subscription_id & modify it according to your custom subscription item model.

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateSellerSubscriptionItemsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('seller_subscription_items', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('seller_subscription_id');
            $table->string('stripe_id')->index();
            $table->string('stripe_plan');
            $table->integer('quantity');
            $table->timestamps();

            // Short key name to support 64 character limit- http://dev.mysql.com/doc/refman/5.5/en/identifiers.html
            $table->unique(['seller_subscription_id', 'stripe_plan'], 'seller_subscription_id_stripe_plan_unique');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('seller_subscription_items');
    }
}

After defining the SellerSubscription and SellerSubscriptionItem models, define a hasMany relation by adding the subscriptions method on the Seller class.

And finally, run the migration command to create/update tables in the database.

php artisan migrate

The Seller model

Now, modify the Seller model to override the subscriptions relation coming from the Billable trait. Instead of defining a relationship between the Laravel\Cashier\Subscription class, define it with App\Models\SellerSubscription.

Your finished seller model should look like this.

<?php

namespace App\Models;

use Laravel\Cashier\Billable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Seller extends Model
{
    use HasFactory, Billable;

    public function subscriptions()
    {
        return $this->hasMany(SellerSubscription::class)->orderBy('created_at', 'desc');
    }
}

Next, let's set up Stripe webhooks for the Seller model. By default, the cashier will use the /stripe/webhook route to handle Stripe webhooks for the default configured model.

Webhooks for the Seller model.

Stripe can notify your application in case the customer's payment method declined and many such events. We need to ensure that our application is handling it. The Cashier package makes it easy by using the /stripe/webhook route to handle these events.

By default, it handles these events for the default configured model App\Models\User. In our case, we should register a different route the handle Stripe webhooks for our custom model.

First, generate a controller called SellerWebhookController using artisan command.

php artisan make:controller SellerWebhookController

Next, update SellerWebhookController by extending Laravel\Cashier\Http\Controllers\WebhookController class. And, also modify the getUserByStripeId method to use our custom model Seller.

<?php

namespace App\Http\Controllers;

use App\Models\Seller;
use Laravel\Cashier\Http\Controllers\WebhookController;

class SellerWebhookController extends WebhookController
{
    protected function getUserByStripeId($stripeId)
    {
        return Seller::where('stripe_id', $stripeId)->first();
    }
}

After controller, register a new route SellerWebhookController to handle webhook events coming from the Stripe.

use App\Http\Controllers\SellerWebhookController;

// Route for handling Stripe events
Route::post('/stripe/seller/webhook', [SellerWebhookController::class, 'handleWebhook']);

Next, in your Stripe control panel, you should enable the following webhooks for URL https://{yourapplication.com}/stripe/seller/webhook.

  • customer.subscription.updated - When subscription is updated

  • customer.subscription.deleted - When subscription is cancelled/deleted.

  • customer.updated - When customer's information updated.

  • customer.deleted - When a customer is deleted.

  • invoice.payment_action_required - When action is required for the payment method. Typically, when the customer's card is declined.

That's it, our application is now ready to handle Stripe webhooks for the custom billable model Seller.

Configure auth guards & providers

In our application, sellers can authenticate themselves. So it makes sense to configure the Seller model to take advantage of Laravel's authentication functionality.

Start by registering a new User provider in the providers array in config/auth.php for our billable model Seller and also register a new guard in the guards array in config/auth.php file.

'guards' => [
        // ...

        'seller' => [
            'driver' => 'session',
            'provider' => 'sellers',
        ],
    // ...
],

// ...

'providers' => [
        // ...

        'sellers' => [
            'driver' => 'eloquent',
            'model' => App\Models\Seller::class,
        ],
    // ...
]

After this, we should able to retrieve our authenticated seller by using the $request->user('seller') helper.

Uses

With all the setup, we are finally ready to use our Seller model with the Cashier. Now we should be able to retrieve our authenticated seller using $request->user('seller') and override Cashier's default model using config helper.

// retrieve authenticated seller
$seller = $request->user('seller');

// override cashiers default model at the runtime
config(['cashier.model' => 'App\Models\Seller']);

Let's see it, how we can use it in actual code.

Creating a new Subscription

When creating a new subscription for our custom model Seller, retrieve it first by using the $request->user('seller') helper. And then set the cashier billable model to Seller at runtime using config helper.

use Illuminate\Http\Request;

Route::post('/seller/subscribe', function (Request $request) {
    config(['cashier.model' => 'App\Models\Seller']);

    $request->user('seller')->newSubscription(
        'default', 'price_premium'
    )->create($request->paymentMethodId);

    // ...
});

Retrieving Stripe Customer

When we need to retrieve a customer using Stripe ID, we should use Cashier::findBillable(). But before that don't forget to set Cashier's billable model using config helper.

use Laravel\Cashier\Cashier;

config(['cashier.model' => 'App\Models\Seller']);

$seller = Cashier::findBillable($stripeId);

Stripe Billing Portal

If you are using Stripe's billing portal in your application, you can redirect our custom Seller model to his billing portal like this.

use Illuminate\Http\Request;

Route::get('/billing-portal', function (Request $request) {
    // Override cashier's default model in case
    config(['cashier.model' => 'App\Models\Seller']);

    // Retrive authenticated seller
    $seller = $request->user('seller');

    return $seller->redirectToBillingPortal();
});

That's it! These are a few ways to use multiple models with the Cashier package. You can find more information about the Cashier in the documentation.

Summary

Laravel Cashier is a wonderful package for managing subscriptions in Laravel. Of course, there can be other ways to use Cashier with multiple models. This is one of the approaches you can use when you have more than two billable models.

Anyway, this was one long article, and thank you for going through the article! If you have questions about the article, then hit me up on Twitter @swapnil_bhavsar.

PS: Here is the source code for the project on GitHub - https://github.com/IamSwap/laravel-cashier-multiple-models