DEV Community

Swapnil Bhavsar
Swapnil Bhavsar

Posted on • Originally published at swapnil.dev

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's 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
Enter fullscreen mode Exit fullscreen mode

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

But if you are in hurry, 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']);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 is going to 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 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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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']);
Enter fullscreen mode Exit fullscreen mode

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,
        ],
    // ...
]
Enter fullscreen mode Exit fullscreen mode

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']);
Enter fullscreen mode Exit fullscreen mode

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);

    // ...
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

That's it! These are 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 an awesome 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

Top comments (0)