How to configure Laravel Cashier with multiple models
Photo by Brian Lundquist on Unsplash
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
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']);
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.
-
2019_05_03_000001_create_customer_columns.php
-
2019_05_03_000002_create_subscriptions_table.php
-
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 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.
- The
Seller
model withsellers
table. - The
SellerSubscription
model withseller_subscriptions
table. - The
SellerSubscriptionItem
model withseller_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.
-
Seller.php
(In/app/models
directory) - The Seller model -
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.
-
SellerSubscription.php
(In/app/models
directory) -
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.
-
SellerSubscriptionItem.php
(In/app/models
directory) -
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 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
rajesh, sumit, robindirksen1, ajay1991cse, driesvints liked this article