100% found this document useful (1 vote)
433 views

Laravelsaas

This document outlines how to build a SaaS application called PayMe using Laravel. PayMe allows freelancers and small businesses to create payment forms and accept payments through Stripe. The document discusses planning the app by designing wireframes and the database. It then covers building the app by creating views, authentication, payment processing, and more. Tips are provided on pricing models, legal aspects, and recommended books. Source code is available to help learn how to structure a SaaS project in Laravel.

Uploaded by

Luz DeMars
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
433 views

Laravelsaas

This document outlines how to build a SaaS application called PayMe using Laravel. PayMe allows freelancers and small businesses to create payment forms and accept payments through Stripe. The document discusses planning the app by designing wireframes and the database. It then covers building the app by creating views, authentication, payment processing, and more. Tips are provided on pricing models, legal aspects, and recommended books. Source code is available to help learn how to structure a SaaS project in Laravel.

Uploaded by

Luz DeMars
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 120

Building SaaS with Laravel

2 Max Kostinevich
Contents

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
What we are going to build . . . . . . . . . . . . . . . . . . . . 5
Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Getting the source code . . . . . . . . . . . . . . . . . . . . . 8
Working with the source code . . . . . . . . . . . . . . . . . . 9
Workspace setup . . . . . . . . . . . . . . . . . . . . . . . . . 10
About the author . . . . . . . . . . . . . . . . . . . . . . . . . 10
Contact the author . . . . . . . . . . . . . . . . . . . . . . . . 11
Chapter 1. Planning our app . . . . . . . . . . . . . . . . . . . . . . 12
Designing wireframes . . . . . . . . . . . . . . . . . . . . . . 12
Designing database . . . . . . . . . . . . . . . . . . . . . . . . 14
Chapter 2. Building our app . . . . . . . . . . . . . . . . . . . . . . 17
Creating new app . . . . . . . . . . . . . . . . . . . . . . . . . 17
Preparing views . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Static pages . . . . . . . . . . . . . . . . . . . . . . . 21
Asset compilation . . . . . . . . . . . . . . . . . . . . 23
Authentication pages . . . . . . . . . . . . . . . . . . 29
Building main features . . . . . . . . . . . . . . . . . . . . . . 34
Dashboard . . . . . . . . . . . . . . . . . . . . . . . . 34
Settings . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Payment forms . . . . . . . . . . . . . . . . . . . . . . 55
Payment form frontend . . . . . . . . . . . . . . . . . 67
Accepting payments . . . . . . . . . . . . . . . . . . . 69
Managing payments . . . . . . . . . . . . . . . . . . . 89
Dashboard stats . . . . . . . . . . . . . . . . . . . . . 93
Adding notifications . . . . . . . . . . . . . . . . . . . 100

3
Building SaaS with Laravel

Small improvements . . . . . . . . . . . . . . . . . . . 108


Building master admin . . . . . . . . . . . . . . . . . . . . . . 109
Deploying our app . . . . . . . . . . . . . . . . . . . . . . . . 114
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
Chapter 3. Useful tips . . . . . . . . . . . . . . . . . . . . . . . . . . 117
Types of SaaS . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
Thoughts on pricing and customer retention . . . . . . . . . . 117
Thoughts on legal aspects . . . . . . . . . . . . . . . . . . . . 118
Recommended books . . . . . . . . . . . . . . . . . . . . . . 119
Afterword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120

4 Max Kostinevich
Building SaaS with Laravel

Introduction

In this book you’ll learn how to design, build and launch a real-world SaaS
application using Laravel framework. This book will be useful to solopreneurs,
bootstrappers and indie makers who wanted to launch their SaaS business.
I’m going to explain step-by-step each stage of project development - from
designing wireframes to deployment.
You’ll learn how to:

• Convert your idea to wireframes


• Design database
• Convert HTML template to Laravel views
• Organize project routes
• Accept payments through Stripe and collect fees
• Distribute payments using Stripe Connect
• Send email notifications
• Install and use Laravel Horizon to manage queues
• Work with 3rd-party API to convert currencies
• Build master administration panel to manage the project
• Deploy the application

I also included the list of tips and advices which will be useful to you when you
decide to launch you SaaS.
Learn more about this book at maxkostinevich.com/books/laravel-saas

What we are going to build

The project we’re going to develop in this book is called PayMe.

Max Kostinevich 5
Building SaaS with Laravel

PayMe is a checkout payment solution to accept payments online for free-


lancers, digital artists and small agencies.
The idea of the project is pretty simple: the Seller (e.g. freelance designer)
registers on PayMe, connects their Stripe Account, creates a payment form
where he can enter service description, amount, and choose a currency. When
the payment form is created, it becomes available via unique URL, which
then could be shared with the Customer. When the Customer pays via the
payment form, the paid amount is automatically transferred to Seller’s Stripe
Account.
You may see an example of payment form on the following screenshot:

As our main goal is to validate our idea and build MVP (minimum-viable-
product) as soon as possible and spend as less time as we can, we’re not going
to implement any subscription-based features. Instead of this, we’re going to
collect some fee (e.g. percent or fixed price) on each payment made through
our app, which will be deducted automatically from the Seller’s account. You
can see the payment flow on the diagram below:

6 Max Kostinevich
Building SaaS with Laravel

On the example above, the Customer paid $10 to the Seller. From this amount
we collected $1.23 as a fee for our application. Additional $0.59 have been
deducted by Stripe as their own fee. As a result, the seller earned $8.18 net
amount ($10 - $1.23 - $0.59 = $8.18).

PayMe demo could be found using the links below:

• Live demo
• Sample payment form

Max Kostinevich 7
Building SaaS with Laravel

Prerequisites

The project we are going to develop in this book is designed for beginner
developers, however I assume that you’re familiar with Laravel framework and
know how to work with Controllers, Models, and run Artisan Commands.

We’re going to use Laravel 5.8.

If you’re absolutely new to Laravel, I’d recommend you to check some video
courses first, for example:

• Laracasts
• Codecourse
• Udemy

As we’re a going to use Stripe Connect and Stripe API to handle payments and
payouts, you’ll need to have a Stripe Account. Currently Stripe is available in
34+ countries, so if you’re living in a country where Stripe isn’t yet supported -
don’t worry, you still will be able to create development account with Stripe.

Getting the source code

The source code of the PayMe project is included to your purchase. Be sure to
carefully read the README for installation instructions.

Disclaimer

The source code is provided for learning purposes without warranty of any
kind. You’re free to use provided source code (or any parts of it) as you’d like.
Redistribution (i.e. reselling) or sharing (e.g. via public GitHub repository) is
not allowed without prior written permission.

8 Max Kostinevich
Building SaaS with Laravel

Working with the source code

For your convenience, all important steps in the source code are marked by
git tags.
You can easily switch between specific tags by using git checkout command.
For example, to switch to tag step-2.1, just type in your terminal:
git checkout step-2.1

To list all available tags, just type the following command:


git tag -n

You’ll see the list of all available tags:

During the book you may see the following notes:


Related tag: step-x.x

That means that the section or chapter has a related tag in the app source

Max Kostinevich 9
Building SaaS with Laravel

code.

Workspace setup

There are no strict requirements which tools to use for local development, you
will be fine with Homestead or Valet. I’d recommend to avoid XAMPP, WAMP
and similar software.
Personally, I use the following setup:

• Docker with installed Nginx, PHP 7, Redis and MySQL


• Ngrok to expose local server to internet over secure tunnel
• PHPStorm as my main IDE
• Notepad++ for quick edits
• Cmder as my main command-line tool

If you’re new to Docker and haven’t worked with it before, I recorded a short
video explaining how to get started with Docker for Laravel development, you
can watch it here.
I also created a ready-to-go Docker template, which is available on the
Github.
Also, if you haven’t worked with Ngrok before, I would recommend you to try
it out! Ngrok allows you expose your local server to the internet over a secure
tunnel. This tool is super-useful for testing OAuth integrations, webhooks,
3rd-party API calls and so on.

About the author

Max Kostinevich is a solutions consultant and web-developer. Max have over


10 years of extensive experience in eCommerce and SaaS consulting and de-

10 Max Kostinevich
Building SaaS with Laravel

velopment, and have worked with dozens of companies worldwide, including


multinational companies on the Inc. 5000.

Contact the author

If you have any questions, ideas, suggestions, or want to report an error, please
email me at [email protected]
For most recent updates, please follow me on Twitter: maxkostinevich

Max Kostinevich 11
Building SaaS with Laravel

Chapter 1. Planning our app

Detailed plan is the key to successful results. However, when planning the MVP,
it’s important to keep your requirements list as simple as possible, just because
you can easily got drown in all your notes, ideas, and wanted features.
So first, we need to clearly define what our project does and extract most
essential features of our project and write them down in project specification
file. Based on this file we can create wireframes to get better idea of how our
project will looks like.

Designing wireframes

So what the wireframing is? Wireframing is the process of transforming app


spec into graphicc representation at the structural level. You may think about
wireframing as about low-level design where you focus on features and layout
instead of high-level details. Wireframes helps us to get better idea of how our
project may looks like, how all features will work together and how long the
development process may take before we even write a single line of code.
When designing wireframes it’s important to not to focus too much on small
details. It’s a good idea to ask yourself - how it may looks like and how it should
work?’
Usually wireframing takes a few iterations before we get clear idea of what we
are going to build.
I recommend to make some initial wireframing on paper, as this is most quick-
est way to do this. Then you may create digital copy of your wireframes. There
are several tools which I use for wireframing:

• Sneakpeekit - just a print template for wireframes on the paper

12 Max Kostinevich
Building SaaS with Laravel

• Balsamiq - a great tool allowing to quickly create wireframes and mock-


ups
• UX-App - a web-based application allowing to create interactive wire-
frames

For PayMe I created interactive wireframes using UX App, see the image below.
You can also find these wireframes here.

This is the final version of wireframes for PayMe. Before creating these wire-
frames in UX App, I spent some time to make a few versions on paper.

After we have our wireframes on file, we can proceed to the next step - designing

Max Kostinevich 13
Building SaaS with Laravel

database.

Designing database

A good way to design database is to ask yourself the following questions:

• Which models may I need?


• Which attributes each models may need?
• How these models should be relate to one another?

It’s important to remember that our database structure may change during
the development process, and that’s totally fine. At that stage our main goal -
is to define starting point from which we can start building something.
For PayMe we can define 3 main models:

• Users (Sellers)
• Forms
• Payments (Sales)

In additional to default Laravel attributes, for each User (Seller) we’ll need to
store their connected Stripe Account ID, their profile picture, and company.
For Payment Forms we’ll need to store related User ID, UID (which will be used
in unique URL), service description, amount and currency.
For Payments we’ll need to store related Form ID, charge ID (Transaction ID
from Stripe), customer name and email, application fee we collected for that
payment, and receipt URL (which is automatically generated by Stripe).
We can also assume the following conditions:

• Each User can have multiple Forms


• Each User can have multiple Payments

14 Max Kostinevich
Building SaaS with Laravel

• Each Form can have multiple Payments


• Each Form relates to only one User
• Each Payment relates to only one Form
• Each Payment relates to only one User (the same as the owner of the
Form)
• The Payment could be refunded to the customer

As you can see on the image below, the database structure is pretty simple:

Please, note - payment model may not have a user_id, as we can get this ID
from the related Form model. However I decided to store related user_id in
Payments model too, as it will make it much easier to us to calculate statistics
and get the history of user payments.
Useful tools to design the database:

• QuickDatabaseDiagrams
• DBDiagram

Max Kostinevich 15
Building SaaS with Laravel

• Lucidchart
• Draw.io

In the next chapter we’ll start building our app.

16 Max Kostinevich
Building SaaS with Laravel

Chapter 2. Building our app

In this chapter we’re going to build our application from scratch.


After we have our wireframes and database design finished, we can start build-
ing our app.
There are no strict rules on how exactly to build the app, personally I prefer
the following sequence:

1. Create wireframes.
2. Design the database structure.
3. Prepare application plain HTML templates for each important page/lay-
out.
4. Install fresh Laravel application.
5. Convert plain HTML templates to Laravel views.
6. Start building main functionality.

For this book I will leave the creation of plain HTML templates behind the
scenes, as this is not the main focus of this book and the process is different
for each project. You may find all plain HTML templates in the source code
attached to the book.

Creating new app


Related tag: step-1.x

I assume that you already prepared your local environment. So let’s create a
new Laravel application by running the following command in our console:
laravel new payme

Max Kostinevich 17
Building SaaS with Laravel

Then let’s update our .env file and update database credentials and APP_URL
variable.

As I use Ngrok, my APP_URL variable looks as following:

APP_URL=https://payme.ngrok.io

I always force https protocol in my apps. To do this, we’ll just need to add
\URL::forceScheme('https'); to boot() method in our app/Providers/
AppServiceProvider.php:

class AppServiceProvider extends ServiceProvider


{

//...

public function boot()


{
// Force SSL
\URL::forceScheme('https');
}
}

As our app requires user registration and authentication, we need to enable


Laravel’s built-in authentication feature. To do this we’ll need just run php
artisan make:auth and php artisan migrate in our console.

After we enabled authentication, we can also enable Laravel’s built-in email


verification feature. This feature will force newly registered users to verify their
email addresses. To do this, we’ll need to make just a few things:

1. Make sure that our User model implements Illuminate\Contracts\


Auth\MustVerifyEmail contract:

18 Max Kostinevich
Building SaaS with Laravel

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements MustVerifyEmail


{
use Notifiable;

// ...
}

2. Make sure that users table have email_verified_at column (it’s in-
cluded by default).
3. After that we can pass the verify option to the Auth::routes method
to activate email verification routes:

Auth::routes(['verify' => true]);

For email testing on local development I use Mailhog which catches all email
sent by our app to it’s own local tiny SMTP server with a web-based UI. Mailhog
is included by default to my Docker template. To use Mailhog we’ll need to
update email settings in our .env file:
MAIL_DRIVER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025

Next, we can copy our HTML templates into /resources/views/_HTML folder.


This step is not necessary and it’s just my personal preference as I’d like to

Max Kostinevich 19
Building SaaS with Laravel

keep these files at one place. After we convert our HTML templates to Laravel
views, we’ll remove this folder.
At this stage we can also install some libraries which we’ll use later. I usually
install Guzzle, which helps us to make HTTP requests (e.g. API calls). To install
this library, just run the following command in your console:
composer require guzzlehttp/guzzle

Another useful package I usually install is a Laravel Horizon. Horizon helps us


to manage our Redis queues by providing web-based UI. If you’re not familiar
with queues or Horizon - don’t worry’ we’ll talk about it a little bit later. To
install horizon just run the following command in your console:
composer require laravel/horizon

After that we can install Horizon:


php artisan horizon:install

And create failed_jobs table (in this table Laravel will store all information
about our failed jobs) using the following command:
php artisan queue:failed-table

php artisan migrate

In order to use Horizon and Redis, we need update change QUEUE_CONNECTION


variable in our .env file:
QUEUE_CONNECTION=redis

That’s all, at the next step we’ll convert our HTML templates into Laravel views
and customize our authentication pages.

20 Max Kostinevich
Building SaaS with Laravel

Preparing views
Related tag: step-2.x

After we provisioned our new Laravel app, it’s time to convert HTML templates
into Laravel’s blade templates.
Depending on how you decide to organize the structure of your app, your app
may have some static pages (e.g. privacy policy, terms of service, etc.). There
are a few popular ways to do this:

• The app can be hosted on the main domain, in this case all secondary
pages should be served by the app;
• The app can be hosted on a sub-domain, e.g. app.example.com, in this
case all secondary pages could be served using separate CMS (e.g. Word-
Press) on the main domain.

In our case, we’ll have 3 static secondary pages which should be served by our
app:

• Homepage
• Terms of Service
• Privacy Policy

Let’s start by converting these pages to Laravel views.

Static pages

Related tag: step-2.1

First, let’s copy index.html, privacy.html and terms.html from /resources


/views/_HTML/ to resources/views/pages/ and change extension of these
files to .blade.php.

Max Kostinevich 21
Building SaaS with Laravel

As we have custom 404 page, let’s also copy 404.html from /resources/
views/_HTML/ to resources/views/errors/ and change the file extension
to .blade.php.

Then, let’s create a new controller called PageController to handle our static
pages. To create a new controller just run the following command in the
console:

php artisan make:controller PageController

You may notice that our controller has been created in app/Http/Controllers
/ folder.

Then we can remove welcome.blade.php view file from resources/views/


and change / route in /routes/web.php to

Route::get('/', 'PageController@home')->name('page.home');

We can also add routes to our other static pages:

Route::get('/terms', 'PageController@terms')->name('page.terms
');
Route::get('/privacy', 'PageController@privacy')->name('page.
privacy');

Then we need to create these 3 methods (home(), terms() and privacy()) in


our PageController:

22 Max Kostinevich
Building SaaS with Laravel

class PageController extends Controller


{
// Homepage
public function home()
{
return view('pages.home');
}

// Terms
public function terms()
{
return view('pages.terms');
}

// Privacy
public function privacy()
{
return view('pages.privacy');
}
}

If you check our new homepage, you may notice that it is rendered with issues,
as all assets (js, css, images) are missing. Let’s fix it now!

Asset compilation

Related tag: step-2.1

To make our static pages work correctly we need to properly setup and con-
figure the process of asset compilation. This process is depends on how your
HTML templates are actually built and in some cases may take some time to
setup everything correctly. In our case, HTML templates are built using Laravel
Mix, so it will takes us just a few minutes to setup asset compilation process.

Max Kostinevich 23
Building SaaS with Laravel

First, lets copy js/,sass/ and vendor/ folders from /resources/views/


_HTML/resources/ to /resources/

Then we need to rename core.js to app.js in /resources/js/.


Then we need to copy the content of webpack.mix.js located in /resources
/views/_HTML/ to our app’s webpack.mix.js located in the root of our appli-
cation.
Then we need to copy images from /resources/views/_HTML/public/img/
to /public/img/.
After that we need to update Bootstrap to version 4.13 in the package.json
.
We also need to install jquery-migrate library by running the following com-
mand in our console:
npm install --save jquery-migrate

After that we can build our assets by running the following command:
npm run dev

Then let’s get back to our static pages and update URLs to all missing assets.
In /resources/views/pages/home.blade.php we need to update the follow-
ing:

• Lang attribute in html tag: <html lang="{{ str_replace('_', '-',


app()->getLocale())}}">
• Add csrf-token meta tag: <meta name="csrf-token"content="{{
csrf_token()}}">
• Update page title tag: <title>{{ config('app.name', 'Laravel'
)}} - Home</title>
• Update links to favicon, css, js and images:

24 Max Kostinevich
Building SaaS with Laravel

– <link rel="shortcut icon"href="{{ asset('favicon.png')


}}">
– <link rel="stylesheet"href="{{ asset('css/app.css')}}"
>
– <script src="{{ asset('js/app.js')}}"></script>
– <img src="{{ asset('img/icon-card.svg')}}">

• Update URLs to login, sign up and other pages:

– href="{{ route('login')}}
– href="{{ route('register')}}

We need to make the same changes in /resources/views/pages/terms.


blade.php file.

As the same layout is used in Terms Of Service and Privacy Policy pages, we
can extract the layout from terms.blade.php:
First, let’s create a new file called static.blade.php in /resources/views/
layouts/ folder.

Then copy all the content from from terms.blade.php to static.blade.php.


In static.blade.php replace the content of <main> tag with the following
content:
@yield('content')

We can also update the title tag, as we’d like to have an option to pass custom
page title for each page:
<title>{{ config('app.name', 'Laravel') }} - @yield('title')</
title>

Then we can delete everything fromterms.blade.php except the content


of container div and tell the template to use our static layout by using

Max Kostinevich 25
Building SaaS with Laravel

@extends directive:

@extends('layouts.static')
@section('title', 'Terms of Service')

@section('content')
<div class="container">
<!-- Terms of Service content -->
</div>
@endsection

We can also extract the content of the footer into a component, as it will be the
same in all our pages. To do this, let’s create a footer.blade.php inside of /
resources/views/components/ folder. Then let’s copy the content of footer
tag from static.blade.php to this file. Then we can use this component in
our static layout by using @include directive:

@include('components.footer')

Do not forget to update all necessary links in the footer component.

Our finished Terms Of Service page is shown on the image below:

26 Max Kostinevich
Building SaaS with Laravel

Once we created layout file for our static pages and updated our Terms of
Service page, we can update our Privacy Policy page. To do this, we just need
to use @extends directive to tell the view to use static layout and insert the
content of the page by using @section directive:

Max Kostinevich 27
Building SaaS with Laravel

@extends('layouts.static')
@section('title', 'Privacy Policy')

@section('content')
<div class="container">
<!-- Privacy Policy content -->
</div>
@endsection

The last thing we need to do is to update our 404 page. The process will be
the same as we did this for our Homepage. We need to update the following in
/resources/views/errors/404.blade.php:

• Lang attribute in html tag: <html lang="{{ str_replace('_', '-',


app()->getLocale())}}">
• Add csrf-token meta tag: <meta name="csrf-token"content="{{
csrf_token()}}">
• Update page title tag: <title>{{ config('app.name', 'Laravel'
)}} - Not found</title>
• Update links to favicon, css, js and images:

– <link rel="shortcut icon"href="{{ asset('favicon.png')


}}">
– <link rel="stylesheet"href="{{ asset('css/app.css')}}"
>
– <script src="{{ asset('js/app.js')}}"></script>

• Update year and app name in the footer

Alright! We finished with our static pages and can move on by customizing our
authentication pages.

28 Max Kostinevich
Building SaaS with Laravel

Authentication pages

Related tag: step-2.2

Let’s customize our authentication pages. First, let’s copy login.html, signup
.html, password-reset.html, password-reset-2.html and password-
verify.html from /resources/views/_HTML/ to /resources/views/auth/
.
Then let’s start by customizing our Login page. I prefer to keep original login
.blade.php for reference until the customization is done. So first, we need to
rename original login.blade.php to something like _old_login.blade.php
and then rename login.html to login.blade.php
Then we need to update our new login.blade.php the same as we did this
for our Homepage:

• Lang attribute in html tag: <html lang="{{ str_replace('_', '-',


app()->getLocale())}}">
• Add csrf-token meta tag: <meta name="csrf-token"content="{{
csrf_token()}}">
• Update page title tag: <title>{{ config('app.name', 'Laravel'
)}} - Not found</title>
• Update links to favicon, css, js and images:

– <link rel="shortcut icon"href="{{ asset('favicon.png')


}}">
– <link rel="stylesheet"href="{{ asset('css/app.css')}}"
>
– <script src="{{ asset('js/app.js')}}"></script>

We also need to add appropriate form action and CSRF field to the login form:

Max Kostinevich 29
Building SaaS with Laravel

<form method="POST" action="{{ route('login') }}">


@csrf
<!-- ... -->
</form>

We can use original _old_login.blade.php as an example. Then we need


to apply all necessary names and classes to form input fields and add error
messages, for example:

<!-- Form Group -->


<div class="form-group">
<label class="form-label" for="email">Email address</label
>
<input type="email" class="form-control{{ $errors->has('
email') ? ' is-invalid' : '' }}" name="email" id="email
" value="{{ old('email') }}"
placeholder="Email address"/>
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
<!-- End Form Group -->

We also need to update all necessary links to Password Reset and Signup
pages, for example:

30 Max Kostinevich
Building SaaS with Laravel

@if (Route::has('register'))
<div class="col-6">
<span class="small text-muted">Don't have an account
?</span>
<a class="small" href="{{ route('register') }}">Signup
</a>
</div>
@endif

Alright, we just finished customization of our Login page, and we can remove
_old_login.blade.php as we do not need it anymore.

Before moving on with other authentication pages, we can extract the layout
from our Login page, as the same layout will be used across other authentica-
tion pages:

Let’s create a new file called auth.blade.php in /resources/views/layouts


/ folder. Then let’s copy the content of login.blade.php to this file.

Max Kostinevich 31
Building SaaS with Laravel

In auth.blade.php replace the form tag with @yield directive:


@yield('content')

And update title tag:


<title>{{ config('app.name', 'Laravel') }} - @yield('title')</
title>

Then we can remove everything fromlogin.blade.php except the content


of form tag and tell the template to use our auth layout by using @extends
directive:
@extends('layouts.auth')

@section('title', 'Login')

@section('content')
<!-- Form -->
<form class="js-validate mt-5" method="POST" action="{{
route('login') }}">
@csrf
<!-- ... -->
</form>
@endsection

Alright! Once we finished with our Login page, we can start customizing
our Registration page. First, let’s rename original register.blade.php to
_old_register.blade.php and use this file for reference.

Then let’s rename signup.html to register.blade.php and remove every-


thing except of registration form. Then we can apply our auth layout and
update registration form the same way as we did this for our login form. We
need to update form action, add CSRF field, update input names, classes and
error messages. Also we need to update the link to our Login page. After that

32 Max Kostinevich
Building SaaS with Laravel

we can remove _old_register.blade.php and proceed with Password Reset


forms.
In Laravel Password Reset feature consists of 2 separate forms:

• Password Request Form - the form where the user requests a password
reset by entering his email;
• Password Reset Form - the form where the update the password after
clicking a link which has been emailed on previous form.

Let’s start by customizing Password Request form. First, we need to


rename original email.blade.php file located in /resources/views/auth/
passwords/ to something like _old_email.blade.php (as we’d like to keep
this file for reference).
Then we need to rename password-reset.html to email.blade.php and
move this file to /resources/views/auth/passwords/ directory.
Then we need to apply auth layout the same way we did this for our Login
page and update the form action, add CSRF field, update input names, classes
and error messages. Then we can remove original _old_email.blade.php.
Next, we need to customize Password Reset form, the process is absolutely
the same:

• Rename reset.blade.php file located in /resources/views/auth/


passwords/ to _old_reset.blade.php;

• Rename password-reset-2.html to reset.blade.php and move this


file to /resources/views/auth/passwords/ directory.
• Apply auth layout;
• Update the form action, add CSRF field, update input names, classes
and error messages;

Max Kostinevich 33
Building SaaS with Laravel

• Do not forget to add a hidden field to the form which will store generated
token:
<input type="hidden" name="token" value="{{ $token }}">

The last page we need to customize - is a email verification template. Built-in


email verification feature has been introduced in Laravel 5.7 and I strongly
recommend you to use this feature in your projects.
Let’s rename verify.blade.php to _old_verify.blade.php file located in
/resources/views/auth/.

Then let’s rename password-verify.html to verify.blade.php.


This page does not contain any forms, so all we need is to apply auth layout
and update text copy and the link to Resend Password route. Then we can
remove old_verify.blade.php.
Congratulations! We just finished customization of our authentication pages
and can proceed with building features for our app.

Building main features


Related tag: step-3.x

Once we customized authentication pages for our app, we can start building
main features.

Dashboard

Related tag: step-3.1

Let’s get started by creating a Dashboard.

34 Max Kostinevich
Building SaaS with Laravel

At this step we can remove created by default HomeController located in app


/Http/Controllers/ as we don’t need it anymore, along with /home route
and home view.

Then we need to create a new DashboardController controller by running


the following command:

php artisan make:controller DashboardController

Next, we need to assign auth middleware to this controller, we can do this by


using middleware method in our controller’s constructor:

class DashboardController extends Controller


{

/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}

//...
}

Then we need to create a /dashboard route. As we’d like to allow access to our
app to users who verified email, we can apply verified middleware. So at
the end of our routes file /routes/web.php we can add the following lines:

Max Kostinevich 35
Building SaaS with Laravel

Route::group(
[
'middleware' => ['verified'],
],
function () {
Route::get('/dashboard', 'DashboardController@index')
->name('dashboard');
}
);

Next we need to copy dashboard.html from /resources/views/_HTML/ to


/resources/views/dashboard/ and rename it to index.blade.php.

Once we created a view (we’re going to customize it in a while), we need to


create a index method in our DashboardController which should return a
Dashboard view:
class DashboardController extends Controller
{
// ...

/**
* Show the application dashboard.
*/
public function index()
{
return view('dashboard.index');
}

As we do not have a /home route anymore, and our main entry-point is /


dashboard, let’s set a proper redirect to this route. There are a few options we
can do this, one option is to set a redirect in our routes file, it may looks like

36 Max Kostinevich
Building SaaS with Laravel

the following:
Route::get('/home', function () {
return redirect()->route('dashboard');
});

Or we can change $redirectTo attribute to /dashboard in all controllers (ex-


cept ForgotPasswordController) located in /app/Http/Controllers/Auth
/:

/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = '/dashboard';

And then change entry point in RedirectIfAuthenticated.php middleware


located in /app/Http/Middleware/:
class RedirectIfAuthenticated
{
//...

public function handle($request, Closure $next, $guard =


null)
{
if (Auth::guard($guard)->check()) {
return redirect('/dashboard');
}
return $next($request);
}
}

A> A small note: If built-in Email Verification feature doesn’t work properly, for

Max Kostinevich 37
Building SaaS with Laravel

example - you receive “Token Expired” error when clicking on Email Verification
link, than you need to apply a quick fix: Just go to TrustProxies.php middle-
ware located in /app/Http/Middleware/ and update $proxies attribute to
$proxies = '*';

Next let’s get back to our dashboard view located in /resources/views/


dashboard/index.blade.php and update the following:

• Lang attribute in html tag: <html lang="{{ str_replace('_', '-',


app()->getLocale())}}">
• Add csrf-token meta tag: <meta name="csrf-token"content="{{
csrf_token()}}">
• Update page title tag: <title>{{ config('app.name', 'Laravel'
)}} - Dashobard</title>
• Update links to favicon, css, js and images:

– <link rel="shortcut icon"href="{{ asset('favicon.png')


}}">
– <link rel="stylesheet"href="{{ asset('css/app.css')}}"
>
– <script src="{{ asset('js/app.js')}}"></script>

• Replace footer tag with footer component: @include('components.


footer')
• Update user name in header by adding the following code: {{ Auth::
user()->name }}

We also need to update a link to user logout. In Laravel, logout is processed by


POST request, so we need to create a hidden form for this purpose:

38 Max Kostinevich
Building SaaS with Laravel

<!-- Logout Link -->


<a href="#" onclick="event.preventDefault(); document.
getElementById('logout-form').submit();">Logout</a>
<!-- Logout Form -->
<form id="logout-form" action="{{ route('logout') }}" method="
POST" style="display: none;">
@csrf
</form>
<!-- End Logout Form -->

Then we can extract app layout and header component from our Dashboard
view as shown on the picture below:

Max Kostinevich 39
Building SaaS with Laravel

At this step we also can create a notification component and include it to


our app layout:

40 Max Kostinevich
Building SaaS with Laravel

@if ($errors->any())
<div class="alert alert-danger" role="alert">
<div class="font-weight-bold">Oops! Please, fix the
following errors:</div>
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif

@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif

We can also add a guest middleware to our homepage, as we’d like to redirect
all logged-in users straight to Dashboard. To do so just go to PageController
controller and add __construct method:

class PageController extends Controller


{
//...

public function __construct()


{
$this->middleware('guest')->only('home');
}

//...

Max Kostinevich 41
Building SaaS with Laravel

Settings

Related tag: step-3.2

After we finished our Dashboard, let’s create our Settings page.


First, let’s copy settings.html from /resources/views/_HTML/ to /
resources/views/settings/ and rename it to edit.blade.php.

Then let’s create a SettingsController by running the following command


in our console:
php artisan make:controller SettingsController

Do not forget to assign auth middleware to this controller the same way we
did this for DashboardController:
class SettingsController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}

//...
}

Then let’s add a edit() method to our SettingsController which will used
to render our settings form. At this step we can also create a placeholder for
our update():

42 Max Kostinevich
Building SaaS with Laravel

class SettingsController extends Controller


{
//...

/**
* Show the settings page.
*/
public function edit()
{
return view('settings.edit');
}

/**
* Update user settings.
*/
public function update(Request $request)
{
// @TODO: save user's settings
return true;
}

After this we can add our /settings route to our /routes/web.php file just
below /dashboard route:

Max Kostinevich 43
Building SaaS with Laravel

Route::group(
[
'middleware' => ['verified'],
],
function () {
// Dashboard
//...

// Settings
Route::get('/settings', 'SettingsController@edit')->
name('settings.edit');
Route::patch('/settings', 'SettingsController@update')
->name('settings.update');

});

As we’d like to store optional user’s company name, we need to add a column
for this to our Users table. To do this, we need to run the following command
in our console:

php artisan make:migration add_company_to_users_table --table=


users

We can specify a database table name we’d like to use by passing --table
option to our artisan command.

Let’s call our new column company and place it after user name. Our migration
will looks as follow:

44 Max Kostinevich
Building SaaS with Laravel

class AddCompanyToUsersTable extends Migration


{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('company')->after('name')->nullable
();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('company');
});
}
}

Do not forget to add dropColumn to down() method, so company column will


be deleted on migration rollback.
Then we can run our migration using the following command:
php artisan migrate

After we created our migration, we can start working on settings form itsef.

Max Kostinevich 45
Building SaaS with Laravel

First, we need to apply app layout to our settings.blade.php view. Then we


need to update our settings form:

• Update a form method to POST, set form action to action="{{ route


('settings.update')}}" and add csrf field;
• Update form fields: add proper name and value attributes;
• Add type="submit" to Submit button;

Then we need to update our update() method in SettingsController:

46 Max Kostinevich
Building SaaS with Laravel

class SettingsController extends Controller


{
//...

/**
* Update user settings.
*/
public function update(Request $request)
{
$user = auth()->user();

$request->validate([
'name' => 'required',
'email' => 'required|email|unique:users,email,' .
$user->id
]);

$user->name = $request->input('name');
$user->email = $request->input('email');
$user->company = $request->input('company');

$user->save();

return redirect()
->back()
->with('status', 'Your settings have been updated
successfully.');
}
}

Notice a little trick we used in form validation - this way we can be sure all
users have unique emails.
Next, let’s add allow user to upload their avatar or profile picture:
First, we need to add enctype="multipart/form-data"to our settings form,

Max Kostinevich 47
Building SaaS with Laravel

this way we will be able to accept user uploads. Then we need to change
upload field name to avatar. But wait, we do not have such field in our users
table, so let’s fix this by creating another migration. As you can remember, we
can do this by following command:

php artisan make:migration add_avatar_to_users_table --table=


users

And add a avatar column to our users table:

48 Max Kostinevich
Building SaaS with Laravel

class AddAvatarToUsersTable extends Migration


{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('avatar')->after('email')->nullable
();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('avatar');
});
}
}

Again, do not forget to use dropColumn on down() method, so avatar column


will be removed on migration rollback. Then run php artisan migrate to
run newly created migration.

Then we need to update update() method in SettingsController as fol-


lows:

Max Kostinevich 49
Building SaaS with Laravel

class SettingsController extends Controller


{

//...

/**
* Update user settings.
*/
public function update(Request $request)
{
$user = auth()->user();
$request->validate([
'name' => 'required',
'email' => 'required|email|unique:users,email,' .
$user->id,
'avatar' => 'image|mimes:jpeg,jpg,png,gif|max:1024
'
]);
$user->name = $request->input('name');
$user->email = $request->input('email');
$user->company = $request->input('company');

if ($request->file('avatar')) {
$avatar = $request->file('avatar')->store('uploads
', 'public');
$user->avatar = $avatar;
}

$user->save();

return redirect()
->back()
->with('status', 'Your settings have been updated
successfully.');
}
}

50 Max Kostinevich
Building SaaS with Laravel

As you can see, uploading images in Laravel can be done within just a few lines
of code: first, we validate our avatar field and make sure that uploading file is
an image. Then, we store this file in our /uploads/ directory.

Important: Do not forget to run php artisan storage:link to create the


symbolic link to your storage directory.

The last thing we need to do - is to show the uploaded avatar on our settings
form, if no avatar uploaded - let’s show a placeholder (a first letter of user’s
name):

<div class="u-lg-avatar mr-3">


@if(auth()->user()->avatar)
<img class="img-fluid rounded-circle border shadow-sm"
src="{{ url('storage/' . auth()->user()->avatar) }}">
@else
<span class="btn btn-lg btn-icon text-muted gradient-half-
primary-v2 rounded-circle border shadow-sm">
<span class="btn-icon__inner">{{ substr(Auth::user()->
name, 0, 1) }}</span>
</span>
@endif
</div>

We also wanted to allow users to delete their avatars, so let’s do this! First,
let’s create a new deleteAvatar() method in our SettingsController:

Max Kostinevich 51
Building SaaS with Laravel

class SettingsController extends Controller


{
//...

/**
* Delete user avatar
*/
public function deleteAvatar(Request $request)
{
$user = auth()->user();
if ($user->avatar) {
File::delete('storage/' . $user->avatar);
$user->avatar = '';
$user->save();
}
return redirect()
->back()
->with('status', 'Your avatar has been updated
successfully.');
}
}

As you can see, the method is pretty simple: we just check if current user have
an avatar, and if so - we just delete this file and set avatar to empty string.

Let’s also create a new route to our /routes/web.php file:

Route::delete('/settings/avatar', '
SettingsController@deleteAvatar')->name('settings.
delete_avatar');

Then we need to create a new form in our settings/edit.blade.php view,


we can place it just above our main form:

52 Max Kostinevich
Building SaaS with Laravel

<form id="delete-avatar" method="post" action="{{ route('


settings.delete_avatar') }}">
@csrf
<input type="hidden" name="_method" value="delete">
</form>

But how we actually submit this form? We can do this by adding simple
javascript to Delete button on our main form:

<button type="button" class="btn btn-sm btn-soft-secondary mb


-1 mb-sm-0"
onclick="if(confirm('Delete avatar?')){document.
getElementById('delete-avatar').submit();return false;}
">
Delete
</button>

Great! We almost done with our settings form. The one more thing we need
to take care of - is provide an option to user to change the password. Our
password update form contains 3 fields: current password, new password and
confirm new password.

First, we need to update the form itself in our settings/edit.blade.php


view:

• Update form method, action, add csrf field;


• Update input name and class attributes;
• Add type="submit" to Update Password button.

Then we can create a updatePassword() method in our SettingsController


:

Max Kostinevich 53
Building SaaS with Laravel

class SettingsController extends Controller


{
//...

/**
* Update user password
*/
public function updatePassword(Request $request)
{
$user = auth()->user();
$request->validate([
'old_password' => ['required', 'required_with:
password',
function ($attribute, $value, $fail) use (
$user) {
if (!password_verify($value, $user->
password)) {
return $fail(__('The current password
is incorrect.'));
}
}
],
'password' => 'required|required_with:old_password
|string|min:6|confirmed',
]);
$user->password = bcrypt($request->input('password'));
$user->save();
return redirect()
->back()
->with('status', 'Your password has been updated
successfully.');
}
}

First, we check if old_password and password fields are presented. Then we

54 Max Kostinevich
Building SaaS with Laravel

check if old_password match current password. If so - check if new password


is entered twice correctly by using confirmed validation rule. If everything is
ok - update the user password.
Then we need to add a route to /routes/web.php:
Route::patch('/settings/password', '
SettingsController@updatePassword')->name('settings.
update_password');

The one last thing we need to do - is to update our header component in


/resources/views/components/:

• Add user avatar;


• And add correct route to settings page;

Alright, we’re finished settings form and can move on! Let’s proceed with
making payment forms.

Payment forms

Related tag: step-3.3

Let’s start working on payment forms CRUD (Create/Read/Update/Delete). We


need to create a payment form model, migration and a resource controller.
We can create all these files using one single command:
php artisan make:model Form -mcr

Using the command above, we create a Form model, flag m creates a migration
for this model, flag c creates a controller for this model, and flag r tells artisan
that we need a resource controller.

Max Kostinevich 55
Building SaaS with Laravel

For each payment form we need to know the owner of that form, so we need to
store a user_id, we also need some kind of unique identifier (uid) which will
be used as a permalink for that particular form. You can see entire database
schema in “Designing database” chapter. Let’s add all necessary columns to
our migration file:

56 Max Kostinevich
Building SaaS with Laravel

class CreateFormsTable extends Migration


{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('forms', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->nullable();
$table->string('uid')->nullable();
$table->string('description')->nullable();
$table->integer('amount')->nullable();
$table->string('currency')->nullable();
$table->tinyInteger('is_active')->unsigned()->
nullable()->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('forms');
}

Then we need to add relations to our models, so we’ll be able to get all User
forms:

Max Kostinevich 57
Building SaaS with Laravel

class User extends Authenticatable implements MustVerifyEmail


{
//...

// User forms
public function forms()
{
return $this->hasMany('App\Form');
}
}

And vice versa - we need to add a user() method in order to be able to get an
owner for a particularForm

class Form extends Model


{
// Owner of the form
public function user()
{
return $this->belongsTo('App\User');
}
}

As we already have placeholders for all CRUD methods in our FormController,


we can add all needed routes to /routes/web.php:

58 Max Kostinevich
Building SaaS with Laravel

Route::group(
[
'middleware' => ['verified'],
],
function () {
//...

// Forms
Route::get('/forms', 'FormController@index')->name('
forms.index');
Route::get('/forms/create', 'FormController@create')->
name('forms.create');
Route::post('/forms/create', 'FormController@store')->
name('forms.store');
Route::get('/forms/{uid}', 'FormController@edit')->
name('forms.edit');
Route::patch('/forms/{form}', 'FormController@update')
->name('forms.update');
Route::delete('/forms/{uid}', 'FormController@destroy'
)->name('forms.destroy');
});

Then let’s prepare our views. First, we need to copy forms.html from
/resources/views/_HTML/ to /resources/views/forms/ and rename it
to index.blade.php. Then let’s apply app layout to this view and update
index() method in our FormController:

Max Kostinevich 59
Building SaaS with Laravel

class FormController extends Controller


{
//...

/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$forms = auth()->user()->forms()->orderBy('id', 'desc'
)->paginate(25);
return view('forms.index', compact('forms'));
}
}

We’ll get back to index.blade.php file a little bit later. Meanwhile let’s pre-
pare form to create and edit our payment forms: copy form-new.html from
/resources/views/_HTML/ to /resources/views/forms/ and rename it to
edit.blade.php. Then apply app layout and update form method to POST and
form action to {{ $form->id ? route('forms.update', $form): route(
'forms.store')}}. As we’re going to use the same form for create and update
our payment forms, we can use the following little trick here:
@if($form->id)
<input type="hidden" name="_method" value="patch">
@endif

If $form->id is exists, we use forms.update route and add a patch method


to update existing payment form, otherwise we use store method to create a
new payment form.
Let’s update store() method in FormController, in our case it will looks as

60 Max Kostinevich
Building SaaS with Laravel

follow:

class FormController extends Controller


{
//...

/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$form = new Form();
$form->user_id = auth()->user()->id;
return $this->update($request, $form);
}

//...
}

In the method above we create a new Form, assign current logged-in user to it
and then call update() method, which looks as follow:

Max Kostinevich 61
Building SaaS with Laravel

class FormController extends Controller


{
//...

/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Form $form
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Form $form)
{
$request->validate([
'description' => 'required',
'amount' => 'required|numeric|min:1',
'currency' => 'required',
]);
$form->description = $request->input('description');
$form->amount = (float)str_replace(',', '', $request->
input('amount')) * 100;
$form->currency = $request->input('currency');
$form->is_active = $request->input('is_active');
$form->uid = $form->uid ?? uniqid();
$message = $form->id ? 'Payment form has been updated
successfully' : 'Payment form has been created
successfully';
$form->save();
return redirect()
->route('forms.edit', $form)
->with('status', $message);
}

//...
}

62 Max Kostinevich
Building SaaS with Laravel

First, we validate incoming data, then we update form attributes. If this is a new
form - we also generating an uid by using uniqid() function. Then we store
that form and redirect back to form edit view with a successful message.

As Stripe stores all amounts in cents, we need to multiple input amount


by 100. So, for example, if payment amount is $100.00, we need to store
10000 in our database. To show formatted amount to the user, we can add a
amountFormatted() method to our Form model:

class Form extends Model


{
//...

// Return formatted amount


public function amountFormatted()
{
return number_format($this->amount / 100, 2, '.', ',')
;
}

//...
}

Now let’s get back to our forms/index.blade.php view and show the list of
user’s forms. As we’re already passing user forms to the view via the index()
method in FormController, we can use $forms variable to show the forms.
We can use for, foreach, while or forelse statements for this purpose. Let’s
use forelse statement, and show No records found message if user haven’t
created any forms yet:

Max Kostinevich 63
Building SaaS with Laravel

<tbody class="font-size-1">
@forelse($forms as $form)
<tr>
<td class="align-middle font-weight-normal">
<a href="#" target="_blank" class="d-block text-{{
$form->is_active ? 'success' : 'muted' }}
small">
<span class="fas fa-circle small mr-1"></span>
{{ $form->uid }}</a>
</td>
<td class="align-middle">
<span class="d-block">{{ $form->
amountFormattedWithCurrency() }}</span>
<span class="d-block text-muted small">{{ $form->
description }}</span>
</td>
<td class="align-middle">
<span class="d-block">2,390.00 USD</span>
<a href="#" class="link-muted small">10 payments</
a>
</td>
<td class="align-middle">
<a href="{{ route('forms.edit', $form->uid) }}"
class="small mr-3"><span class="fas fa-edit"></
span> Edit</a>
<a href="#" class="small text-danger"></span>
Delete</a>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="align-center">
<strong>No records found</strong><br>
</td>
</tr>
@endforelse
</tbody>

64 Max Kostinevich
Building SaaS with Laravel

Notice we added a amountFormattedWithCurrency() to Form model to show


the formatted amount with currency:

class Form extends Model


{
//...
// Return formatted amount with currency
public function amountFormattedWithCurrency()
{
return $this->amountFormatted() . ' ' . strtoupper(
$this->currency);
}
}

In this function we’re using amountFormatted() method created in previous


step.

We’ll add all missing information such as link to the form frontend, total pay-
ments and transactions amount in the next steps.

Meanwhile, let’s also add an option to delete the payment form.

First, let’s update destroy() method in our FormController:

Max Kostinevich 65
Building SaaS with Laravel

class FormController extends Controller


{
//...

/**
* Remove the specified resource from storage.
*/
public function destroy($uid)
{
$form = Form::where('id', $uid)
->orWhere('uid', $uid)
->firstOrFail();

if (auth()->user()->id != $form->user_id) {
return abort(401);
}

$form->delete();

return redirect()
->route('forms.index')->with('status', 'Payment
form has been deleted successfully');
}
}

As you can see, this method is pretty simple: first, we’re trying to find a form
with provided uid, then we’re checking if logged-in user is the owner of that
form (in other words - if current logged-in user have rights to delete that form).
If everything is ok - we’re deleting the form and redirecting user back to forms
.index view with some message.

Then let’s add delete form to our forms.index view:

66 Max Kostinevich
Building SaaS with Laravel

<a href="#" class="small text-danger" onclick="if(confirm('


Delete this record?')){document.getElementById('delete-
entity-{{ $form->uid }}').submit();return false;}"><span
class="far fa-trash-alt"></span> Delete</a>

<form id="delete-entity-{{ $form->uid }}" action="{{ route('


forms.destroy', $form->uid) }}" method="POST">
<input type="hidden" name="_method" value="DELETE">
@csrf
</form>

We’re doing a little trick here: we added a non-visible form with id property,
we also added a javascript event handler to Delete link to trigger that form.
Alright, we almost finished with payment forms! A few things we need to do:

• Create a custom pagination component, see /resources/views/


components/pagination.blade.php for more information;
• Add links to Payment Forms in our header

After that we can proceed with preparing a frontend view for the payment
form.

Payment form frontend

Related tag: step-3.4

Let’s prepare view for our customer-facing payment form. First, we need to
copy payment-form.html from /resources/views/_HTML/ to /resources/
views/payment-form/ and rename it to show.blade.php.

Then we need to create a PaymentFormController and add a show() method


to it:

Max Kostinevich 67
Building SaaS with Laravel

class PaymentFormController extends Controller


{
// Show the payment form
public function show($uid)
{
$form = Form::where('id', $uid)
->orWhere('uid', $uid)
->where('is_active', 1)
->firstOrFail();
return view('payment-form.show', compact('form'));
}
}

In show() method we select an active payment form by id or uid and render


the payment form view we recently created.
Then we need to add a new route to /routes/web.php:
// Payment Form
Route::get('/p/{uid}', 'PaymentFormController@show')->name('
form.show');

Let’s get back for a second to our /resources/views/forms/index.blade.


php and add a link to front-end payment form:

<!--- ~Line:44 -->


<td class="align-middle font-weight-normal">
<a href="{{ route('form.show', $form->uid) }}" target="
_blank" class="d-block text-{{ $form->is_active ? '
success' : 'muted' }} small">
<span class="fas fa-circle small mr-1"></span>
{{ route('form.show', $form->uid) }}</a>
</td>

Then let’s finish our payment form view, we need to update paths to our assets,

68 Max Kostinevich
Building SaaS with Laravel

the same as we did this for our Homepage:

• Lang attribute in html tag: <html lang="{{ str_replace('_', '-',


app()->getLocale())}}">
• Add csrf-token meta tag: <meta name="csrf-token"content="{{
csrf_token()}}">
• Update page title tag: <title>{{ config('app.name', 'Laravel'
)}} - Make a payment</title>
• Update links to favicon, css, js and images:

– <link rel="shortcut icon"href="{{ asset('favicon.png')


}}">
– <link rel="stylesheet"href="{{ asset('css/app.css')}}"
>
– <script src="{{ asset('js/app.js')}}"></script>

• Update payment form data (user name, avatar, payment amount, pay-
ment description, etc).

Great! In next chapter we start working on accepting payments via Stripe.

Accepting payments

Related tag: step-3.5

In this chapter we are going to add payment processing feature to our app.
First, we need to install stripe-php package, to do this just enter the following
command to your console:
composer require stripe/stripe-php

Max Kostinevich 69
Building SaaS with Laravel

As we’re going to use Stripe Connect to make payouts, we need to store Stripe
Account ID for each of our users. Let’s create a new migration using the follow-
ing command:

php artisan make:migration add_stripe_to_users_table --table=


users

And then add a new stripe_account_id field, so our migration will look as
follows:

70 Max Kostinevich
Building SaaS with Laravel

class AddStripeToUsersTable extends Migration


{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('stripe_account_id')->after('email'
)->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('stripe_account_id');
});
}
}

Do not forget to run run this migration using php artisan migrate com-
mand.

Then, let’s create a new config file to store our Stripe credentials. To do this
just create a new stripe.php file in /config directory with the following con-
tent:

Max Kostinevich 71
Building SaaS with Laravel

<?php
// Stripe Settings
return [
// Stripe Publishable Key
'publishable_key' => env('STRIPE_PUBLISHABLE_KEY', ''),
// Stripe Secret Key
'secret' => env('STRIPE_SECRET', ''),
// Stripe Connect Client ID
'client_id' => env('STRIPE_CLIENT_ID', ''),
];

Now we can use config() helper function to get the value of config variable, for
example, to get a Stripe Client ID we would call config('stripe.client_id'
). As all secret keys and passwords should be stored in .env file, we are using
env() helper function to pass actual value from our .env file.

To get your Stripe API keys login to your Stripe Dashboard (or create a new
account if you haven’t registered yet) and go to Developers -> API keys on
the left sidebar menu. Please, note: If you want to get API keys for your devel-
opment (test) application, do not forget to switch View Test Data option as
shown on the image below:

To get Stripe Client ID, you need to go to Settings -> Connect settings as
shown on the image below:

72 Max Kostinevich
Building SaaS with Laravel

And then copy Client ID value:

Also at this step you may add a URI Redirect to https://YOURAPP/stripe


/authenticate (do not forget to replace YOURAPP with your app domain
name/or with your Ngrok subdomain if you’re developing on your local
machine). This URI will be used to authenticate users via Stripe Connect
OAuth, so they will be able to accept the payments directly to their Stripe
account.

Next, let’s create a Service Provider where we bind our Stripe API object to
singleton. Service Providers are usually used to instantiate API wrappers,

Max Kostinevich 73
Building SaaS with Laravel

packages and other components used in our app..

For example, let’s say we’re building application which interacts with Shopify
via API. Instead of instantiating Shopify API Wrapper object each time, we can
create Shopify Service Provider and instantiate Shopify API wrapper only once,
it may looks as follows:

<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
//...

class ShopifyServiceProvider extends ServiceProvider


{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->singleton('ShopifyAPI', function () {
$api = new ShopifyAPI();
$api->setApiKey('Shopify API Key');
$api->setShop('myawesomestore.myshopify.com');
return $api;
});
}
//...
}

Then in our controller we can use this Shopify API object :

74 Max Kostinevich
Building SaaS with Laravel

<?php
namespace App\Http\Controllers;
//...
class OrderController extends Controller
{
public function index()
{
$shopify = resolve('ShopifyAPI');
$orders = $shopify->getOrders();
return orders;
}
//..
}

Visit laracasts.com if you want to learn more about Service Providers and
Service Containers, Jeffrey Way did a great job explaining them.

Let’s get back to our StripeServiceProvider. To create a new Service


Provider, run the following command in your console:

php artisan make:provider StripeServiceProvider

And then edit register() method in app/Providers/StripeServiceProvider


.php:

Max Kostinevich 75
Building SaaS with Laravel

<?php
namespace App\Providers;
//...

class StripeServiceProvider extends ServiceProvider


{
public function register()
{
$this->app->singleton(Stripe::class, function () {
Stripe::setApiKey(config('stripe.secret'));
Stripe::setClientId(config('stripe.client_id'));
return new Stripe();
});
}

//...
}

Then add \App\Providers\StripeServiceProvider::class to config/app


.php to register Stripe Service Provider.

As Stripe API Wrapper uses static methods, we do not need to store instantiated
object in a variable:

<?php
//...
$charge = \Stripe\Charge::create(
[
'amount' => 1000,
'currency' => 'USD',
]
);

Then we need to create routes for Stripe oAuth in our routes/web.php file:

76 Max Kostinevich
Building SaaS with Laravel

Route::group(
[
'middleware' => ['verified'],
],
function () {
//...
// Stripe oAuth
Route::get('/stripe/oauth', '
StripeOAuthController@oauth')->name('stripe.oauth')
;
Route::get('/stripe/authenticate', '
StripeOAuthController@authenticate')->name('stripe.
authenticate');
Route::post('/stripe/deactivate', '
StripeOAuthController@deactivate')->name('stripe.
deactivate');
//...
});

Then let’s update our StripeOAuthController.php controller as follows:

Max Kostinevich 77
Building SaaS with Laravel

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Stripe\Stripe;

class StripeOAuthController extends Controller


{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct(Stripe $stripe)
{
$this->middleware('auth');
}

public function oauth()


{
$url = \Stripe\OAuth::authorizeUrl([
'scope' => 'read_write',
]);
return redirect($url);
}

public function authenticate(Request $request)


{
$user = auth()->user();
if ($request->get('code')) {
// The user has been redirected back from Stripe
with an authorization code.
$code = $request->get('code');
try {
$response = \Stripe\OAuth::token([
'grant_type' => 'authorization_code',
'code' => $code,
78 ]); Max Kostinevich
} catch (\Stripe\Error\OAuth\OAuthBase $e) {
exit("Error: " . $e->getMessage());
}
Building SaaS with Laravel

Then let’s get back to our Settings page at /resources/views/settings/edit


.blade.php and add Stripe oAuth authorization/deactivation link:

Max Kostinevich 79
Building SaaS with Laravel

<!-- Connect Stripe -->


<div class="border-bottom mb-3 pb-3">
<div class="card">
<div class="card-body p-5 text-center">
@if(auth()->user()->stripe_account_id)
<span class="btn btn-icon btn-soft-success text-
success rounded-circle m-3">
<span class="btn-icon__inner"><span class="fas
fa-check"></span></span>
</span>
<span class="d-block text-muted small">Your Stripe
Account is connected.</span>
<a href="#" class="d-block text-danger small"
onclick="if(confirm('Deactivate Stripe Account
?')){document.getElementById('deactivate-stripe
-account').submit();return false;}">Deactivate
</a>
<form id="deactivate-stripe-account" method="post"
action="{{ route('stripe.deactivate') }}">
@csrf
</form>
@else
<a href="{{ route('stripe.oauth') }}" class="btn
btn-primary mb-3">Connect Stripe Account</a>
<span class="d-block text-muted small">In order to
get paid, please connect your
<a href="https://stripe.com"
target="_blank">Stripe</a>
account
</span>
@endif
</div>
</div>
</div>
<!-- End Connect Stripe -->

80 Max Kostinevich
Building SaaS with Laravel

Then let’s create Payment model and migration for it by using the following
command:

php artisan make:model Payment -m

Then let’s add all needed columns to our payment migration, so it will looks
as follow:

Max Kostinevich 81
Building SaaS with Laravel

class CreatePaymentsTable extends Migration


{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('payments', function (Blueprint $table)
{
$table->bigIncrements('id');
$table->bigInteger('user_id')->nullable();
$table->bigInteger('form_id')->nullable();
$table->string('charge_id')->nullable();
$table->string('customer_name')->nullable();
$table->string('customer_email')->nullable();
$table->integer('amount')->nullable();
$table->integer('application_fee_amount')->
nullable();
$table->string('currency')->nullable();
$table->string('receipt_url')->nullable();
$table->tinyInteger('is_refunded')->unsigned()->
nullable()->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('payments');
}
}

82 Max Kostinevich
Building SaaS with Laravel

Then add connection to users to Payments model:


class Payment extends Model
{
// Payment receiver
public function user()
{
return $this->belongsTo('App\User');
}
}

Then connect User:


class User extends Authenticatable implements MustVerifyEmail
{
//...
// Payments
public function payments()
{
return $this->hasMany('App\Payment');
}
}

and add Form model connections:


class Form extends Model
{
//...
public function payments()
{
return $this->hasMany('App\Payment');
}
}

Then let’s proceed to handling the payments.


Let’s update our payment form in /resources/views/payment-form/show.

Max Kostinevich 83
Building SaaS with Laravel

blade.php. We need to make the following changes:

• Replace payment-form div with form: <form method="post"id="


payment-form"class="payment-form">
• Update form input names, make name and email fields required
• Add div where we’ll display any error messages: <div id="card-errors
"class="text-danger small"></div>
• Add hidden field where we’ll store Stripe token: <input type="hidden
"name="stripeToken"id="stripeToken"value="">
• Add javascript function which will generate token using Stripe Elements
and pass data to PaymentFormController via AJAX:

84 Max Kostinevich
Building SaaS with Laravel

// Initialize Stripe object


var stripe = Stripe('{{ config('stripe.publishable_key') }}');
// Create Stripe Element
var elements = stripe.elements();
var style = {};
// Attach card number field to Stripe Element
var cardNumber = elements.create('cardNumber', {
'placeholder': '0000 0000 0000 0000',
'style': style
});
cardNumber.mount('#card-number');
// Attach expiration date field to Stripe Element
var expDate = elements.create('cardExpiry', {
'placeholder': 'DD/YY',
'style': style
});
expDate.mount('#card-expiration');
// Attach CVC field to Stripe Element
var cardCVC = elements.create('cardCvc', {
'placeholder': 'CVC',
'style': style
});
cardCVC.mount('#card-cvc');

// Handle form submission


$('#payment-form').on('submit', function (e) {
e.preventDefault();
// Clear error message
$('#card-errors').html('');
// Generate Stripe Token
stripe.createToken(cardNumber).then(function(result) {
if (result.error) {
// Show Card error message
$('#card-errors').html(result.error.message);
} else {
stripeTokenHandler(result.token);
}
});
Max
});Kostinevich 85
// Send Payment data with Tokent to Payment Form Controller
function stripeTokenHandler(token) {
// Update Stripe Token ID
var form = $('#payment-form');
Building SaaS with Laravel

Then we need to add a new route to /routes/web.php:

Route::post('/p/{uid}', 'PaymentFormController@store')->name('
form.store');

Next, let’s go to our PaymentFormController and create store() method.


In this method we need to create a new charge using Stripe API and store
information about the payment in our database.

86 Max Kostinevich
Building SaaS with Laravel

class PaymentFormController extends Controller


{
//...
// Process the payment via AJAX
public function store(Request $request)
{
// Get the form
$uid = $request->route('uid');
$form = Form::where('uid', $uid)
->where('is_active', 1)
->with('user')
->firstOrFail();
try {
$token = $request->input('stripeToken');
// create charge
$charge = \Stripe\Charge::create(
array(
'amount' => $form->amount,
'currency' => $form->currency,
'source' => $token,
'application_fee_amount' =>
get_payment_fee($form->amount),
'transfer_data' => [
'destination' => $form->user->
stripe_account_id,
],
)
);
// Create new payment
$payment = new Payment;
$payment->user_id = $form->user->id;
$payment->form_id = $form->id;
$payment->customer_name = $request->input('
customer_name');
$payment->customer_email = $request->input('
customer_email');
$payment->charge_id = $charge->id;
Max Kostinevich$payment->amount = $charge->amount; 87
$payment->currency = $charge->currency;
$payment->application_fee_amount = $charge->
application_fee_amount;
$payment->receipt_url = $charge->receipt_url;
Building SaaS with Laravel

A few notes:

As we want to send the payment directly to the Seller, we set transfer_data


->destination property to user’s connected Stripe Account ID. At the same
time we also want to keep some part of the payment as our fee, to do this we
can create a get_payment_fee helper function in /app/helpers.php:

<?php
// Calculate application fee (in cents) for the payment
function get_payment_fee($amount)
{
// Get 5% + 50 cents
$fee = $amount * 0.05 + 50;
$fee = round($fee);
return $fee;
}

Do not forget to register this file in composer.json, to do this just add autoload
property:

"autoload": {
"psr-4": {
"App\\": "app/"
},
"files": [
"app/helpers.php"
],
//...
}

And then run composer dump-autoload command in your console.

Great! In next step we’ll add an option to manage payments through the
dashboard.

88 Max Kostinevich
Building SaaS with Laravel

Managing payments

Related tag: step-3.6

Now we would like to allow our users to see received payments and make
refunds.

First, let’s create PaymentController and useindex() method to show user’s


payments:

class PaymentController extends Controller


{
//...
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$payments = auth()->user()->payments()->orderBy('id',
'desc')->paginate(25);
return view('payments.index', compact('payments'));
}
}

Then we need to create payments view in /resources/views/payments/


index.blade.php, and add relationship to Payment model:

Max Kostinevich 89
Building SaaS with Laravel

class Payment extends Model


{
//...
// Payment form
public function form()
{
return $this->belongsTo('App\Form');
}
//...
}

Next, let’s add an option to refund a specific payment. This feature will al-
low our users to make decision about refunds on their own, and make it
easier for us to maintain the project. We’ll be using update() method in
PaymentController we created earlier. In this method we make an attempt to
make a refund throught Stripe API and, in case of success, mark the payment
as refunded:

90 Max Kostinevich
Building SaaS with Laravel

class PaymentController extends Controller


{

//...
public function update(Request $request, Payment $payment)
{
if (auth()->user()->id != $payment->user_id) {
return abort(401);
}

try {
$refund = \Stripe\Refund::create([
'charge' => $payment->charge_id,
'refund_application_fee' => true,
'reverse_transfer' => true
]);

$payment->is_refunded = 1;
$payment->save();

return redirect()
->back()->with('status', 'Refund has been
processed successfully');
} catch (Exception $e) {
logger()->error($e->getMessage());
}

return redirect()
->back()->withErrors('There was an error
encountered.');
}

Then we need to update /resources/views/payments/index.blade.php

Max Kostinevich 91
Building SaaS with Laravel

view and add a “Refund” link to each payment record (the same way we did it
to handle payment form deletion):

<td class="align-middle">
<a href="{{ $payment->receipt_url }}" target="_blank"
class="text-primary small mr-3"><span class="fas fa-
receipt"></span> Receipt</a>
@if($payment->is_refunded)
<span class="small text-muted"><span class="fas fa-
circle small"></span> Refunded</span>
@else
<a href="#" class="text-danger small" onclick="if(confirm
('Refund this payment?')){document.getElementById('
refund-entity-{{ $payment->id }}').submit();return
false;}"><span class="fas fa-redo-alt"></span> Refund</
a>
<form id="refund-entity-{{ $payment->id }}" action="{{
route('payments.update', $payment) }}" method="POST">
<input type="hidden" name="_method" value="PATCH">
@csrf
</form>
@endif
</td>

Do not forget to add all necessary routes to /routes/web.php and update


header navigation in /resources/views/components/header.blade.php
.

Then let’s add payment information to payment forms view (/resources/


views/forms/index.blade.php):

92 Max Kostinevich
Building SaaS with Laravel

<td class="align-middle">
<span class="d-block">{{ amountFormattedWithCurrency($form
->payments->sum('amount'), $form->currency) }}</span>
<a href="{{ route('form.payments.index', $form->uid) }}"
class="link-muted small">{{ $form->payments()->count()
}} {{ Str::plural('payment', $form->payments()->count()
) }}</a>
</td>

And finally, let’s add recent payments to the dashboard:


class DashboardController extends Controller
{
//...
public function index()
{
$payments = auth()->user()->payments()->orderBy('id',
'desc')->limit(10)->get();
return view('dashboard.index', compact('payments'));
}
}

and do not forget to update dashboard view and add a link to all payments in
/resources/views/components/header.blade.php.

Dashboard stats

Related tag: step-3.7

After we added payments handling and management, we can add some pay-
ment statistics to our dashboard. As payments can be made in different cur-
rencies, we want to convert all payments to USD.
First, let’s add all available currencies to our config/app.php file:

Max Kostinevich 93
Building SaaS with Laravel

return [
//...

// Available currencies
'currencies' => [
'usd',
'eur',
'cad',
'aud',
],
//...
];

Now we can get the list of all available currencies using the config function,
for example: $currencies = config('app.currencies');
Then we need to use this currency list in Edit Payment Form view located in
/resources/views/forms/edit.blade.php:

<div class="col-5">
<select name="currency" class="form-control" {{ $form->id
? 'disabled' : '' }}>
@foreach(config('app.currencies') as $currency)
<option value="{{ $currency }}" {{ old('currency',
$form->currency) == $currency ? 'selected' : '
' }}>{{ strtoupper($currency) }}</option>
@endforeach
</select>
</div>

We also prevented currency change for existing form by adding disabled


property to currency select field: {{ $form->id ? 'disabled': ''}}.
As our app supports payments in different currencies, we want to convert
all payment amounts to USD when showing statistics in the Dashboard. For

94 Max Kostinevich
Building SaaS with Laravel

currency converting we’ll be using CurrencyLayer service. CurrencyLayer pro-


vides a convenient API for converting currencies. They also have a Free plan,
which should be enough for our application on early stages.

So, first, sign up at https://currencylayer.com/ and obtain new API key. Then
create a new configuration file currencylayer.php in /config/ directory with
the following content:

// Currencylayer Settings
return [
// Currencylayer API Key
// Obtain your free key at https://currencylayer.com
'api' => env('CURRENCYLAYER_API_KEY', ''),

];

Then you need to add CURRENCYLAYER_API_KEY variable to .env file.

Okay, now we can get currency exchange rate by making a request to Cur-
rencyLayer API. It’s a good idea to store received currency exchange rate in
the cache of our application and refresh it once a day. It will allow us to sig-
nificantly reduce number of API calls to CurrencyLayer and reduce our costs.
To do this, we can add the following functionality to boot method of our
AppServiceProvider:

Max Kostinevich 95
Building SaaS with Laravel

class AppServiceProvider extends ServiceProvider


{
//...
public function boot()
{
// Refresh currency exchange rates every 100000
seconds (~27.8 hours)
cache()->remember('currency_rates', 100000, function
() {
$reverse_rates = [];
$client = new GuzzleHttp\Client();
$result = $client->request('GET', 'http://apilayer
.net/api/live', [
'query' => [
'access_key' => config('currencylayer.api'
),
'source' => 'usd',
'currencies' => implode(',', config('app.
currencies')),
'format' => 1
]
]);
$result = json_decode($result->getBody()->
getContents(), true);
if(!array_key_exists('error', $result)){
$rates = $result['quotes'];
// reverse rates
foreach ($rates as $pair => $rate) {
$pair = strtolower(substr($pair, 3));
$reverse_rates[$pair] = 1 / $rate;
}
}
return $reverse_rates;
});
}
}

96 Max Kostinevich
Building SaaS with Laravel

CurrencyLayer API allow us to get exchange rates for multiple currencies within
one API call, to do this we just need to pass a comma-separated list of needed
currencies.
As we want to include only non-refunded payments to our stats, we can add
NotRefunded scope to Payment model. It’s a good idea to also add a Refunded
scope:
class Payment extends Model
{
//...

// Scope a query to only include refunded payments.


public function scopeRefunded($query)
{
return $query->where('is_refunded', 1);
}

// Scope a query to only include not refunded payments.


public function scopeNotRefunded($query)
{
return $query->where('is_refunded', 0);
}
}

Now we can get user’s not refunded payments using the following query:
$payments = $user->payments()->notRefunded()->get().

Now we can add getStats method to User model which should return an
array of needed metrics for our stats. We need the following metrics:

• Total sales;
• Total net earnings;
• Sales in last 30 days;
• Number of payments made within last 30 days;

Max Kostinevich 97
Building SaaS with Laravel

Our getStats method may looks as follows:

98 Max Kostinevich
Building SaaS with Laravel

class User extends Authenticatable implements MustVerifyEmail


{
//...

// Get user payments stats


public function getStats()
{
$stats = [];
// Calculate total sales
$stats['totalSales'] = $this->payments()->notRefunded
()->get()->groupBy('currency')->map(function ($item
) {
return $item->sum(function ($payment) {
return $payment->amount;
});
})->map(function ($amount, $currency) {
// convert all earnings to USD
$reverse_rates = cache('currency_rates');
return $amount * $reverse_rates[$currency];
})->sum();

// Calculate net earnings


$stats['netEarnings'] = $this->payments()->notRefunded
()->get()->groupBy('currency')->map(function ($item
) {
return $item->sum(function ($payment) {
return $payment->amount - $payment->
application_fee_amount;
});
})->map(function ($amount, $currency) {
// convert all earnings to USD
$reverse_rates = cache('currency_rates');
return $amount * $reverse_rates[$currency];
})->sum();

// Calculate sales and payments in last 30 days


$date = \Carbon\Carbon::today()->subDays(30);
$stats['salesLast30Days'] = $this->payments()->
Max Kostinevich
notRefunded()->where('created_at', '>=', $date)-> 99
get()->groupBy('currency')->map(function ($item) {
return $item->sum(function ($payment) {
return $payment->amount - $payment->
application_fee_amount;
Building SaaS with Laravel

Then we need to pass stats data to dashboard view via DashboardController


:

class DashboardController extends Controller


{
//...

/**
* Show the application dashboard.
*/
public function index()
{

$payments = auth()->user()->payments()->orderBy('id',
'desc')->limit(10)->get();
$stats = auth()->user()->getStats();

return view('dashboard.index', ['payments' =>


$payments, 'stats' => $stats]);
}
}

Next, we can use $stats variable to show metrics in /resources/views/


dashboard/index.blade.php view, for example:

<div>
<span>{{ amountFormattedWithCurrency($stats['netEarnings'
]) }}</span>
<span>Total net earnings</span>
</div>

Adding notifications

100 Max Kostinevich


Building SaaS with Laravel

Related tag: step-3.8

After we implemented all necessary functionality to our app, we can add email
notifications. We want to send email notifications to our users in the following
cases:

• When new payment is processed:

– Send email notification to the seller;


– Send email notification to the customer;

• When payment is refunded:

– Send email notification to the customer;

Of course, we can add something like this to our controllers:


Mail::send('emails.new_payment', ['user' => $user], function (
$message) use ($user) {
$message->from('[email protected]', 'Payme App');
$message->to($user->email, $user->name)->subject('New
payment received!');
});

However, this is not a good practice, as such approach is hard to maintain


and is not recommended. Instead of sending email notifications directly from
controllers, we can use Laravel Events and Event Listeners. Laravel’s events
provide a simple and flexible mechanism, allowing you to subscribe and listen
for different events that may happen in your application.
For example, if you were building a ecommerce application, you may have
OrderCreated event, which may happen in several places of your application
(e.g. order has been placed by customer through website, order has been
placed via POS terminal, order has been placed through Admin Dashboard,

Max Kostinevich 101


Building SaaS with Laravel

etc.). Each time OrderCreated event is dispatched, you want to perform num-
ber of actions (e.g. send email to the customer, send order information to
fulfillment center, send notification to your Slack channel, etc.), so for each ac-
tion you create an Event Listener which listen for specific event and perform
all needed tasks.

In our case, we’ll have the following events and event listeners:

• Event: PaymentCreated

– Event listener: SendPaymentReceivedNotification


– Event listener: SendPaymentConfirmationNotification

• Event: PaymentRefunded

– Event listener: SendPaymentRefundedNotification

Laravel Framework provides a convenient way to create events and


listeners. First, we need to define all needed Events and Listeners in
EventsServiceProvider like this:

102 Max Kostinevich


Building SaaS with Laravel

class EventServiceProvider extends ServiceProvider


{
//...
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\PaymentCreated' => [
'App\Listeners\SendPaymentReceivedNotification',
'App\Listeners\SendPaymentConfirmationNotification
',
],
'App\Events\PaymentRefunded' => [
'App\Listeners\SendPaymentRefundedNotification',
],
];
//...
}

And then run php artisan event:generate command in the console to au-
tomatically generate all necessary PHP classes.

Then we need to dispatch these events using event() function, let’s dispatch
PaymentCreated event in PaymentFormController:

Max Kostinevich 103


Building SaaS with Laravel

class PaymentFormController extends Controller


{
//...
// Process the payment via AJAX
public function store(Request $request)
{
//...
// Create new payment
$payment = new Payment;
$payment->user_id = $form->user->id;
$payment->form_id = $form->id;
$payment->customer_name = $request->input('
customer_name');
$payment->customer_email = $request->input('
customer_email');
$payment->charge_id = $charge->id;
$payment->amount = $charge->amount;
$payment->currency = $charge->currency;
$payment->application_fee_amount = $charge->
application_fee_amount;
$payment->receipt_url = $charge->receipt_url;
$payment->save();
event(new PaymentCreated($payment));
//...
}
}

and PaymentRefunded in PaymentController:

104 Max Kostinevich


Building SaaS with Laravel

class PaymentController extends Controller


{
//...
public function update(Request $request, Payment $payment)
{
//...
$refund = \Stripe\Refund::create([
'charge' => $payment->charge_id,
'refund_application_fee' => true,
'reverse_transfer' => true
]);
$payment->is_refunded = 1;
$payment->save();
event(new PaymentRefunded($payment));
//...
}
}

As we’d like to send email notifications, we need to create them using


php artisan make:notification command. For example, to create
PaymentConfirmedNotification notification, we need to run the following
command in our console: artisan make:notification PaymentConfirmedNotificatio
. After that we can define the notification content in toMail method:

Max Kostinevich 105


Building SaaS with Laravel

class PaymentConfirmedNotification extends Notification


{
use Queueable;
public $payment;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Payment $payment)
{
$this->payment = $payment;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$url = $this->payment->receipt_url;
return (new MailMessage)
->subject('Payment confirmation')
->greeting('Hello, ' . $this->payment->
customer_name)
->line('You just paid ' . $this->payment->
amountFormattedWithCurrency() . ' to ' . $this
106 ->payment->user->name) Max Kostinevich
->action('Download Receipt', $url)
->line('Thank you for your business!');
}
/**
Building SaaS with Laravel

And then call this notification in our SendPaymentConfirmationNotification


event listener:

// Send Payment Confirmation to the Customer


class SendPaymentConfirmationNotification
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param PaymentCreated $event
* @return void
*/
public function handle(PaymentCreated $event)
{
Notification::route('mail', $event->payment->
customer_email)
->notify(new PaymentConfirmedNotification($event->
payment));
}
}

It’s a good idea to use Queues to handle event listeners and notifications, as
usually these tasks may take some time to perform all needed actions. To
make event listener queueable, we need to add ShouldQueue interface to
SendPaymentConfirmationNotification, so it will look as follows:

Max Kostinevich 107


Building SaaS with Laravel

class SendShipmentNotification implements ShouldQueue


{
//...
}

To let our notifications be queued, we need to add add ShouldQueue interface


and Queueable trait to the notification class, for example:
class PaymentRefundedNotification extends Notification
implements ShouldQueue
{
use Queueable;
// ...
}

Sometimes, queued tasks are failing, so we need a way to repeat failing tasks.
Luckily, Laravel provides such functionality out of the box. First, we need to
create failed_jobs table and run migration:
php artisan queue:failed-table

php artisan migrate

All failed jobs will be stored in this table, to run failed job we’ll need to run
php artisan queue:work command.

Small improvements

Related tag: step-3.9

Before starting making a Master Dashboard for our application, let make a few
small improvements:

108 Max Kostinevich


Building SaaS with Laravel

• Let’s remove _HTML folder from our views directory, as we do not need
these files anymore;
• Let’s prevent payment form to be appear if user’s Stripe account is not
connected;
• It’s also a good idea to enable soft deleting for payment forms and users;
• Let’s show a notification on the dashboard, if Stripe account is not con-
nected;

Awesome, the last thing we need to do - is a Master Dashboard, which will


allow us to manage our users and see important metrics of our application.

Building master admin

Related tag: step-4.x

In this chapter we’re going to build a really simple Master Admin, where we
can see the list of all our users with some metrics (like Lifetime Earnings) and
ability to delete/disable certain users. Master Admin is not required for MVP,
you can always build it later. If you’re building an MVP, I would not recommend
to spend a lot of time on building Master Admin, as on early stages it’s better
to focus on core features of your app.

First, we need a way to determine if current logged in user can have an access
to Master Admin. There are a lot of options to do this (e.g. adding a Role-
management system), however, we’d like to keep things as simple as possible.
And in our case we’ll have only one admin user. So let’s add a new admin config
with the following content:

Max Kostinevich 109


Building SaaS with Laravel

<?php
// Master Admin Settings
return [
// Master Admin email
'email' => env('ADMIN_EMAIL', '')
];

Then, in our .env file we need to set ADMIN_EMAIL variable. Then we need to
add isAdmin method to User model:

class User
{
//...
// Is Master Admin
public function isAdmin()
{
return in_array($this->email, [
config('admin.email')
]);
}
//...
}

This method returns true if user’s email match with the admin email, defined
in config file we created.

Then we need to create a new IsAdmin middleware using the following com-
mand: php artisan make:middleware IsAdmin. handle() method of our
middleware may looks as follow:

110 Max Kostinevich


Building SaaS with Laravel

class IsAdmin
{
public function handle($request, Closure $next)
{
if (auth()->check() && auth()->user()->isAdmin()) {
return $next($request);
}
return redirect()->route('dashboard');
}
}

Then we need to define new routes in /routes/web.php for our master ad-
min:
Route::group(
[
'middleware' => ['auth', 'is_admin'],
'prefix' => 'admin',
'as' => 'admin.',
'namespace' => 'Admin',
],
function () {
// Users
Route::get('/users', 'UserController@index')->name('
users.index');
Route::delete('/users/{user}', 'UserController@destroy
')->name('users.destroy');
Route::patch('/users/{id}', 'UserController@restore')
->name('users.restore');
}
);

And create a new UserController inside of /app/Http/Controllers/Admin/


directory with the following methods:

Max Kostinevich 111


Building SaaS with Laravel

class UserController extends Controller


{

public function index()


{
$users = User::withTrashed()->orderBy('id', 'desc')->
with('payments')->paginate(25);
return view('admin.users.index', compact('users'));
}
// Delete User
public function destroy(User $user)
{
if($user->isAdmin() || auth()->user()->id == $user->id
){
return abort(401);
}
$user->delete();
return redirect()
->back()->with('status', 'User has been deleted
successfully');
}
// Restore User
public function restore($user_id)
{
$user = User::withTrashed()
->where('id', $user_id)->firstOrFail();
$user->restore();
return redirect()
->back()->with('status', 'User has been restored
successfully');
}
}

Then we need to create a view for our master admin in /resources/views/


admin/ directory.

112 Max Kostinevich


Building SaaS with Laravel

As we’re using Laravel Horizon to manage queues, we also need to allow our
master admin to log in to Horizon instance. To do so we need to modify gate()
method in /app/Providers/HorizonServiceProvider.php as follows:
class HorizonServiceProvider extends
HorizonApplicationServiceProvider
{
//...
protected function gate()
{
Gate::define('viewHorizon', function ($user) {
return in_array($user->email, [
config('admin.email')
]);
});
}
}

The last thing we need to do - is to add links to our master admin and Horizon
instance to our layouts/app.blade.php view file. Do not forget to make these
links visible to master admin only, using user()->isAdmin() method:
<!-- Navigation -->
<!-- ... -->
@if(auth()->user()->isAdmin())
<li class="nav-item u-header__nav-item">
<a class="nav-link u-header__nav-link" href="/horizon/"
target="_blank">Horizon</a>
</li>
<li class="nav-item u-header__nav-item">
<a class="nav-link u-header__nav-link" href="{{ route('
admin.users.index') }}">Administration</a>
</li>
@endif
<!-- ... -->

Max Kostinevich 113


Building SaaS with Laravel

Awesome! At this moment we implemented all necessary features and can


deploy our app to production server.

Deploying our app

At first attempt I wanted to make this chapter in the form of a step-by-step


tutorial about application deployment. However, as UI of some tools may
change and deployment process might be a little bit different depending
on your preferred workflow, I decided to give a quick overview of popular
deployment options and provice you a few useful link where you can learn
more.
There are a number of tools and deployment options we can use to deploy our
application. For example, some of popular choices are:

• Laravel Forge - Server management and deployments via Git;


• Envoyer - Zero-downtime deployments;
• Ploi.io - Server management, zero-downtime deployments and auto-
backups;
• PaaS and managed hosting (e.g. Heroku and Cloudways);
• Containerized hosting and deployment (e.g. Docker);
• Serverless deployment through Laravel Vapor;

Personally, I prefer to use DigitalOcean for hosting, Forge for server manage-
ment and Envoyer for zero-downtime deployments. If a short period of down-
time isn’t crucial to your app, you may use Forge without Envoyer for deploy-
ment.
If you want to learn more about Forge and Envoyer, I would recommend you
to check these awesome series on Laracasts:

• Learn Forge

114 Max Kostinevich


Building SaaS with Laravel

• Learn Envoyer

Another great tool for deployment and server management is Ploi, created
by Dennis Smink. Ploi includes all features Forge and Envoyer has, and cost a
little bit less than Forge and Envoyer in total.

If you’re beginner and do not know a lot about server management, you may
take a look at Cloudways, they offer easy-to-use managed hosting platform
for Laravel apps.

Laravel Vapor - is entire tool in Laravel eco-system, it allows you to host Laravel
application in Serverless infrastructure. Serverless approach have it’s own
pros and cons, and out of scope of this book.

Regardless of tools you choose, deployment process includes the following


steps:

• Setup git repository;


• Provision new server;
• Create new database;
• Configure DNS records and setup SSL certificates;
• Prepare deployment recipe;
• Get all required API keys and prepare .env file for production;
• Create daemon and queue worker (if your app uses queues like beanstalk
or redis);

It’s also a good idea to keep deployment instructions in Readme file of your
project.

When going to production, do not forget to setup auto-backups. I also recom-


mend to setup some app monitoring tools to be notified when any error occur.
For example, you may use Bugsnag or Sentry, both tools provide a free plan.

Max Kostinevich 115


Building SaaS with Laravel

Summary

Okay, we finished and launched our MVP. I deliberately left small flaws in app
source code, so you can take some practice in code refactoring. For example,
one of the clear areas for refactoring is amountFormattedWithCurrency()
function, as this function is currently defined twice - in Payment model and in
our helper file.
You may also add some new features, for example:

• Add support of digital downloads to allow your users to sell digital con-
tent;
• Add support for recurring payments;
• Add an option which allows your users to embed payment forms to their
websites as javascript widget;
• ..and more!

116 Max Kostinevich


Building SaaS with Laravel

Chapter 3. Useful tips

In this chapter I’ll share my thoughts and useful tips about making and running
Sofware-as-a-Service applications.

Types of SaaS

I would distinct two different types of SaaS: - Standalone SaaS - This is type
of SaaS, which is not depending on any other service.

• Extensions for other products - This type of SaaS depends on another


product or service. For example - Shopify Apps, Apps for Quickbooks,
Apps for Salesforce, etc. (e.g. Shopify Apps, Apps for Quickbooks, etc)

Both of types have their own pros and cons. For example, making a standalone
SaaS gives you more freedom and flexibility, as you’re not depending on other
product. On other side, it could be harder to get your first users of standalone
SaaS, as many established products have their own marketplace where devel-
opers like you can publish extensions (plugins or apps) for that big product.
Making an extension for established product usually requires you to follow
some design guidelines, rules, and marketplace terms. However, it gives you
an access to existing and loyal customers who might be happy to use your
extension (and pay you!).

Thoughts on pricing and customer retention

There are several tips about pricing I would like to share:

• If you’re planning to charge pretty small amount for your SaaS


(e.g. $3/mo), it’s a good idea to offer yearly package, as it will gives you

Max Kostinevich 117


Building SaaS with Laravel

more money on inital period, which you can re-invest to marketing.


• To get more money on inital period, you may also offer a lifetime deal to
your first X customers.
• Providing a reasonable trial period may increase your conversion rate.
In most cases, 7-14 days is enough to try a product and make a decision.
• Be careful with the free plan, as it may increase amount of support
requests you need to handle.
• Be proactive with your first customers, try to follow-up with each sign-up
and get a quick feedback about your product.

If you’re willing to learn more about different aspects of running SaaS, I would
recommend to join SaaS Club by Omer Khan. SaaS Club provides a lot of useful
resources, such as group coaching sessions, a huge content library, access to
private community and expert master classes.

Thoughts on legal aspects

If you’re active on Twitter, you probably heard about Reilly Chase and his story.
So if you’re working on your project while keeping your daily job, it’s a good
idea to get a legal advice and consult your lawyer to make sure you do not
violate the terms of your employment contract.
You may also consider to create a new company for your new project. De-
pending on your requirements, there are several services allowing you to open
company remotely. Most of popular options are:

• Stripe Atlas - for US-based company;


• e-Residency - for Estonian company;

Before opening a company, it’s a good idea to consult with your accountant
regarding all important questions (e.g. Tax/VAT handling, local law compliance,

118 Max Kostinevich


Building SaaS with Laravel

etc).

Recommended books

There are a lot of books about making and running software projects, I’ll just
share my Top-3:

• Rework by Jason Fried and David Heinemeier Hansson


• Making ideas happen by Scott Belsky
• Start Small, Stay Small by Rob Walling

Max Kostinevich 119


Building SaaS with Laravel

Afterword

Thank you for reading this book, I hope you found it useful!
If you have any questions, found a typo or just want to provide a feedback,
feel free to shoot me a tweet at maxkostinevich or email me at hello@maxko
stinevich.com
— Max Kostinevich

120 Max Kostinevich

You might also like