This article is a continuation of Creating a Laravel SaaS Framework .

Welcome to part 5 of the Laravel Software as a Service Framework series. In this article we’ll install Laravel Cashier so that we can start requiring payments for registering with our application… because we’re not working for FREE here, are we?

This article assumes that you’ve already signed up for a Stripe account. I’ll be posting another article soon on that process if you have any questions, but for now the internet is your friend.

Installing Laravel Cashier

Start by installing Cashier. For our project, Composer will install version 10.4 and update all dependencies.

$ composer require laravel/cashier

If you consult the Laravel Cashier documentation, you’ll see that the default setup for Cashier runs on the App\User model, but that’s not what we really want with this setup. Because all of our ‘users’ are spread out between multiple databases, we need to pick a different model to use as our billable one.

What makes the most sense here is the Website model that comes with the Hyn Tenancy package. Taking a look through the Hyn Tenancy Documentation, it’s clear that the Website is the main model and we’ll use that as a stand-in for the default User.

Modifying the Default Cashier Migrations

In order to us a custom model, we’ll need to modify our migrations a little. Let’s start by publishing the migrations that come with Cashier.

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

This will create the default migrations, in the default folder.

Laravel Cashier migrations folder structure

At first, I thought of moving these to the tenant folder and running them on each tenant’s User but that would not scale well. There would be no central place to keep track of subscriptions, manage webhooks or build any type of central management. Looking up subscriptions would also require iterating over each website’s database — not good at scale.

Instead, we’ll declare a new Website model as the keeper of our subscription data. To do this, first modify the subscriptions migration. Simply change the ‘user_id’ to ‘website_id’ in two places.

database\migrations\2019_05_03_000002_create_subscriptions_table.php
    ...

    public function up()
    {
        Schema::create('subscriptions', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('website_id'); // change this
            $table->string('name');
            $table->string('stripe_id');
            $table->string('stripe_status');
            $table->string('stripe_plan');
            $table->integer('quantity');
            $table->timestamp('trial_ends_at')->nullable();
            $table->timestamp('ends_at')->nullable();
            $table->timestamps();

            $table->index(['website_id', 'stripe_status']); // also change this
        });
    }

    ...

You’ll also need to update the customer migration. Update the table to ‘websites’.

database\migrations\2019_05_03_000001_create_customer_columns.php
<?php

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

class CreateCustomerColumns extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('websites', function (Blueprint $table) { // update this
            $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::table('websites', function (Blueprint $table) { // update this
            $table->dropColumn([
                'stripe_id',
                'card_brand',
                'card_last_four',
                'trial_ends_at',
            ]);
        });
    }
}

Running the Migrations

At this point, we should be able to run our migrations and verify that the correct table and columns have been added.

$ php artisan migrate

The migration should add 4 new columns to your ‘tenancy’ database in the ‘websites’ table, and create a brand new ‘subscriptions’ table as well.

Cashier website table columns
Cashier's subscriptions table

Verify that your subscriptions table has the correct ‘website_id’ column and NOT ‘user_id’.

Cashier subscription website_id column

Creating Custom Models for Billing Tenants

We’re going to need two models in order to successfully create subscriptions. The first is the main Website model, which implements the default Hyn version and a Subscription model which will let Cashier know that we always want to use the System connection for subscriptions.

Create a New Website Model

$ php artisan make:model Website

The new model will need to implement Hyn\Tenancy\Contracts\Website and use Cashier’s Billable trait. If you’re unfamiliar with what ‘implements’ means, it’s basically a framework to follow when creating your class. Check out this Stack Overflow thread on extending and implementing.

App\Website.php
<?php

namespace App;

use Hyn\Tenancy\Abstracts\SystemModel;
use Hyn\Tenancy\Contracts\Website as WebsiteContract;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Laravel\Cashier\Billable;

class Website extends SystemModel implements WebsiteContract
{
    use SoftDeletes, Billable;

    /**
    * Get all of the hostnames for the Website.
    *
    * @return \Illuminate\Database\Eloquent\Collection
    */
    public function hostnames(): HasMany
    {
        return $this->hasMany(config('tenancy.models.hostname'));
    }

    /**
    * Get all of the subscriptions for the Website using a custom Subscription model.
    *
    * @return \Illuminate\Database\Eloquent\Collection
    */
    public function subscriptions()
    {
	return $this->hasMany(\App\Subscription::class, $this->getForeignKey())->orderBy('created_at', 'desc');
    }
}

Create a New Subscription Model

To ensure we’re always using the System connection, we’ll need our own Subscription model. This simple step will avoid any errors you might get from attempting to create these with the default setup.

$ php artisan make:model Subscription
App\Subscription.php
<?php

namespace App;

use Hyn\Tenancy\Traits\UsesSystemConnection;
use Laravel\Cashier\Subscription as CashierSubscription;

class Subscription extends CashierSubscription
{
    use UsesSystemConnection;    
}

Update The Application To Use The New Models

Now that we have our new models, anywhere that we were importing “Hyn\Tenancy\Models\Website” should be updated to “App\Website”. So far (if you’ve been following along), this should just be in our CreateTenant command and our RegisterController.

We also need to tell Cashier that we’re going to be using this new model instead of the default. In your .env file add the following:

.env
CASHIER_MODEL=App\Website

While you’re at it, this is a good time to add in your Stripe keys. Grab these from your Stripe dashboard. While building and testing, use the _test_ versions and swap these over later for production.

.env
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-secret-key

Update the services.php file with the new settings.

config\services.php
    ...

    'stripe' => [
        'model'  => env('CASHIER_MODEL'),
        'key' => env('STRIPE_KEY'),
        'secret' => env('STRIPE_SECRET'),
    ],

After that, we need to tell the Tenancy package that we’re using a new model as well. Open up the tenancy.php file and update the ‘website’ field to our new version.

config\tenancy.php
'models' => [
        /**
         * Specify different models to be used for the global, system database
         * connection. These are also used in their relationships. Models
         * used have to implement their respective contracts and
         * either extend the SystemModel or use the trait
         * UsesSystemConnection.
         */

        // Must implement \Hyn\Tenancy\Contracts\Hostname
        'hostname' => \Hyn\Tenancy\Models\Hostname::class,

        // Must implement \Hyn\Tenancy\Contracts\Website
        'website' => \App\Website::class
    ],

Now clear your cache to make sure the configuration is updated.

$ php artisan cache:clear

And you should be all configured.

Until Next Time

We now have everything we need to start adding subscriptions to our tenants. In the next article, I’ll create a couple testing plans in Stripe, update the registration form to include the Stripe Element, talk about the new Setup Intents and run through the setup of a subscription.

Stay tuned!

Additional Resources: