Prévia do material em texto
<p>Building SaaS with Laravel</p><p>2 Max Kostinevich</p><p>Contents</p><p>Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5</p><p>What we are going to build . . . . . . . . . . . . . . . . . . . . 5</p><p>Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . 8</p><p>Getting the source code . . . . . . . . . . . . . . . . . . . . . 8</p><p>Working with the source code . . . . . . . . . . . . . . . . . . 9</p><p>Workspace setup . . . . . . . . . . . . . . . . . . . . . . . . . 10</p><p>About the author . . . . . . . . . . . . . . . . . . . . . . . . . 10</p><p>Contact the author . . . . . . . . . . . . . . . . . . . . . . . . 11</p><p>Chapter 1. Planning our app . . . . . . . . . . . . . . . . . . . . . . 12</p><p>Designing wireframes . . . . . . . . . . . . . . . . . . . . . . 12</p><p>Designing database . . . . . . . . . . . . . . . . . . . . . . . . 14</p><p>Chapter 2. Building our app . . . . . . . . . . . . . . . . . . . . . . 17</p><p>Creating new app . . . . . . . . . . . . . . . . . . . . . . . . . 17</p><p>Preparing views . . . . . . . . . . . . . . . . . . . . . . . . . . 21</p><p>Static pages . . . . . . . . . . . . . . . . . . . . . . . 21</p><p>Asset compilation . . . . . . . . . . . . . . . . . . . . 23</p><p>Authentication pages . . . . . . . . . . . . . . . . . . 29</p><p>Building main features . . . . . . . . . . . . . . . . . . . . . . 34</p><p>Dashboard . . . . . . . . . . . . . . . . . . . . . . . . 34</p><p>Settings . . . . . . . . . . . . . . . . . . . . . . . . . . 42</p><p>Payment forms . . . . . . . . . . . . . . . . . . . . . . 55</p><p>Payment form frontend . . . . . . . . . . . . . . . . . 67</p><p>Accepting payments . . . . . . . . . . . . . . . . . . . 69</p><p>Managing payments . . . . . . . . . . . . . . . . . . . 89</p><p>Dashboard stats . . . . . . . . . . . . . . . . . . . . . 93</p><p>Adding notifications . . . . . . . . . . . . . . . . . . . 100</p><p>3</p><p>Building SaaS with Laravel</p><p>Small improvements . . . . . . . . . . . . . . . . . . . 108</p><p>Building master admin . . . . . . . . . . . . . . . . . . . . . . 109</p><p>Deploying our app . . . . . . . . . . . . . . . . . . . . . . . . 114</p><p>Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116</p><p>Chapter 3. Useful tips . . . . . . . . . . . . . . . . . . . . . . . . . . 117</p><p>Types of SaaS . . . . . . . . . . . . . . . . . . . . . . . . . . . 117</p><p>Thoughts on pricing and customer retention . . . . . . . . . . 117</p><p>Thoughts on legal aspects . . . . . . . . . . . . . . . . . . . . 118</p><p>Recommended books . . . . . . . . . . . . . . . . . . . . . . 119</p><p>A�erword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120</p><p>4 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Introduction</p><p>In this book you’ll learn how to design, build and launch a real-world SaaS</p><p>application using Laravel framework. This book will be useful to solopreneurs,</p><p>bootstrappers and indie makers who wanted to launch their SaaS business.</p><p>I’m going to explain step-by-step each stage of project development - from</p><p>designing wireframes to deployment.</p><p>You’ll learn how to:</p><p>• Convert your idea to wireframes</p><p>• Design database</p><p>• Convert HTML template to Laravel views</p><p>• Organize project routes</p><p>• Accept payments through Stripe and collect fees</p><p>• Distribute payments using Stripe Connect</p><p>• Send email notifications</p><p>• Install and use Laravel Horizon to manage queues</p><p>• Work with 3rd-party API to convert currencies</p><p>• Build master administration panel to manage the project</p><p>• Deploy the application</p><p>I also included the list of tips and advices which will be useful to you when you</p><p>decide to launch you SaaS.</p><p>Learn more about this book at maxkostinevich.com/books/laravel-saas</p><p>What we are going to build</p><p>The project we’re going to develop in this book is called PayMe.</p><p>Max Kostinevich 5</p><p>https://maxkostinevich.com/books/laravel-saas</p><p>Building SaaS with Laravel</p><p>PayMe is a checkout payment solution to accept payments online for free-</p><p>lancers, digital artists and small agencies.</p><p>The idea of the project is pretty simple: the Seller (e.g. freelance designer)</p><p>registers on PayMe, connects their Stripe Account, creates a payment form</p><p>where he can enter service description, amount, and choose a currency. When</p><p>the payment form is created, it becomes available via unique URL, which</p><p>then could be shared with the Customer. When the Customer pays via the</p><p>payment form, the paid amount is automatically transferred to Seller’s Stripe</p><p>Account.</p><p>Youmay see an example of payment form on the following screenshot:</p><p>As our main goal is to validate our idea and build MVP (minimum-viable-</p><p>product) as soon as possible and spend as less time as we can, we’re not going</p><p>to implement any subscription-based features. Instead of this, we’re going to</p><p>collect some fee (e.g. percent or fixed price) on each payment made through</p><p>our app, which will be deducted automatically from the Seller’s account. You</p><p>can see the payment flow on the diagram below:</p><p>6 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>On the example above, the Customer paid $10 to the Seller. From this amount</p><p>we collected $1.23 as a fee for our application. Additional $0.59 have been</p><p>deducted by Stripe as their own fee. As a result, the seller earned $8.18 net</p><p>amount ($10 - $1.23 - $0.59 = $8.18).</p><p>PayMe demo could be found using the links below:</p><p>• Live demo</p><p>• Sample payment form</p><p>Max Kostinevich 7</p><p>https://payme.rocks/</p><p>https://payme.rocks/p/5d11eba50345e</p><p>Building SaaS with Laravel</p><p>Prerequisites</p><p>The project we are going to develop in this book is designed for beginner</p><p>developers, however I assume that you’re familiar with Laravel framework and</p><p>know how to work with Controllers, Models, and run Artisan Commands.</p><p>We’re going to use Laravel 5.8.</p><p>If you’re absolutely new to Laravel, I’d recommend you to check some video</p><p>courses first, for example:</p><p>• Laracasts</p><p>• Codecourse</p><p>• Udemy</p><p>As we’re a going to use Stripe Connect and Stripe API to handle payments and</p><p>payouts, you’ll need to have a Stripe Account. Currently Stripe is available in</p><p>34+ countries, so if you’re living in a country where Stripe isn’t yet supported -</p><p>don’t worry, you still will be able to create development account with Stripe.</p><p>Getting the source code</p><p>The source code of the PayMe project is included to your purchase. Be sure to</p><p>carefully read the README for installation instructions.</p><p>Disclaimer</p><p>The source code is provided for learning purposes without warranty of any</p><p>kind. You’re free to use provided source code (or any parts of it) as you’d like.</p><p>Redistribution (i.e. reselling) or sharing (e.g. via public GitHub repository) is</p><p>not allowed without prior written permission.</p><p>8 Max Kostinevich</p><p>https://laracasts.com/</p><p>https://codecourse.com/</p><p>https://www.udemy.com/</p><p>https://stripe.com/connect</p><p>https://stripe.com</p><p>https://stripe.com/global</p><p>Building SaaS with Laravel</p><p>Working with the source code</p><p>For your convenience, all important steps in the source code are marked by</p><p>git tags.</p><p>You can easily switch between specific tags by using git checkout command.</p><p>For example, to switch to tag step-2.1, just type in your terminal:</p><p>git checkout step-2.1</p><p>To list all available tags, just type the following command:</p><p>git tag -n</p><p>You’ll see the list of all available tags:</p><p>During the book youmay see the following notes:</p><p>Related tag: step-x.x</p><p>That means that the section or chapter has a related tag in the app source</p><p>Max Kostinevich 9</p><p>Building SaaS with Laravel</p><p>code.</p><p>Workspace setup</p><p>There are no strict requirements which tools to use for local development, you</p><p>will be fine with Homestead or Valet. I’d recommend to avoid XAMPP, WAMP</p><p>and similar so�ware.</p><p>Personally, I use the following setup:</p><p>• Docker with installed Nginx, PHP 7, Redis and MySQL</p><p>• Ngrok to expose local server to internet over secure tunnel</p><p>• PHPStorm as mymain IDE</p><p>• Notepad++ for quick edits</p><p>• Cmder as mymain command-line tool</p><p>If you’re new to Docker and haven’t worked with it before, I recorded a short</p><p>video explaining how to get started with Docker for Laravel development, you</p><p>can watch it here.</p><p>I also created a ready-to-go Docker template, which is available on the</p><p>Github.</p><p>Also, if you haven’t worked with Ngrok before,</p><p>var form = $('#payment-form');</p><p>$('#stripeToken').val(token.id);</p><p>// Submit the form via AJAX</p><p>var button = $('#paybtn');</p><p>button.attr('disabled', true);</p><p>button.html('<i class="fas fa-spinner fa-spin"></i> Please</p><p>wait..');</p><p>$.ajax({</p><p>url : form.attr('action'),</p><p>type : form.attr('method'),</p><p>dataType: 'json',</p><p>data : form.serialize(),</p><p>success : function( data ) {</p><p>var hand = setTimeout(function(){</p><p>$('#payment-form').hide();</p><p>$('#payment-alert').show();</p><p>clearTimeout(hand);</p><p>}, 1000);</p><p>},</p><p>error : function( xhr, err ) {</p><p>// Log errors if AJAX call is failed</p><p>console.log(xhr);</p><p>console.log(err);</p><p>$('#payment-form').hide();</p><p>$('#payment-error').show();</p><p>}</p><p>});</p><p>return false;</p><p>}</p><p>Max Kostinevich 85</p><p>Building SaaS with Laravel</p><p>Then we need to add a new route to /routes/web.php:</p><p>Route::post('/p/{uid}', 'PaymentFormController@store')->name('</p><p>form.store');</p><p>Next, let’s go to our PaymentFormController and create store() method.</p><p>In this method we need to create a new charge using Stripe API and store</p><p>information about the payment in our database.</p><p>86 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class PaymentFormController extends Controller</p><p>{</p><p>//...</p><p>// Process the payment via AJAX</p><p>public function store(Request $request)</p><p>{</p><p>// Get the form</p><p>$uid = $request->route('uid');</p><p>$form = Form::where('uid', $uid)</p><p>->where('is_active', 1)</p><p>->with('user')</p><p>->firstOrFail();</p><p>try {</p><p>$token = $request->input('stripeToken');</p><p>// create charge</p><p>$charge = \Stripe\Charge::create(</p><p>array(</p><p>'amount' => $form->amount,</p><p>'currency' => $form->currency,</p><p>'source' => $token,</p><p>'application_fee_amount' =></p><p>get_payment_fee($form->amount),</p><p>'transfer_data' => [</p><p>'destination' => $form->user-></p><p>stripe_account_id,</p><p>],</p><p>)</p><p>);</p><p>// Create new payment</p><p>$payment = new Payment;</p><p>$payment->user_id = $form->user->id;</p><p>$payment->form_id = $form->id;</p><p>$payment->customer_name = $request->input('</p><p>customer_name');</p><p>$payment->customer_email = $request->input('</p><p>customer_email');</p><p>$payment->charge_id = $charge->id;</p><p>$payment->amount = $charge->amount;</p><p>$payment->currency = $charge->currency;</p><p>$payment->application_fee_amount = $charge-></p><p>application_fee_amount;</p><p>$payment->receipt_url = $charge->receipt_url;</p><p>$payment->save();</p><p>// return success</p><p>return response()->json(['success' => true], 200);</p><p>} catch (Exception $e) {</p><p>logger()->error($e->getMessage());</p><p>}</p><p>// otherwise return error</p><p>return response()->json([</p><p>'success' => 'false',</p><p>'errors' => 'Oops! Something went wrong.',</p><p>], 400);</p><p>}</p><p>}</p><p>Max Kostinevich 87</p><p>Building SaaS with Laravel</p><p>A few notes:</p><p>As we want to send the payment directly to the Seller, we set transfer_data</p><p>->destination property to user’s connected Stripe Account ID. At the same</p><p>time we also want to keep some part of the payment as our fee, to do this we</p><p>can create a get_payment_fee helper function in /app/helpers.php:</p><p><?php</p><p>// Calculate application fee (in cents) for the payment</p><p>function get_payment_fee($amount)</p><p>{</p><p>// Get 5% + 50 cents</p><p>$fee = $amount * 0.05 + 50;</p><p>$fee = round($fee);</p><p>return $fee;</p><p>}</p><p>Donot forget to register this file incomposer.json, todo this just addautoload</p><p>property:</p><p>"autoload": {</p><p>"psr-4": {</p><p>"App\\": "app/"</p><p>},</p><p>"files": [</p><p>"app/helpers.php"</p><p>],</p><p>//...</p><p>}</p><p>And then run composer dump-autoload command in your console.</p><p>Great! In next step we’ll add an option to manage payments through the</p><p>dashboard.</p><p>88 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Managing payments</p><p>Related tag: step-3.6</p><p>Now we would like to allow our users to see received payments and make</p><p>refunds.</p><p>First, let’s create PaymentController and useindex()method to show user’s</p><p>payments:</p><p>class PaymentController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Display a listing of the resource.</p><p>*</p><p>* @return \Illuminate\Http\Response</p><p>*/</p><p>public function index()</p><p>{</p><p>$payments = auth()->user()->payments()->orderBy('id',</p><p>'desc')->paginate(25);</p><p>return view('payments.index', compact('payments'));</p><p>}</p><p>}</p><p>Then we need to create payments view in /resources/views/payments/</p><p>index.blade.php, and add relationship to Paymentmodel:</p><p>Max Kostinevich 89</p><p>Building SaaS with Laravel</p><p>class Payment extends Model</p><p>{</p><p>//...</p><p>// Payment form</p><p>public function form()</p><p>{</p><p>return $this->belongsTo('App\Form');</p><p>}</p><p>//...</p><p>}</p><p>Next, let’s add an option to refund a specific payment. This feature will al-</p><p>low our users to make decision about refunds on their own, and make it</p><p>easier for us to maintain the project. We’ll be using update() method in</p><p>PaymentControllerwe created earlier. In thismethodwemake an attempt to</p><p>make a refund throught Stripe API and, in case of success, mark the payment</p><p>as refunded:</p><p>90 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class PaymentController extends Controller</p><p>{</p><p>//...</p><p>public function update(Request $request, Payment $payment)</p><p>{</p><p>if (auth()->user()->id != $payment->user_id) {</p><p>return abort(401);</p><p>}</p><p>try {</p><p>$refund = \Stripe\Refund::create([</p><p>'charge' => $payment->charge_id,</p><p>'refund_application_fee' => true,</p><p>'reverse_transfer' => true</p><p>]);</p><p>$payment->is_refunded = 1;</p><p>$payment->save();</p><p>return redirect()</p><p>->back()->with('status', 'Refund has been</p><p>processed successfully');</p><p>} catch (Exception $e) {</p><p>logger()->error($e->getMessage());</p><p>}</p><p>return redirect()</p><p>->back()->withErrors('There was an error</p><p>encountered.');</p><p>}</p><p>}</p><p>Then we need to update /resources/views/payments/index.blade.php</p><p>Max Kostinevich 91</p><p>Building SaaS with Laravel</p><p>view and add a “Refund” link to each payment record (the same way we did it</p><p>to handle payment form deletion):</p><p><td class="align-middle"></p><p><a href="{{ $payment->receipt_url }}" target="_blank"</p><p>class="text-primary small mr-3"><span class="fas fa-</p><p>receipt"></span> Receipt</a></p><p>@if($payment->is_refunded)</p><p><span class="small text-muted"><span class="fas fa-</p><p>circle small"></span> Refunded</span></p><p>@else</p><p><a href="#" class="text-danger small" onclick="if(confirm</p><p>('Refund this payment?')){document.getElementById('</p><p>refund-entity-{{ $payment->id }}').submit();return</p><p>false;}"><span class="fas fa-redo-alt"></span> Refund</</p><p>a></p><p><form id="refund-entity-{{ $payment->id }}" action="{{</p><p>route('payments.update', $payment) }}" method="POST"></p><p><input type="hidden" name="_method" value="PATCH"></p><p>@csrf</p><p></form></p><p>@endif</p><p></td></p><p>Do not forget to add all necessary routes to /routes/web.php and update</p><p>header navigation in /resources/views/components/header.blade.php</p><p>.</p><p>Then let’s add payment information to payment forms view (/resources/</p><p>views/forms/index.blade.php):</p><p>92 Max Kostinevich</p><p>Building SaaS with Laravel</p><p><td class="align-middle"></p><p><span class="d-block">{{ amountFormattedWithCurrency($form</p><p>->payments->sum('amount'), $form->currency) }}</span></p><p><a href="{{ route('form.payments.index', $form->uid) }}"</p><p>class="link-muted small">{{ $form->payments()->count()</p><p>}} {{ Str::plural('payment', $form->payments()->count()</p><p>) }}</a></p><p></td></p><p>And finally, let’s add recent payments to the dashboard:</p><p>class DashboardController extends Controller</p><p>{</p><p>//...</p><p>public function index()</p><p>{</p><p>$payments = auth()->user()->payments()->orderBy('id',</p><p>'desc')->limit(10)->get();</p><p>return view('dashboard.index', compact('payments'));</p><p>}</p><p>}</p><p>and do not forget to update dashboard view and add a link to all payments in</p><p>/resources/views/components/header.blade.php.</p><p>Dashboard stats</p><p>Related tag: step-3.7</p><p>A�er we added payments handling andmanagement, we can add some pay-</p><p>ment statistics to our dashboard. As payments can bemade in di�erent cur-</p><p>rencies, we want to convert all payments to USD.</p><p>First, let’s add all available currencies to our config/app.php file:</p><p>Max Kostinevich 93</p><p>Building SaaS with Laravel</p><p>return [</p><p>//...</p><p>// Available currencies</p><p>'currencies' => [</p><p>'usd',</p><p>'eur',</p><p>'cad',</p><p>'aud',</p><p>],</p><p>//...</p><p>];</p><p>Nowwe can get the list of all available currencies using the config function,</p><p>for example: $currencies = config('app.currencies');</p><p>Then we need to use this currency list in Edit Payment Form view located in</p><p>/resources/views/forms/edit.blade.php:</p><p><div class="col-5"></p><p><select name="currency" class="form-control" {{ $form->id</p><p>? 'disabled' : '' }}></p><p>@foreach(config('app.currencies') as $currency)</p><p><option value="{{ $currency }}" {{ old('currency',</p><p>$form->currency) == $currency ? 'selected' : '</p><p>' }}>{{ strtoupper($currency) }}</option></p><p>@endforeach</p><p></select></p><p></div></p><p>We also prevented currency change for existing form by adding disabled</p><p>property to currency select field: {{ $form->id ? 'disabled': ''}}.</p><p>As our app supports payments in di�erent currencies, we want to convert</p><p>all payment amounts to USD when showing statistics in the Dashboard. For</p><p>94 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>currency converting we’ll be using CurrencyLayer service. CurrencyLayer pro-</p><p>vides a convenient API for converting currencies. They also have a Free plan,</p><p>which should be enough for our application on early stages.</p><p>So, first, sign up at https://currencylayer.com/ and obtain new API key. Then</p><p>create anewconfiguration filecurrencylayer.php in/config/directorywith</p><p>the following content:</p><p>// Currencylayer Settings</p><p>return [</p><p>// Currencylayer API Key</p><p>// Obtain your free key at https://currencylayer.com</p><p>'api' => env('CURRENCYLAYER_API_KEY', ''),</p><p>];</p><p>Then you need to add CURRENCYLAYER_API_KEY variable to .env file.</p><p>Okay, now we can get currency exchange rate by making a request to Cur-</p><p>rencyLayer API. It’s a good idea to store received currency exchange rate in</p><p>the cache of our application and refresh it once a day. It will allow us to sig-</p><p>nificantly reduce number of API calls to CurrencyLayer and reduce our costs.</p><p>To do this, we can add the following functionality to boot method of our</p><p>AppServiceProvider:</p><p>Max Kostinevich 95</p><p>https://currencylayer.com/</p><p>Building SaaS with Laravel</p><p>class AppServiceProvider extends ServiceProvider</p><p>{</p><p>//...</p><p>public function boot()</p><p>{</p><p>// Refresh currency exchange rates every 100000</p><p>seconds (~27.8 hours)</p><p>cache()->remember('currency_rates', 100000, function</p><p>() {</p><p>$reverse_rates = [];</p><p>$client = new GuzzleHttp\Client();</p><p>$result = $client->request('GET', 'http://apilayer</p><p>.net/api/live', [</p><p>'query' => [</p><p>'access_key' => config('currencylayer.api'</p><p>),</p><p>'source' => 'usd',</p><p>'currencies' => implode(',', config('app.</p><p>currencies')),</p><p>'format' => 1</p><p>]</p><p>]);</p><p>$result = json_decode($result->getBody()-></p><p>getContents(), true);</p><p>if(!array_key_exists('error', $result)){</p><p>$rates = $result['quotes'];</p><p>// reverse rates</p><p>foreach ($rates as $pair => $rate) {</p><p>$pair = strtolower(substr($pair, 3));</p><p>$reverse_rates[$pair] = 1 / $rate;</p><p>}</p><p>}</p><p>return $reverse_rates;</p><p>});</p><p>}</p><p>}</p><p>96 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>CurrencyLayer API allowus to get exchange rates formultiple currencieswithin</p><p>one API call, to do this we just need to pass a comma-separated list of needed</p><p>currencies.</p><p>As we want to include only non-refunded payments to our stats, we can add</p><p>NotRefunded scope to Paymentmodel. It’s a good idea to also add a Refunded</p><p>scope:</p><p>class Payment extends Model</p><p>{</p><p>//...</p><p>// Scope a query to only include refunded payments.</p><p>public function scopeRefunded($query)</p><p>{</p><p>return $query->where('is_refunded', 1);</p><p>}</p><p>// Scope a query to only include not refunded payments.</p><p>public function scopeNotRefunded($query)</p><p>{</p><p>return $query->where('is_refunded', 0);</p><p>}</p><p>}</p><p>Now we can get user’s not refunded payments using the following query:</p><p>$payments = $user->payments()->notRefunded()->get().</p><p>Now we can add getStats method to User model which should return an</p><p>array of neededmetrics for our stats. We need the following metrics:</p><p>• Total sales;</p><p>• Total net earnings;</p><p>• Sales in last 30 days;</p><p>• Number of payments made within last 30 days;</p><p>Max Kostinevich 97</p><p>Building SaaS with Laravel</p><p>Our getStatsmethodmay looks as follows:</p><p>98 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class User extends Authenticatable implements MustVerifyEmail</p><p>{</p><p>//...</p><p>// Get user payments stats</p><p>public function getStats()</p><p>{</p><p>$stats = [];</p><p>// Calculate total sales</p><p>$stats['totalSales'] = $this->payments()->notRefunded</p><p>()->get()->groupBy('currency')->map(function ($item</p><p>) {</p><p>return $item->sum(function ($payment) {</p><p>return $payment->amount;</p><p>});</p><p>})->map(function ($amount, $currency) {</p><p>// convert all earnings to USD</p><p>$reverse_rates = cache('currency_rates');</p><p>return $amount * $reverse_rates[$currency];</p><p>})->sum();</p><p>// Calculate net earnings</p><p>$stats['netEarnings'] = $this->payments()->notRefunded</p><p>()->get()->groupBy('currency')->map(function ($item</p><p>) {</p><p>return $item->sum(function ($payment) {</p><p>return $payment->amount - $payment-></p><p>application_fee_amount;</p><p>});</p><p>})->map(function ($amount, $currency) {</p><p>// convert all earnings to USD</p><p>$reverse_rates = cache('currency_rates');</p><p>return $amount * $reverse_rates[$currency];</p><p>})->sum();</p><p>// Calculate sales and payments in last 30 days</p><p>$date = \Carbon\Carbon::today()->subDays(30);</p><p>$stats['salesLast30Days'] = $this->payments()-></p><p>notRefunded()->where('created_at', '>=', $date)-></p><p>get()->groupBy('currency')->map(function ($item) {</p><p>return $item->sum(function ($payment) {</p><p>return $payment->amount - $payment-></p><p>application_fee_amount;</p><p>});</p><p>})->map(function ($amount, $currency) {</p><p>// convert all earnings to USD</p><p>$reverse_rates = cache('currency_rates');</p><p>return $amount * $reverse_rates[$currency];</p><p>})->sum();</p><p>// Payments quantity in last 30 days;</p><p>$stats['paymentsLast30Days'] = $this->payments()-></p><p>notRefunded()->where('created_at', '>=', $date)-></p><p>count();</p><p>return $stats;</p><p>}</p><p>}</p><p>Max Kostinevich 99</p><p>Building SaaS with Laravel</p><p>Thenwe need to pass stats data to dashboard view via DashboardController</p><p>:</p><p>class DashboardController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Show the application dashboard.</p><p>*/</p><p>public function index()</p><p>{</p><p>$payments = auth()->user()->payments()->orderBy('id',</p><p>'desc')->limit(10)->get();</p><p>$stats = auth()->user()->getStats();</p><p>return view('dashboard.index', ['payments' =></p><p>$payments, 'stats' => $stats]);</p><p>}</p><p>}</p><p>Next, we can use $stats variable to show metrics in /resources/views/</p><p>dashboard/index.blade.php view, for example:</p><p><div></p><p><span>{{ amountFormattedWithCurrency($stats['netEarnings'</p><p>]) }}</span></p><p><span>Total net earnings</span></p><p></div></p><p>Adding notifications</p><p>100 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Related tag: step-3.8</p><p>A�er we implemented all necessary functionality to our app, we can add email</p><p>notifications. We want to send email notifications to our users in the following</p><p>cases:</p><p>• When new payment is processed:</p><p>– Send email notification to the seller;</p><p>– Send email notification to the customer;</p><p>• When payment is refunded:</p><p>– Send email notification to the customer;</p><p>Of course, we can add something like this to our controllers:</p><p>Mail::send('emails.new_payment', ['user' => $user], function (</p><p>$message) use ($user) {</p><p>$message->from('no-reply@example.com', 'Payme App');</p><p>$message->to($user->email, $user->name)->subject('New</p><p>payment received!');</p><p>});</p><p>However, this is not a good practice, as such approach is hard to maintain</p><p>and is not recommended. Instead of sending email notifications directly from</p><p>controllers, we can use Laravel Events and Event Listeners. Laravel’s events</p><p>provide a simple and flexible mechanism, allowing you to subscribe and listen</p><p>for di�erent events that may happen in your application.</p><p>For example, if you were building a ecommerce application, you may have</p><p>OrderCreated event, which may happen in several places of your application</p><p>(e.g. order has been placed by customer through website, order has been</p><p>placed via POS terminal, order has been placed through Admin Dashboard,</p><p>Max Kostinevich 101</p><p>https://laravel.com/docs/master/events</p><p>Building SaaS with Laravel</p><p>etc.). Each time OrderCreated event is dispatched, you want to perform num-</p><p>ber of actions (e.g. send email to the customer, send order information to</p><p>fulfillment center, send notification to your Slack channel, etc.), so for each ac-</p><p>tion you create an Event Listenerwhich listen for specific event and perform</p><p>all needed tasks.</p><p>In our case, we’ll have the following events and event listeners:</p><p>• Event: PaymentCreated</p><p>– Event listener: SendPaymentReceivedNotification</p><p>– Event listener: SendPaymentConfirmationNotification</p><p>• Event: PaymentRefunded</p><p>– Event listener: SendPaymentRefundedNotification</p><p>Laravel Framework provides a convenient way to create events and</p><p>listeners. First,</p><p>we need to define all needed Events and Listeners in</p><p>EventsServiceProvider like this:</p><p>102 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class EventServiceProvider extends ServiceProvider</p><p>{</p><p>//...</p><p>/**</p><p>* The event listener mappings for the application.</p><p>*</p><p>* @var array</p><p>*/</p><p>protected $listen = [</p><p>'App\Events\PaymentCreated' => [</p><p>'App\Listeners\SendPaymentReceivedNotification',</p><p>'App\Listeners\SendPaymentConfirmationNotification</p><p>',</p><p>],</p><p>'App\Events\PaymentRefunded' => [</p><p>'App\Listeners\SendPaymentRefundedNotification',</p><p>],</p><p>];</p><p>//...</p><p>}</p><p>And then run php artisan event:generate command in the console to au-</p><p>tomatically generate all necessary PHP classes.</p><p>Then we need to dispatch these events using event() function, let’s dispatch</p><p>PaymentCreated event in PaymentFormController:</p><p>Max Kostinevich 103</p><p>Building SaaS with Laravel</p><p>class PaymentFormController extends Controller</p><p>{</p><p>//...</p><p>// Process the payment via AJAX</p><p>public function store(Request $request)</p><p>{</p><p>//...</p><p>// Create new payment</p><p>$payment = new Payment;</p><p>$payment->user_id = $form->user->id;</p><p>$payment->form_id = $form->id;</p><p>$payment->customer_name = $request->input('</p><p>customer_name');</p><p>$payment->customer_email = $request->input('</p><p>customer_email');</p><p>$payment->charge_id = $charge->id;</p><p>$payment->amount = $charge->amount;</p><p>$payment->currency = $charge->currency;</p><p>$payment->application_fee_amount = $charge-></p><p>application_fee_amount;</p><p>$payment->receipt_url = $charge->receipt_url;</p><p>$payment->save();</p><p>event(new PaymentCreated($payment));</p><p>//...</p><p>}</p><p>}</p><p>and PaymentRefunded in PaymentController:</p><p>104 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class PaymentController extends Controller</p><p>{</p><p>//...</p><p>public function update(Request $request, Payment $payment)</p><p>{</p><p>//...</p><p>$refund = \Stripe\Refund::create([</p><p>'charge' => $payment->charge_id,</p><p>'refund_application_fee' => true,</p><p>'reverse_transfer' => true</p><p>]);</p><p>$payment->is_refunded = 1;</p><p>$payment->save();</p><p>event(new PaymentRefunded($payment));</p><p>//...</p><p>}</p><p>}</p><p>As we’d like to send email notifications, we need to create them using</p><p>php artisan make:notification command. For example, to create</p><p>PaymentConfirmedNotification notification, we need to run the following</p><p>command inour console: artisan make:notification PaymentConfirmedNotification</p><p>. A�er that we can define the notification content in toMailmethod:</p><p>Max Kostinevich 105</p><p>Building SaaS with Laravel</p><p>class PaymentConfirmedNotification extends Notification</p><p>{</p><p>use Queueable;</p><p>public $payment;</p><p>/**</p><p>* Create a new notification instance.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function __construct(Payment $payment)</p><p>{</p><p>$this->payment = $payment;</p><p>}</p><p>/**</p><p>* Get the notification's delivery channels.</p><p>*</p><p>* @param mixed $notifiable</p><p>* @return array</p><p>*/</p><p>public function via($notifiable)</p><p>{</p><p>return ['mail'];</p><p>}</p><p>/**</p><p>* Get the mail representation of the notification.</p><p>*</p><p>* @param mixed $notifiable</p><p>* @return \Illuminate\Notifications\Messages\MailMessage</p><p>*/</p><p>public function toMail($notifiable)</p><p>{</p><p>$url = $this->payment->receipt_url;</p><p>return (new MailMessage)</p><p>->subject('Payment confirmation')</p><p>->greeting('Hello, ' . $this->payment-></p><p>customer_name)</p><p>->line('You just paid ' . $this->payment-></p><p>amountFormattedWithCurrency() . ' to ' . $this</p><p>->payment->user->name)</p><p>->action('Download Receipt', $url)</p><p>->line('Thank you for your business!');</p><p>}</p><p>/**</p><p>* Get the array representation of the notification.</p><p>*</p><p>* @param mixed $notifiable</p><p>* @return array</p><p>*/</p><p>public function toArray($notifiable)</p><p>{</p><p>return [</p><p>//</p><p>];</p><p>}</p><p>}</p><p>106 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>And thencall thisnotification inourSendPaymentConfirmationNotification</p><p>event listener:</p><p>// Send Payment Confirmation to the Customer</p><p>class SendPaymentConfirmationNotification</p><p>{</p><p>/**</p><p>* Create the event listener.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function __construct()</p><p>{</p><p>//</p><p>}</p><p>/**</p><p>* Handle the event.</p><p>*</p><p>* @param PaymentCreated $event</p><p>* @return void</p><p>*/</p><p>public function handle(PaymentCreated $event)</p><p>{</p><p>Notification::route('mail', $event->payment-></p><p>customer_email)</p><p>->notify(new PaymentConfirmedNotification($event-></p><p>payment));</p><p>}</p><p>}</p><p>It’s a good idea to use Queues to handle event listeners and notifications, as</p><p>usually these tasks may take some time to perform all needed actions. To</p><p>make event listener queueable, we need to add ShouldQueue interface to</p><p>SendPaymentConfirmationNotification, so it will look as follows:</p><p>Max Kostinevich 107</p><p>Building SaaS with Laravel</p><p>class SendShipmentNotification implements ShouldQueue</p><p>{</p><p>//...</p><p>}</p><p>To let our notifications be queued, we need to add add ShouldQueue interface</p><p>and Queueable trait to the notification class, for example:</p><p>class PaymentRefundedNotification extends Notification</p><p>implements ShouldQueue</p><p>{</p><p>use Queueable;</p><p>// ...</p><p>}</p><p>Sometimes, queued tasks are failing, so we need a way to repeat failing tasks.</p><p>Luckily, Laravel provides such functionality out of the box. First, we need to</p><p>create failed_jobs table and runmigration:</p><p>php artisan queue:failed-table</p><p>php artisan migrate</p><p>All failed jobs will be stored in this table, to run failed job we’ll need to run</p><p>php artisan queue:work command.</p><p>Small improvements</p><p>Related tag: step-3.9</p><p>Before starting making a Master Dashboard for our application, let make a few</p><p>small improvements:</p><p>108 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>• Let’s remove _HTML folder from our views directory, as we do not need</p><p>these files anymore;</p><p>• Let’s prevent payment form to be appear if user’s Stripe account is not</p><p>connected;</p><p>• It’s also a good idea to enable so� deleting for payment forms and users;</p><p>• Let’s show a notification on the dashboard, if Stripe account is not con-</p><p>nected;</p><p>Awesome, the last thing we need to do - is a Master Dashboard, which will</p><p>allow us to manage our users and see important metrics of our application.</p><p>Buildingmaster admin</p><p>Related tag: step-4.x</p><p>In this chapter we’re going to build a really simple Master Admin, where we</p><p>can see the list of all our users with somemetrics (like Lifetime Earnings) and</p><p>ability to delete/disable certain users. Master Admin is not required for MVP,</p><p>you can always build it later. If you’re building anMVP, I would not recommend</p><p>to spend a lot of time on building Master Admin, as on early stages it’s better</p><p>to focus on core features of your app.</p><p>First, we need a way to determine if current logged in user can have an access</p><p>to Master Admin. There are a lot of options to do this (e.g. adding a Role-</p><p>management system), however, we’d like to keep things as simple as possible.</p><p>And in our casewe’ll have only one admin user. So let’s add a new admin config</p><p>with the following content:</p><p>Max Kostinevich 109</p><p>https://laravel.com/docs/master/eloquent#soft-deleting</p><p>Building SaaS with Laravel</p><p><?php</p><p>// Master Admin Settings</p><p>return [</p><p>// Master Admin email</p><p>'email' => env('ADMIN_EMAIL', '')</p><p>];</p><p>Then, in our .env file we need to set ADMIN_EMAIL variable. Then we need to</p><p>add isAdminmethod to Usermodel:</p><p>class User</p><p>{</p><p>//...</p><p>// Is Master Admin</p><p>public function isAdmin()</p><p>{</p><p>return in_array($this->email, [</p><p>config('admin.email')</p><p>]);</p><p>}</p><p>//...</p><p>}</p><p>This method returns true if user’s email match with the admin email, defined</p><p>in config file we created.</p><p>Then we need to create a new IsAdminmiddleware using the following com-</p><p>mand: php artisan make:middleware IsAdmin. handle()method of our</p><p>middleware may looks as follow:</p><p>110 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class IsAdmin</p><p>{</p><p>public function handle($request, Closure $next)</p><p>{</p><p>if (auth()->check() && auth()->user()->isAdmin()) {</p><p>return $next($request);</p><p>}</p><p>return redirect()->route('dashboard');</p><p>}</p><p>}</p><p>Then we need to define new routes in /routes/web.php for our master ad-</p><p>min:</p><p>Route::group(</p><p>[</p><p>'middleware' => ['auth', 'is_admin'],</p><p>'prefix' => 'admin',</p><p>'as' => 'admin.',</p><p>'namespace' => 'Admin',</p><p>],</p><p>function () {</p><p>// Users</p><p>Route::get('/users', 'UserController@index')->name('</p><p>users.index');</p><p>Route::delete('/users/{user}', 'UserController@destroy</p><p>')->name('users.destroy');</p><p>Route::patch('/users/{id}', 'UserController@restore')</p><p>->name('users.restore');</p><p>}</p><p>);</p><p>Andcreate anewUserController insideof/app/Http/Controllers/Admin/</p><p>directory with the following</p><p>methods:</p><p>Max Kostinevich 111</p><p>Building SaaS with Laravel</p><p>class UserController extends Controller</p><p>{</p><p>public function index()</p><p>{</p><p>$users = User::withTrashed()->orderBy('id', 'desc')-></p><p>with('payments')->paginate(25);</p><p>return view('admin.users.index', compact('users'));</p><p>}</p><p>// Delete User</p><p>public function destroy(User $user)</p><p>{</p><p>if($user->isAdmin() || auth()->user()->id == $user->id</p><p>){</p><p>return abort(401);</p><p>}</p><p>$user->delete();</p><p>return redirect()</p><p>->back()->with('status', 'User has been deleted</p><p>successfully');</p><p>}</p><p>// Restore User</p><p>public function restore($user_id)</p><p>{</p><p>$user = User::withTrashed()</p><p>->where('id', $user_id)->firstOrFail();</p><p>$user->restore();</p><p>return redirect()</p><p>->back()->with('status', 'User has been restored</p><p>successfully');</p><p>}</p><p>}</p><p>Then we need to create a view for our master admin in /resources/views/</p><p>admin/ directory.</p><p>112 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>As we’re using Laravel Horizon to manage queues, we also need to allow our</p><p>master admin to log in to Horizon instance. To do sowe need tomodify gate()</p><p>method in /app/Providers/HorizonServiceProvider.php as follows:</p><p>class HorizonServiceProvider extends</p><p>HorizonApplicationServiceProvider</p><p>{</p><p>//...</p><p>protected function gate()</p><p>{</p><p>Gate::define('viewHorizon', function ($user) {</p><p>return in_array($user->email, [</p><p>config('admin.email')</p><p>]);</p><p>});</p><p>}</p><p>}</p><p>The last thing we need to do - is to add links to our master admin and Horizon</p><p>instance to our layouts/app.blade.php view file. Donot forget tomake these</p><p>links visible to master admin only, using user()->isAdmin()method:</p><p><!-- Navigation --></p><p><!-- ... --></p><p>@if(auth()->user()->isAdmin())</p><p><li class="nav-item u-header__nav-item"></p><p><a class="nav-link u-header__nav-link" href="/horizon/"</p><p>target="_blank">Horizon</a></p><p></li></p><p><li class="nav-item u-header__nav-item"></p><p><a class="nav-link u-header__nav-link" href="{{ route('</p><p>admin.users.index') }}">Administration</a></p><p></li></p><p>@endif</p><p><!-- ... --></p><p>Max Kostinevich 113</p><p>https://github.com/laravel/horizon</p><p>Building SaaS with Laravel</p><p>Awesome! At this moment we implemented all necessary features and can</p><p>deploy our app to production server.</p><p>Deploying our app</p><p>At first attempt I wanted to make this chapter in the form of a step-by-step</p><p>tutorial about application deployment. However, as UI of some tools may</p><p>change and deployment process might be a little bit di�erent depending</p><p>on your preferred workflow, I decided to give a quick overview of popular</p><p>deployment options and provice you a few useful link where you can learn</p><p>more.</p><p>There are a number of tools and deployment options we can use to deploy our</p><p>application. For example, some of popular choices are:</p><p>• Laravel Forge - Server management and deployments via Git;</p><p>• Envoyer - Zero-downtime deployments;</p><p>• Ploi.io - Server management, zero-downtime deployments and auto-</p><p>backups;</p><p>• PaaS andmanaged hosting (e.g. Heroku and Cloudways);</p><p>• Containerized hosting and deployment (e.g. Docker);</p><p>• Serverless deployment through Laravel Vapor;</p><p>Personally, I prefer to use DigitalOcean for hosting, Forge for server manage-</p><p>ment and Envoyer for zero-downtime deployments. If a short period of down-</p><p>time isn’t crucial to your app, youmay use Forge without Envoyer for deploy-</p><p>ment.</p><p>If you want to learn more about Forge and Envoyer, I would recommend you</p><p>to check these awesome series on Laracasts:</p><p>• Learn Forge</p><p>114 Max Kostinevich</p><p>https://forge.laravel.com/</p><p>https://envoyer.io/</p><p>https://ploi.io/</p><p>https://www.heroku.com/</p><p>https://www.cloudways.com/</p><p>https://vapor.laravel.com/</p><p>https://www.digitalocean.com/</p><p>https://laracasts.com/series/learn-laravel-forge</p><p>Building SaaS with Laravel</p><p>• Learn Envoyer</p><p>Another great tool for deployment and server management is Ploi, created</p><p>by Dennis Smink. Ploi includes all features Forge and Envoyer has, and cost a</p><p>little bit less than Forge and Envoyer in total.</p><p>If you’re beginner and do not know a lot about server management, youmay</p><p>take a look at Cloudways, they o�er easy-to-use managed hosting platform</p><p>for Laravel apps.</p><p>Laravel Vapor - is entire tool in Laravel eco-system, it allows you to host Laravel</p><p>application in Serverless infrastructure. Serverless approach have it’s own</p><p>pros and cons, and out of scope of this book.</p><p>Regardless of tools you choose, deployment process includes the following</p><p>steps:</p><p>• Setup git repository;</p><p>• Provision new server;</p><p>• Create new database;</p><p>• Configure DNS records and setup SSL certificates;</p><p>• Prepare deployment recipe;</p><p>• Get all required API keys and prepare .env file for production;</p><p>• Createdaemonandqueueworker (if your appusesqueues likebeanstalk</p><p>or redis);</p><p>It’s also a good idea to keep deployment instructions in Readme file of your</p><p>project.</p><p>When going to production, do not forget to setup auto-backups. I also recom-</p><p>mend to setup some appmonitoring tools to be notified when any error occur.</p><p>For example, youmay use Bugsnag or Sentry, both tools provide a free plan.</p><p>Max Kostinevich 115</p><p>https://laracasts.com/series/envoyer</p><p>https://twitter.com/dennis_smink</p><p>https://www.bugsnag.com/</p><p>https://sentry.io/</p><p>Building SaaS with Laravel</p><p>Summary</p><p>Okay, we finished and launched our MVP. I deliberately le� small flaws in app</p><p>source code, so you can take some practice in code refactoring. For example,</p><p>one of the clear areas for refactoring is amountFormattedWithCurrency()</p><p>function, as this function is currently defined twice - in Paymentmodel and in</p><p>our helper file.</p><p>Youmay also add some new features, for example:</p><p>• Add support of digital downloads to allow your users to sell digital con-</p><p>tent;</p><p>• Add support for recurring payments;</p><p>• Add an option which allows your users to embed payment forms to their</p><p>websites as javascript widget;</p><p>• ..andmore!</p><p>116 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Chapter 3. Useful tips</p><p>In this chapter I’ll sharemy thoughts and useful tips aboutmaking and running</p><p>Sofware-as-a-Service applications.</p><p>Types of SaaS</p><p>I would distinct two di�erent types of SaaS: - Standalone SaaS - This is type</p><p>of SaaS, which is not depending on any other service.</p><p>• Extensions for other products - This type of SaaS depends on another</p><p>product or service. For example - Shopify Apps, Apps for Quickbooks,</p><p>Apps for Salesforce, etc. (e.g. Shopify Apps, Apps for Quickbooks, etc)</p><p>Both of types have their ownpros and cons. For example,making a standalone</p><p>SaaS gives youmore freedom and flexibility, as you’re not depending on other</p><p>product. On other side, it could be harder to get your first users of standalone</p><p>SaaS, as many established products have their ownmarketplace where devel-</p><p>opers like you can publish extensions (plugins or apps) for that big product.</p><p>Making an extension for established product usually requires you to follow</p><p>some design guidelines, rules, andmarketplace terms. However, it gives you</p><p>an access to existing and loyal customers who might be happy to use your</p><p>extension (and pay you!).</p><p>Thoughts on pricing and customer retention</p><p>There are several tips about pricing I would like to share:</p><p>• If you’re planning to charge pretty small amount for your SaaS</p><p>(e.g. $3/mo), it’s a good idea to o�er yearly package, as it will gives you</p><p>Max Kostinevich 117</p><p>Building SaaS with Laravel</p><p>more money on inital period, which you can re-invest to marketing.</p><p>• To get more money on inital period, youmay also o�er a lifetime deal to</p><p>your first X customers.</p><p>• Providing a reasonable trial periodmay increase your conversion rate.</p><p>In most cases, 7-14 days is enough to try a product andmake a decision.</p><p>• Be careful with the free plan, as it may increase amount of support</p><p>requests you need to handle.</p><p>• Beproactivewith your first customers, try to follow-upwith each sign-up</p><p>and get a quick feedback about your product.</p><p>If you’re willing to learn more about di�erent aspects of running SaaS, I would</p><p>recommend to join SaaS Club byOmer Khan. SaaS Club provides a lot of useful</p><p>resources, such as group coaching sessions, a huge content library, access to</p><p>private community and expert master classes.</p><p>Thoughts on legal aspects</p><p>If you’re active on Twitter, you probably heard</p><p>about Reilly Chase and his story.</p><p>So if you’re working on your project while keeping your daily job, it’s a good</p><p>idea to get a legal advice and consult your lawyer to make sure you do not</p><p>violate the terms of your employment contract.</p><p>You may also consider to create a new company for your new project. De-</p><p>pending on your requirements, there are several services allowing you to open</p><p>company remotely. Most of popular options are:</p><p>• Stripe Atlas - for US-based company;</p><p>• e-Residency - for Estonian company;</p><p>Before opening a company, it’s a good idea to consult with your accountant</p><p>regarding all important questions (e.g. Tax/VAT handling, local law compliance,</p><p>118 Max Kostinevich</p><p>https://saasclub.io/</p><p>https://twitter.com/_rchase_/status/1082421530934554624</p><p>https://stripe.com/atlas</p><p>https://e-resident.gov.ee/</p><p>Building SaaS with Laravel</p><p>etc).</p><p>Recommended books</p><p>There are a lot of books about making and running so�ware projects, I’ll just</p><p>share my Top-3:</p><p>• Rework by Jason Fried and David Heinemeier Hansson</p><p>• Making ideas happen by Scott Belsky</p><p>• Start Small, Stay Small by Rob Walling</p><p>Max Kostinevich 119</p><p>https://www.amazon.com/Rework/dp/B003BLGD06/</p><p>https://www.amazon.com/Making-Ideas-Happen-Overcoming-Obstacles/dp/1591844118/</p><p>https://www.amazon.com/Start-Small-Stay-Developers-Launching/dp/0615373968</p><p>Building SaaS with Laravel</p><p>A�erword</p><p>Thank you for reading this book, I hope you found it useful!</p><p>If you have any questions, found a typo or just want to provide a feedback,</p><p>feel free to shoot me a tweet at maxkostinevich or email me at hello@maxko</p><p>stinevich.com</p><p>—Max Kostinevich</p><p>120 Max Kostinevich</p><p>https://twitter.com/maxkostinevich</p><p>mailto:hello@maxkostinevich.com</p><p>mailto:hello@maxkostinevich.com</p><p>Introduction</p><p>What we are going to build</p><p>Prerequisites</p><p>Getting the source code</p><p>Working with the source code</p><p>Workspace setup</p><p>About the author</p><p>Contact the author</p><p>Chapter 1. Planning our app</p><p>Designing wireframes</p><p>Designing database</p><p>Chapter 2. Building our app</p><p>Creating new app</p><p>Preparing views</p><p>Static pages</p><p>Asset compilation</p><p>Authentication pages</p><p>Building main features</p><p>Dashboard</p><p>Settings</p><p>Payment forms</p><p>Payment form frontend</p><p>Accepting payments</p><p>Managing payments</p><p>Dashboard stats</p><p>Adding notifications</p><p>Small improvements</p><p>Building master admin</p><p>Deploying our app</p><p>Summary</p><p>Chapter 3. Useful tips</p><p>Types of SaaS</p><p>Thoughts on pricing and customer retention</p><p>Thoughts on legal aspects</p><p>Recommended books</p><p>Afterword</p><p>I would recommend you to try</p><p>it out! Ngrok allows you expose your local server to the internet over a secure</p><p>tunnel. This tool is super-useful for testing OAuth integrations, webhooks,</p><p>3rd-party API calls and so on.</p><p>About the author</p><p>Max Kostinevich is a solutions consultant and web-developer. Max have over</p><p>10 years of extensive experience in eCommerce and SaaS consulting and de-</p><p>10 Max Kostinevich</p><p>https://laravel.com/docs/5.8/homestead</p><p>https://laravel.com/docs/5.8/valet</p><p>https://www.docker.com/</p><p>https://ngrok.com/</p><p>https://www.jetbrains.com/phpstorm/</p><p>https://notepad-plus-plus.org/</p><p>https://cmder.net/</p><p>https://www.youtube.com/watch?v=DNyQX00X_cg</p><p>https://github.com/laravel-101/Laravel-Docker-Template</p><p>https://ngrok.com/</p><p>https://maxkostinevich.com/</p><p>Building SaaS with Laravel</p><p>velopment, and have worked with dozens of companies worldwide, including</p><p>multinational companies on the Inc. 5000.</p><p>Contact the author</p><p>If you have any questions, ideas, suggestions, orwant to report an error, please</p><p>email me at hello@maxkostinevich.com</p><p>For most recent updates, please followme on Twitter: maxkostinevich</p><p>Max Kostinevich 11</p><p>mailto:hello@maxkostinevich.com</p><p>https://twitter.com/maxkostinevich</p><p>Building SaaS with Laravel</p><p>Chapter 1. Planning our app</p><p>Detailed plan is the key to successful results. However, when planning theMVP,</p><p>it’s important to keep your requirements list as simple as possible, just because</p><p>you can easily got drown in all your notes, ideas, and wanted features.</p><p>So first, we need to clearly define what our project does and extract most</p><p>essential features of our project and write them down in project specification</p><p>file. Based on this file we can create wireframes to get better idea of how our</p><p>project will looks like.</p><p>Designing wireframes</p><p>So what the wireframing is? Wireframing is the process of transforming app</p><p>spec into graphicc representation at the structural level. You may think about</p><p>wireframing as about low-level design where you focus on features and layout</p><p>instead of high-level details. Wireframes helps us to get better idea of how our</p><p>project may looks like, how all features will work together and how long the</p><p>development process may take before we even write a single line of code.</p><p>When designing wireframes it’s important to not to focus toomuch on small</p><p>details. It’s a good idea to ask yourself - how itmay looks like and how it should</p><p>work?’</p><p>Usually wireframing takes a few iterations before we get clear idea of what we</p><p>are going to build.</p><p>I recommend to make some initial wireframing on paper, as this is most quick-</p><p>est way to do this. Then youmay create digital copy of your wireframes. There</p><p>are several tools which I use for wireframing:</p><p>• Sneakpeekit - just a print template for wireframes on the paper</p><p>12 Max Kostinevich</p><p>http://sneakpeekit.com/</p><p>Building SaaS with Laravel</p><p>• Balsamiq - a great tool allowing to quickly create wireframes andmock-</p><p>ups</p><p>• UX-App - a web-based application allowing to create interactive wire-</p><p>frames</p><p>For PayMe I created interactive wireframes using UX App, see the image below.</p><p>You can also find these wireframes here.</p><p>This is the final version of wireframes for PayMe. Before creating these wire-</p><p>frames in UX App, I spent some time to make a few versions on paper.</p><p>A�erwehaveourwireframeson file,wecanproceed to thenext step -designing</p><p>Max Kostinevich 13</p><p>https://balsamiq.com/</p><p>https://www.ux-app.com/</p><p>https://www.ux-app.com/device/view?s=MKVE7438&l=1&pg=197339</p><p>Building SaaS with Laravel</p><p>database.</p><p>Designing database</p><p>A good way to design database is to ask yourself the following questions:</p><p>• Which models may I need?</p><p>• Which attributes eachmodels may need?</p><p>• How these models should be relate to one another?</p><p>It’s important to remember that our database structure may change during</p><p>the development process, and that’s totally fine. At that stage our main goal -</p><p>is to define starting point fromwhich we can start building something.</p><p>For PayMe we can define 3 main models:</p><p>• Users (Sellers)</p><p>• Forms</p><p>• Payments (Sales)</p><p>In additional to default Laravel attributes, for each User (Seller) we’ll need to</p><p>store their connected Stripe Account ID, their profile picture, and company.</p><p>For Payment Forms we’ll need to store related User ID, UID (which will be used</p><p>in unique URL), service description, amount and currency.</p><p>For Payments we’ll need to store related Form ID, charge ID (Transaction ID</p><p>from Stripe), customer name and email, application fee we collected for that</p><p>payment, and receipt URL (which is automatically generated by Stripe).</p><p>We can also assume the following conditions:</p><p>• Each User can have multiple Forms</p><p>• Each User can have multiple Payments</p><p>14 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>• Each Form can have multiple Payments</p><p>• Each Form relates to only one User</p><p>• Each Payment relates to only one Form</p><p>• Each Payment relates to only one User (the same as the owner of the</p><p>Form)</p><p>• The Payment could be refunded to the customer</p><p>As you can see on the image below, the database structure is pretty simple:</p><p>Please, note - payment model may not have a user_id, as we can get this ID</p><p>from the related Formmodel. However I decided to store related user_id in</p><p>Payments model too, as it will make it much easier to us to calculate statistics</p><p>and get the history of user payments.</p><p>Useful tools to design the database:</p><p>• QuickDatabaseDiagrams</p><p>• DBDiagram</p><p>Max Kostinevich 15</p><p>https://www.quickdatabasediagrams.com/</p><p>https://dbdiagram.io/home</p><p>Building SaaS with Laravel</p><p>• Lucidchart</p><p>• Draw.io</p><p>In the next chapter we’ll start building our app.</p><p>16 Max Kostinevich</p><p>http://lucidchart.com/</p><p>http://draw.io/</p><p>Building SaaS with Laravel</p><p>Chapter 2. Building our app</p><p>In this chapter we’re going to build our application from scratch.</p><p>A�er we have our wireframes and database design finished, we can start build-</p><p>ing our app.</p><p>There are no strict rules on how exactly to build the app, personally I prefer</p><p>the following sequence:</p><p>1. Create wireframes.</p><p>2. Design the database structure.</p><p>3. Prepare application plain HTML templates for each important page/lay-</p><p>out.</p><p>4. Install fresh Laravel application.</p><p>5. Convert plain HTML templates to Laravel views.</p><p>6. Start building main functionality.</p><p>For this book I will leave the creation of plain HTML templates behind the</p><p>scenes, as this is not the main focus of this book and the process is di�erent</p><p>for each project. You may find all plain HTML templates in the source code</p><p>attached to the book.</p><p>Creating new app</p><p>Related tag: step-1.x</p><p>I assume that you already prepared your local environment. So let’s create a</p><p>new Laravel application by running the following command in our console:</p><p>laravel new payme</p><p>Max Kostinevich 17</p><p>Building SaaS with Laravel</p><p>Then let’s update our .env file and update database credentials and APP_URL</p><p>variable.</p><p>As I use Ngrok, my APP_URL variable looks as following:</p><p>APP_URL=https://payme.ngrok.io</p><p>I always force https protocol in my apps. To do this, we’ll just need to add</p><p>\URL::forceScheme('https'); to boot() method in our app/Providers/</p><p>AppServiceProvider.php:</p><p>class AppServiceProvider extends ServiceProvider</p><p>{</p><p>//...</p><p>public function boot()</p><p>{</p><p>// Force SSL</p><p>\URL::forceScheme('https');</p><p>}</p><p>}</p><p>As our app requires user registration and authentication, we need to enable</p><p>Laravel’s built-in authentication feature. To do this we’ll need just run php</p><p>artisan make:auth and php artisan migrate in our console.</p><p>A�er we enabled authentication, we can also enable Laravel’s built-in email</p><p>verification feature. This feature will force newly registered users to verify their</p><p>email addresses. To do this, we’ll need to make just a few things:</p><p>1. Make sure that our Usermodel implements Illuminate\Contracts\</p><p>Auth\MustVerifyEmail contract:</p><p>18 Max Kostinevich</p><p>https://ngrok.com/</p><p>https://laravel.com/docs/5.8/authentication</p><p>https://laravel.com/docs/5.8/verification</p><p>https://laravel.com/docs/5.8/verification</p><p>Building SaaS with Laravel</p><p><?php</p><p>namespace App;</p><p>use Illuminate\Notifications\Notifiable;</p><p>use Illuminate\Contracts\Auth\MustVerifyEmail;</p><p>use Illuminate\Foundation\Auth\User as Authenticatable;</p><p>class User extends Authenticatable implements MustVerifyEmail</p><p>{</p><p>use Notifiable;</p><p>// ...</p><p>}</p><p>2. Make sure that users table have email_verified_at column (it’s in-</p><p>cluded by default).</p><p>3. A�er that we can pass the verify option to the Auth::routesmethod</p><p>to activate email verification routes:</p><p>Auth::routes(['verify' => true]);</p><p>For email testing on local development I use Mailhog which catches all email</p><p>sent by our app to it’s own local tiny SMTP server with aweb-basedUI. Mailhog</p><p>is included by default to my Docker template. To use Mailhog we’ll need to</p><p>update email settings in our .env file:</p><p>MAIL_DRIVER=smtp</p><p>MAIL_HOST=mailhog</p><p>MAIL_PORT=1025</p><p>Next, we can copy our HTML templates into /resources/views/_HTML folder.</p><p>This step is not necessary and it’s just my personal preference as I’d like to</p><p>Max Kostinevich 19</p><p>https://github.com/mailhog/MailHog</p><p>https://github.com/laravel-101/Laravel-Docker-Template</p><p>Building SaaS with Laravel</p><p>keep these files at one place. A�er we convert our HTML templates to Laravel</p><p>views, we’ll remove this folder.</p><p>At this stage we can also install some libraries which we’ll use later. I usually</p><p>install Guzzle, which helps us to make HTTP requests (e.g. API calls). To install</p><p>this library, just run the following command in your console:</p><p>composer require guzzlehttp/guzzle</p><p>Another useful package I usually install is a Laravel Horizon. Horizon helps us</p><p>to manage our Redis queues by providing web-based UI. If you’re not familiar</p><p>with queues or Horizon - don’t worry’ we’ll talk about it a little bit later. To</p><p>install horizon just run the following command in your console:</p><p>composer require laravel/horizon</p><p>A�er that we can install Horizon:</p><p>php artisan horizon:install</p><p>And create failed_jobs table (in this table Laravel will store all information</p><p>about our failed jobs) using the following command:</p><p>php artisan queue:failed-table</p><p>php artisan migrate</p><p>In order to use Horizon and Redis, we need update change QUEUE_CONNECTION</p><p>variable in our .env file:</p><p>QUEUE_CONNECTION=redis</p><p>That’s all, at the next step we’ll convert our HTML templates into Laravel views</p><p>and customize our authentication pages.</p><p>20 Max Kostinevich</p><p>http://docs.guzzlephp.org/en/stable/</p><p>https://laravel.com/docs/5.8/horizon</p><p>https://laravel.com/docs/5.8/queues</p><p>Building SaaS with Laravel</p><p>Preparing views</p><p>Related tag: step-2.x</p><p>A�er we provisioned our new Laravel app, it’s time to convert HTML templates</p><p>into Laravel’s blade templates.</p><p>Depending on how you decide to organize the structure of your app, your app</p><p>may have some static pages (e.g. privacy policy, terms of service, etc.). There</p><p>are a few popular ways to do this:</p><p>• The app can be hosted on themain domain, in this case all secondary</p><p>pages should be served by the app;</p><p>• The app can be hosted on a sub-domain, e.g. app.example.com, in this</p><p>case all secondary pages could be served using separate CMS (e.g. Word-</p><p>Press) on the main domain.</p><p>In our case, we’ll have 3 static secondary pages which should be served by our</p><p>app:</p><p>• Homepage</p><p>• Terms of Service</p><p>• Privacy Policy</p><p>Let’s start by converting these pages to Laravel views.</p><p>Static pages</p><p>Related tag: step-2.1</p><p>First, let’s copyindex.html, privacy.htmlandterms.html from/resources</p><p>/views/_HTML/ to resources/views/pages/ and change extension of these</p><p>files to .blade.php.</p><p>Max Kostinevich 21</p><p>Building SaaS with Laravel</p><p>As we have custom 404 page, let’s also copy 404.html from /resources/</p><p>views/_HTML/ to resources/views/errors/ and change the file extension</p><p>to .blade.php.</p><p>Then, let’s create a new controller called PageController to handle our static</p><p>pages. To create a new controller just run the following command in the</p><p>console:</p><p>php artisan make:controller PageController</p><p>Youmaynotice that our controller hasbeencreated inapp/Http/Controllers</p><p>/ folder.</p><p>Then we can remove welcome.blade.php view file from resources/views/</p><p>and change / route in /routes/web.php to</p><p>Route::get('/', 'PageController@home')->name('page.home');</p><p>We can also add routes to our other static pages:</p><p>Route::get('/terms', 'PageController@terms')->name('page.terms</p><p>');</p><p>Route::get('/privacy', 'PageController@privacy')->name('page.</p><p>privacy');</p><p>Then we need to create these 3 methods (home(), terms() and privacy()) in</p><p>our PageController:</p><p>22 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class PageController extends Controller</p><p>{</p><p>// Homepage</p><p>public function home()</p><p>{</p><p>return view('pages.home');</p><p>}</p><p>// Terms</p><p>public function terms()</p><p>{</p><p>return view('pages.terms');</p><p>}</p><p>// Privacy</p><p>public function privacy()</p><p>{</p><p>return view('pages.privacy');</p><p>}</p><p>}</p><p>If you check our new homepage, youmay notice that it is rendered with issues,</p><p>as all assets (js, css, images) are missing. Let’s fix it now!</p><p>Asset compilation</p><p>Related tag: step-2.1</p><p>Tomake our static pages work correctly we need to properly setup and con-</p><p>figure the process of asset compilation. This process is depends on how your</p><p>HTML templates are actually built and in some cases may take some time to</p><p>setup everything correctly. In our case, HTML templates are built using Laravel</p><p>Mix, so it will takes us just a fewminutes to setup asset compilation process.</p><p>Max Kostinevich 23</p><p>Building SaaS with Laravel</p><p>First, lets copy js/,sass/ and vendor/ folders from /resources/views/</p><p>_HTML/resources/ to /resources/</p><p>Then we need to rename core.js to app.js in /resources/js/.</p><p>Then we need to copy the content of webpack.mix.js located in /resources</p><p>/views/_HTML/ to our app’s webpack.mix.js located in the root of our appli-</p><p>cation.</p><p>Then we need to copy images from /resources/views/_HTML/public/img/</p><p>to /public/img/.</p><p>A�er that we need to update Bootstrap to version 4.13 in the package.json</p><p>.</p><p>We also need to install jquery-migrate library by running the following com-</p><p>mand in our console:</p><p>npm install --save jquery-migrate</p><p>A�er that we can build our assets by running the following command:</p><p>npm run dev</p><p>Then let’s get back to our static pages and update URLs to all missing assets.</p><p>In /resources/views/pages/home.blade.phpwe need to update the follow-</p><p>ing:</p><p>• Lang attribute in html tag: <html lang="{{ str_replace('_', '-',</p><p>app()->getLocale())}}"></p><p>• Add csrf-token meta tag: <meta name="csrf-token"content="{{</p><p>csrf_token()}}"></p><p>• Update page title tag: <title>{{ config('app.name', 'Laravel'</p><p>)}} - Home</title></p><p>• Update links to favicon, css, js and images:</p><p>24 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>– <link rel="shortcut icon"href="{{ asset('favicon.png')</p><p>}}"></p><p>– <link rel="stylesheet"href="{{ asset('css/app.css')}}"</p><p>></p><p>– <script src="{{ asset('js/app.js')}}"></script></p><p>– <img src="{{ asset('img/icon-card.svg')}}"></p><p>• Update URLs to login, sign up and other pages:</p><p>– href="{{ route('login')}}</p><p>– href="{{ route('register')}}</p><p>We need to make the same changes in /resources/views/pages/terms.</p><p>blade.php file.</p><p>As the same layout is used in Terms Of Service and Privacy Policy pages, we</p><p>can extract the layout from terms.blade.php:</p><p>First, let’s create a new file called static.blade.php in /resources/views/</p><p>layouts/ folder.</p><p>Then copy all the content from from terms.blade.php to static.blade.php.</p><p>In static.blade.php replace the content of <main> tag with the following</p><p>content:</p><p>@yield('content')</p><p>Wecanalso update the title tag, aswe’d like to have anoption topass custom</p><p>page title for each page:</p><p><title>{{ config('app.name', 'Laravel') }} - @yield('title')</</p><p>title></p><p>Then we can delete everything fromterms.blade.php except the content</p><p>of container div and tell the template to use our static layout by using</p><p>Max Kostinevich 25</p><p>Building SaaS with Laravel</p><p>@extends directive:</p><p>@extends('layouts.static')</p><p>@section('title', 'Terms of Service')</p><p>@section('content')</p><p><div class="container"></p><p><!-- Terms of Service content --></p><p></div></p><p>@endsection</p><p>We can also extract the content of the footer into a component, as it will be the</p><p>same in all our pages. To do this, let’s create a footer.blade.php inside of /</p><p>resources/views/components/ folder. Then let’s copy the content of footer</p><p>tag from static.blade.php to this file. Then we can use this component in</p><p>our static layout by using @include directive:</p><p>@include('components.footer')</p><p>Do not forget to update all necessary links in the footer component.</p><p>Our finished Terms Of Service page is shown on the image below:</p><p>26 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Once we created layout file for our static pages and updated our Terms of</p><p>Service page, we can update our Privacy Policy page. To do this, we just need</p><p>to use @extends directive to tell the view to use static layout and insert the</p><p>content of the page by using @section directive:</p><p>Max Kostinevich 27</p><p>Building SaaS with Laravel</p><p>@extends('layouts.static')</p><p>@section('title', 'Privacy Policy')</p><p>@section('content')</p><p><div class="container"></p><p><!-- Privacy Policy content --></p><p></div></p><p>@endsection</p><p>The last thing we need to do is to update our 404 page. The process will be</p><p>the same as we did this for our Homepage. We need to update the following in</p><p>/resources/views/errors/404.blade.php:</p><p>• Lang attribute in html tag: <html lang="{{ str_replace('_', '-',</p><p>app()->getLocale())}}"></p><p>• Add csrf-token meta tag: <meta name="csrf-token"content="{{</p><p>csrf_token()}}"></p><p>• Update page title tag: <title>{{ config('app.name', 'Laravel'</p><p>)}} - Not found</title></p><p>• Update links to favicon, css, js and images:</p><p>– <link rel="shortcut icon"href="{{ asset('favicon.png')</p><p>}}"></p><p>– <link rel="stylesheet"href="{{ asset('css/app.css')}}"</p><p>></p><p>– <script src="{{ asset('js/app.js')}}"></script></p><p>• Update year and app name in the footer</p><p>Alright! We finished with our static pages and canmove on by customizing our</p><p>authentication pages.</p><p>28 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Authentication pages</p><p>Related tag: step-2.2</p><p>Let’s customize our authentication pages. First, let’s copy login.html, signup</p><p>.html, password-reset.html, password-reset-2.html and password-</p><p>verify.html from /resources/views/_HTML/ to /resources/views/auth/</p><p>.</p><p>Then let’s start by customizing our Login page. I prefer to keep original login</p><p>.blade.php for reference until the customization is done. So first, we need to</p><p>rename original login.blade.php to something like _old_login.blade.php</p><p>and then rename login.html to login.blade.php</p><p>Then we need to update our new login.blade.php the same as we did this</p><p>for our Homepage:</p><p>• Lang attribute in html tag: <html lang="{{ str_replace('_', '-',</p><p>app()->getLocale())}}"></p><p>• Add csrf-token meta tag: <meta name="csrf-token"content="{{</p><p>csrf_token()}}"></p><p>• Update page title tag: <title>{{ config('app.name', 'Laravel'</p><p>)}} - Not found</title></p><p>• Update links to favicon, css, js and images:</p><p>– <link rel="shortcut icon"href="{{ asset('favicon.png')</p><p>}}"></p><p>– <link rel="stylesheet"href="{{ asset('css/app.css')}}"</p><p>></p><p>– <script src="{{ asset('js/app.js')}}"></script></p><p>Wealsoneed toaddappropriate formactionandCSRF field to the login form:</p><p>Max Kostinevich 29</p><p>Building SaaS with Laravel</p><p><form method="POST" action="{{ route('login') }}"></p><p>@csrf</p><p><!-- ... --></p><p></form></p><p>We can use original _old_login.blade.php as an example. Then we need</p><p>to apply all necessary names and classes to form input fields and add error</p><p>messages, for example:</p><p><!-- Form Group --></p><p><div class="form-group"></p><p><label class="form-label" for="email">Email address</label</p><p>></p><p><input type="email" class="form-control{{ $errors->has('</p><p>email') ? ' is-invalid' : '' }}" name="email" id="email</p><p>" value="{{ old('email') }}"</p><p>placeholder="Email address"/></p><p>@if ($errors->has('email'))</p><p><span class="invalid-feedback" role="alert"></p><p><strong>{{ $errors->first('email') }}</strong></p><p></span></p><p>@endif</p><p></div></p><p><!-- End Form Group --></p><p>We also need to update all necessary links to Password Reset and Signup</p><p>pages, for example:</p><p>30 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>@if (Route::has('register'))</p><p><div class="col-6"></p><p><span class="small text-muted">Don't have an account</p><p>?</span></p><p><a class="small" href="{{ route('register') }}">Signup</p><p></a></p><p></div></p><p>@endif</p><p>Alright, we just finished customization of our Login page, and we can remove</p><p>_old_login.blade.php as we do not need it anymore.</p><p>Before moving on with other authentication pages, we can extract the layout</p><p>from our Login page, as the same layout will be used across other authentica-</p><p>tion pages:</p><p>Let’s create a new file called auth.blade.php in /resources/views/layouts</p><p>/ folder. Then let’s copy the content of login.blade.php to this file.</p><p>Max Kostinevich 31</p><p>Building SaaS with Laravel</p><p>In auth.blade.php replace the form tag with @yield directive:</p><p>@yield('content')</p><p>And update title tag:</p><p><title>{{ config('app.name', 'Laravel') }} - @yield('title')</</p><p>title></p><p>Then we can remove everything fromlogin.blade.php except the content</p><p>of form tag and tell the template to use our auth layout by using @extends</p><p>directive:</p><p>@extends('layouts.auth')</p><p>@section('title', 'Login')</p><p>@section('content')</p><p><!-- Form --></p><p><form class="js-validate mt-5" method="POST" action="{{</p><p>route('login') }}"></p><p>@csrf</p><p><!-- ... --></p><p></form></p><p>@endsection</p><p>Alright! Once we finished with our Login page, we can start customizing</p><p>our Registration page. First, let’s rename original register.blade.php to</p><p>_old_register.blade.php and use this file for reference.</p><p>Then let’s rename signup.html to register.blade.php and remove every-</p><p>thing except of registration form. Then we can apply our auth layout and</p><p>update registration form the same way as we did this for our login form. We</p><p>need to update form action, add CSRF field, update input names, classes and</p><p>error messages. Also we need to update the link to our Login page. A�er that</p><p>32 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>we can remove _old_register.blade.php and proceed with Password Reset</p><p>forms.</p><p>In Laravel Password Reset feature consists of 2 separate forms:</p><p>• Password Request Form - the form where the user requests a password</p><p>reset by entering his email;</p><p>• Password Reset Form - the formwhere the update the password a�er</p><p>clicking a link which has been emailed on previous form.</p><p>Let’s start by customizing Password Request form. First, we need to</p><p>rename original email.blade.php file located in /resources/views/auth/</p><p>passwords/ to something like _old_email.blade.php (as we’d like to keep</p><p>this file for reference).</p><p>Then we need to rename password-reset.html to email.blade.php and</p><p>move this file to /resources/views/auth/passwords/ directory.</p><p>Then we need to apply auth layout the same way we did this for our Login</p><p>page and update the form action, add CSRF field, update input names, classes</p><p>and error messages. Then we can remove original _old_email.blade.php.</p><p>Next, we need to customize Password Reset form, the process is absolutely</p><p>the same:</p><p>• Rename reset.blade.php file located in /resources/views/auth/</p><p>passwords/ to _old_reset.blade.php;</p><p>• Rename password-reset-2.html to reset.blade.php andmove this</p><p>file to /resources/views/auth/passwords/ directory.</p><p>• Apply auth layout;</p><p>• Update the form action, add CSRF field, update input names, classes</p><p>and error messages;</p><p>Max Kostinevich 33</p><p>Building SaaS with Laravel</p><p>• Donot forget to add a hidden field to the formwhichwill store generated</p><p>token:</p><p><input type="hidden" name="token" value="{{ $token }}"></p><p>The last page we need to customize - is a email verification template. Built-in</p><p>email verification feature has been introduced in Laravel 5.7 and I strongly</p><p>recommend you to use this feature in your projects.</p><p>Let’s rename verify.blade.php to _old_verify.blade.php file located in</p><p>/resources/views/auth/.</p><p>Then let’s rename password-verify.html to verify.blade.php.</p><p>This page does not contain any forms, so all we need is to apply auth layout</p><p>and update text copy and the link to Resend Password route. Then we can</p><p>remove old_verify.blade.php.</p><p>Congratulations! We just finished customization of our authentication pages</p><p>and can proceed with building features for our app.</p><p>Buildingmain features</p><p>Related tag: step-3.x</p><p>Once we customized authentication pages for our app, we can start building</p><p>main features.</p><p>Dashboard</p><p>Related tag: step-3.1</p><p>Let’s get started by creating</p><p>a Dashboard.</p><p>34 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>At this step we can remove created by default HomeController located in app</p><p>/Http/Controllers/ as we don’t need it anymore, along with /home route</p><p>and home view.</p><p>Then we need to create a new DashboardController controller by running</p><p>the following command:</p><p>php artisan make:controller DashboardController</p><p>Next, we need to assign authmiddleware to this controller, we can do this by</p><p>using middlewaremethod in our controller’s constructor:</p><p>class DashboardController extends Controller</p><p>{</p><p>/**</p><p>* Create a new controller instance.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function __construct()</p><p>{</p><p>$this->middleware('auth');</p><p>}</p><p>//...</p><p>}</p><p>Thenwe need to create a /dashboard route. As we’d like to allow access to our</p><p>app to users who verified email, we can apply verifiedmiddleware. So at</p><p>the end of our routes file /routes/web.phpwe can add the following lines:</p><p>Max Kostinevich 35</p><p>Building SaaS with Laravel</p><p>Route::group(</p><p>[</p><p>'middleware' => ['verified'],</p><p>],</p><p>function () {</p><p>Route::get('/dashboard', 'DashboardController@index')</p><p>->name('dashboard');</p><p>}</p><p>);</p><p>Next we need to copy dashboard.html from /resources/views/_HTML/ to</p><p>/resources/views/dashboard/ and rename it to index.blade.php.</p><p>Once we created a view (we’re going to customize it in a while), we need to</p><p>create a indexmethod in our DashboardControllerwhich should return a</p><p>Dashboard view:</p><p>class DashboardController extends Controller</p><p>{</p><p>// ...</p><p>/**</p><p>* Show the application dashboard.</p><p>*/</p><p>public function index()</p><p>{</p><p>return view('dashboard.index');</p><p>}</p><p>}</p><p>As we do not have a /home route anymore, and our main entry-point is /</p><p>dashboard, let’s set a proper redirect to this route. There are a few options we</p><p>can do this, one option is to set a redirect in our routes file, it may looks like</p><p>36 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>the following:</p><p>Route::get('/home', function () {</p><p>return redirect()->route('dashboard');</p><p>});</p><p>Or we can change $redirectTo attribute to /dashboard in all controllers (ex-</p><p>cept ForgotPasswordController) located in /app/Http/Controllers/Auth</p><p>/ :</p><p>/**</p><p>* Where to redirect users after registration.</p><p>*</p><p>* @var string</p><p>*/</p><p>protected $redirectTo = '/dashboard';</p><p>And then change entry point in RedirectIfAuthenticated.phpmiddleware</p><p>located in /app/Http/Middleware/:</p><p>class RedirectIfAuthenticated</p><p>{</p><p>//...</p><p>public function handle($request, Closure $next, $guard =</p><p>null)</p><p>{</p><p>if (Auth::guard($guard)->check()) {</p><p>return redirect('/dashboard');</p><p>}</p><p>return $next($request);</p><p>}</p><p>}</p><p>A> A small note: If built-in Email Verification feature doesn’t work properly, for</p><p>Max Kostinevich 37</p><p>Building SaaS with Laravel</p><p>example - you receive “TokenExpired” errorwhenclickingonEmail Verification</p><p>link, than you need to apply a quick fix: Just go to TrustProxies.phpmiddle-</p><p>ware located in /app/Http/Middleware/ and update $proxies attribute to</p><p>$proxies = '*';</p><p>Next let’s get back to our dashboard view located in /resources/views/</p><p>dashboard/index.blade.php and update the following:</p><p>• Lang attribute in html tag: <html lang="{{ str_replace('_', '-',</p><p>app()->getLocale())}}"></p><p>• Add csrf-token meta tag: <meta name="csrf-token"content="{{</p><p>csrf_token()}}"></p><p>• Update page title tag: <title>{{ config('app.name', 'Laravel'</p><p>)}} - Dashobard</title></p><p>• Update links to favicon, css, js and images:</p><p>– <link rel="shortcut icon"href="{{ asset('favicon.png')</p><p>}}"></p><p>– <link rel="stylesheet"href="{{ asset('css/app.css')}}"</p><p>></p><p>– <script src="{{ asset('js/app.js')}}"></script></p><p>• Replace footer tag with footer component: @include('components.</p><p>footer')</p><p>• Update user name in header by adding the following code: {{ Auth::</p><p>user()->name }}</p><p>We also need to update a link to user logout. In Laravel, logout is processed by</p><p>POST request, so we need to create a hidden form for this purpose:</p><p>38 Max Kostinevich</p><p>Building SaaS with Laravel</p><p><!-- Logout Link --></p><p><a href="#" onclick="event.preventDefault(); document.</p><p>getElementById('logout-form').submit();">Logout</a></p><p><!-- Logout Form --></p><p><form id="logout-form" action="{{ route('logout') }}" method="</p><p>POST" style="display: none;"></p><p>@csrf</p><p></form></p><p><!-- End Logout Form --></p><p>Then we can extract app layout and header component from our Dashboard</p><p>view as shown on the picture below:</p><p>Max Kostinevich 39</p><p>Building SaaS with Laravel</p><p>At this step we also can create a notification component and include it to</p><p>our app layout:</p><p>40 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>@if ($errors->any())</p><p><div class="alert alert-danger" role="alert"></p><p><div class="font-weight-bold">Oops! Please, fix the</p><p>following errors:</div></p><p><ul class="mb-0"></p><p>@foreach ($errors->all() as $error)</p><p><li>{{ $error }}</li></p><p>@endforeach</p><p></ul></p><p></div></p><p>@endif</p><p>@if (session('status'))</p><p><div class="alert alert-success" role="alert"></p><p>{{ session('status') }}</p><p></div></p><p>@endif</p><p>We can also add a guestmiddleware to our homepage, as we’d like to redirect</p><p>all logged-in users straight to Dashboard. To do so just go to PageController</p><p>controller and add __constructmethod:</p><p>class PageController extends Controller</p><p>{</p><p>//...</p><p>public function __construct()</p><p>{</p><p>$this->middleware('guest')->only('home');</p><p>}</p><p>//...</p><p>}</p><p>Max Kostinevich 41</p><p>Building SaaS with Laravel</p><p>Settings</p><p>Related tag: step-3.2</p><p>A�er we finished our Dashboard, let’s create our Settings page.</p><p>First, let’s copy settings.html from /resources/views/_HTML/ to /</p><p>resources/views/settings/ and rename it to edit.blade.php.</p><p>Then let’s create a SettingsController by running the following command</p><p>in our console:</p><p>php artisan make:controller SettingsController</p><p>Do not forget to assign authmiddleware to this controller the same way we</p><p>did this for DashboardController:</p><p>class SettingsController extends Controller</p><p>{</p><p>/**</p><p>* Create a new controller instance.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function __construct()</p><p>{</p><p>$this->middleware('auth');</p><p>}</p><p>//...</p><p>}</p><p>Then let’s add a edit()method to our SettingsControllerwhich will used</p><p>to render our settings form. At this step we can also create a placeholder for</p><p>our update():</p><p>42 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class SettingsController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Show the settings page.</p><p>*/</p><p>public function edit()</p><p>{</p><p>return view('settings.edit');</p><p>}</p><p>/**</p><p>* Update user settings.</p><p>*/</p><p>public function update(Request $request)</p><p>{</p><p>// @TODO: save user's settings</p><p>return true;</p><p>}</p><p>}</p><p>A�er this we can add our /settings route to our /routes/web.php file just</p><p>below /dashboard route:</p><p>Max Kostinevich 43</p><p>Building SaaS with Laravel</p><p>Route::group(</p><p>[</p><p>'middleware' => ['verified'],</p><p>],</p><p>function () {</p><p>// Dashboard</p><p>//...</p><p>// Settings</p><p>Route::get('/settings', 'SettingsController@edit')-></p><p>name('settings.edit');</p><p>Route::patch('/settings', 'SettingsController@update')</p><p>->name('settings.update');</p><p>});</p><p>As we’d like to store optional user’s company name, we need to add a column</p><p>for this to our Users table. To do this, we need to run the following command</p><p>in our console:</p><p>php artisan make:migration add_company_to_users_table --table=</p><p>users</p><p>We can specify a database table name we’d like to use by passing --table</p><p>option to our artisan command.</p><p>Let’s call our new column company and place it a�er user name. Our migration</p><p>will looks as follow:</p><p>44 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class AddCompanyToUsersTable extends Migration</p><p>{</p><p>/**</p><p>* Run the migrations.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function up()</p><p>{</p><p>Schema::table('users', function (Blueprint $table) {</p><p>$table->string('company')->after('name')->nullable</p><p>();</p><p>});</p><p>}</p><p>/**</p><p>* Reverse the migrations.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function down()</p><p>{</p><p>Schema::table('users', function (Blueprint $table) {</p><p>$table->dropColumn('company');</p><p>});</p><p>}</p><p>}</p><p>Do not forget to add dropColumn to down()method, so company column will</p><p>be deleted onmigration rollback.</p><p>Then we can run our migration using the following command:</p><p>php artisan migrate</p><p>A�er we created our migration, we can start working on settings form itsef.</p><p>Max Kostinevich 45</p><p>Building SaaS with Laravel</p><p>First, we need to apply app layout to our settings.blade.php view. Then we</p><p>need to update our</p><p>settings form:</p><p>• Update a formmethod to POST, set form action to action="{{ route</p><p>('settings.update')}}" and add csrf field;</p><p>• Update form fields: add proper name and value attributes;</p><p>• Add type="submit" to Submit button;</p><p>Then we need to update our update()method in SettingsController:</p><p>46 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class SettingsController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Update user settings.</p><p>*/</p><p>public function update(Request $request)</p><p>{</p><p>$user = auth()->user();</p><p>$request->validate([</p><p>'name' => 'required',</p><p>'email' => 'required|email|unique:users,email,' .</p><p>$user->id</p><p>]);</p><p>$user->name = $request->input('name');</p><p>$user->email = $request->input('email');</p><p>$user->company = $request->input('company');</p><p>$user->save();</p><p>return redirect()</p><p>->back()</p><p>->with('status', 'Your settings have been updated</p><p>successfully.');</p><p>}</p><p>}</p><p>Notice a little trick we used in form validation - this way we can be sure all</p><p>users have unique emails.</p><p>Next, let’s add allow user to upload their avatar or profile picture:</p><p>First, we need to add enctype="multipart/form-data"to our settings form,</p><p>Max Kostinevich 47</p><p>Building SaaS with Laravel</p><p>this way we will be able to accept user uploads. Then we need to change</p><p>upload field name to avatar. But wait, we do not have such field in our users</p><p>table, so let’s fix this by creating another migration. As you can remember, we</p><p>can do this by following command:</p><p>php artisan make:migration add_avatar_to_users_table --table=</p><p>users</p><p>And add a avatar column to our users table:</p><p>48 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class AddAvatarToUsersTable extends Migration</p><p>{</p><p>/**</p><p>* Run the migrations.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function up()</p><p>{</p><p>Schema::table('users', function (Blueprint $table) {</p><p>$table->string('avatar')->after('email')->nullable</p><p>();</p><p>});</p><p>}</p><p>/**</p><p>* Reverse the migrations.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function down()</p><p>{</p><p>Schema::table('users', function (Blueprint $table) {</p><p>$table->dropColumn('avatar');</p><p>});</p><p>}</p><p>}</p><p>Again, do not forget to use dropColumn on down()method, so avatar column</p><p>will be removed on migration rollback. Then run php artisan migrate to</p><p>run newly created migration.</p><p>Then we need to update update() method in SettingsController as fol-</p><p>lows:</p><p>Max Kostinevich 49</p><p>Building SaaS with Laravel</p><p>class SettingsController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Update user settings.</p><p>*/</p><p>public function update(Request $request)</p><p>{</p><p>$user = auth()->user();</p><p>$request->validate([</p><p>'name' => 'required',</p><p>'email' => 'required|email|unique:users,email,' .</p><p>$user->id,</p><p>'avatar' => 'image|mimes:jpeg,jpg,png,gif|max:1024</p><p>'</p><p>]);</p><p>$user->name = $request->input('name');</p><p>$user->email = $request->input('email');</p><p>$user->company = $request->input('company');</p><p>if ($request->file('avatar')) {</p><p>$avatar = $request->file('avatar')->store('uploads</p><p>', 'public');</p><p>$user->avatar = $avatar;</p><p>}</p><p>$user->save();</p><p>return redirect()</p><p>->back()</p><p>->with('status', 'Your settings have been updated</p><p>successfully.');</p><p>}</p><p>}</p><p>50 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>As you can see, uploading images in Laravel can be donewithin just a few lines</p><p>of code: first, we validate our avatar field andmake sure that uploading file is</p><p>an image. Then, we store this file in our /uploads/ directory.</p><p>Important: Do not forget to run php artisan storage:link to create the</p><p>symbolic link to your storage directory.</p><p>The last thing we need to do - is to show the uploaded avatar on our settings</p><p>form, if no avatar uploaded - let’s show a placeholder (a first letter of user’s</p><p>name):</p><p><div class="u-lg-avatar mr-3"></p><p>@if(auth()->user()->avatar)</p><p><img class="img-fluid rounded-circle border shadow-sm"</p><p>src="{{ url('storage/' . auth()->user()->avatar) }}"></p><p>@else</p><p><span class="btn btn-lg btn-icon text-muted gradient-half-</p><p>primary-v2 rounded-circle border shadow-sm"></p><p><span class="btn-icon__inner">{{ substr(Auth::user()-></p><p>name, 0, 1) }}</span></p><p></span></p><p>@endif</p><p></div></p><p>We also wanted to allow users to delete their avatars, so let’s do this! First,</p><p>let’s create a new deleteAvatar()method in our SettingsController:</p><p>Max Kostinevich 51</p><p>Building SaaS with Laravel</p><p>class SettingsController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Delete user avatar</p><p>*/</p><p>public function deleteAvatar(Request $request)</p><p>{</p><p>$user = auth()->user();</p><p>if ($user->avatar) {</p><p>File::delete('storage/' . $user->avatar);</p><p>$user->avatar = '';</p><p>$user->save();</p><p>}</p><p>return redirect()</p><p>->back()</p><p>->with('status', 'Your avatar has been updated</p><p>successfully.');</p><p>}</p><p>}</p><p>As you can see, the method is pretty simple: we just check if current user have</p><p>an avatar, and if so - we just delete this file and set avatar to empty string.</p><p>Let’s also create a new route to our /routes/web.php file:</p><p>Route::delete('/settings/avatar', '</p><p>SettingsController@deleteAvatar')->name('settings.</p><p>delete_avatar');</p><p>Then we need to create a new form in our settings/edit.blade.php view,</p><p>we can place it just above our main form:</p><p>52 Max Kostinevich</p><p>Building SaaS with Laravel</p><p><form id="delete-avatar" method="post" action="{{ route('</p><p>settings.delete_avatar') }}"></p><p>@csrf</p><p><input type="hidden" name="_method" value="delete"></p><p></form></p><p>But how we actually submit this form? We can do this by adding simple</p><p>javascript to Delete button on our main form:</p><p><button type="button" class="btn btn-sm btn-soft-secondary mb</p><p>-1 mb-sm-0"</p><p>onclick="if(confirm('Delete avatar?')){document.</p><p>getElementById('delete-avatar').submit();return false;}</p><p>"></p><p>Delete</p><p></button></p><p>Great! We almost done with our settings form. The onemore thing we need</p><p>to take care of - is provide an option to user to change the password. Our</p><p>password update form contains 3 fields: current password, new password and</p><p>confirm new password.</p><p>First, we need to update the form itself in our settings/edit.blade.php</p><p>view:</p><p>• Update formmethod, action, add csrf field;</p><p>• Update input name and class attributes;</p><p>• Add type="submit" to Update Password button.</p><p>Thenwe can create a updatePassword()method in our SettingsController</p><p>:</p><p>Max Kostinevich 53</p><p>Building SaaS with Laravel</p><p>class SettingsController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Update user password</p><p>*/</p><p>public function updatePassword(Request $request)</p><p>{</p><p>$user = auth()->user();</p><p>$request->validate([</p><p>'old_password' => ['required', 'required_with:</p><p>password',</p><p>function ($attribute, $value, $fail) use (</p><p>$user) {</p><p>if (!password_verify($value, $user-></p><p>password)) {</p><p>return $fail(__('The current password</p><p>is incorrect.'));</p><p>}</p><p>}</p><p>],</p><p>'password' => 'required|required_with:old_password</p><p>|string|min:6|confirmed',</p><p>]);</p><p>$user->password = bcrypt($request->input('password'));</p><p>$user->save();</p><p>return redirect()</p><p>->back()</p><p>->with('status', 'Your password has been updated</p><p>successfully.');</p><p>}</p><p>}</p><p>First, we check if old_password and password fields are presented. Then we</p><p>54 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>check if old_passwordmatch current password. If so - check if new password</p><p>is entered twice correctly by using confirmed validation rule. If everything is</p><p>ok - update the user password.</p><p>Then we need to add a route to /routes/web.php:</p><p>Route::patch('/settings/password', '</p><p>SettingsController@updatePassword')->name('settings.</p><p>update_password');</p><p>The one last thing we need to do - is to update our header component in</p><p>/resources/views/components/:</p><p>• Add user avatar;</p><p>• And add correct route to settings page;</p><p>Alright, we’re finished settings form and can move on! Let’s proceed with</p><p>making payment forms.</p><p>Payment forms</p><p>Related tag: step-3.3</p><p>Let’s start working on payment forms CRUD (Create/Read/Update/Delete). We</p><p>need to create a payment formmodel, migration and a resource controller.</p><p>We can create all these files using one single command:</p><p>php artisan make:model Form -mcr</p><p>Using the command above, we create a Formmodel, flag m creates a migration</p><p>for this model, flag c creates a controller for this model, and flag r tells artisan</p><p>that we need a resource controller.</p><p>Max Kostinevich 55</p><p>https://laravel.com/docs/6.x/controllers#resource-controllers</p><p>Building SaaS with Laravel</p><p>For each payment formwe need to know the owner of that form,</p><p>sowe need to</p><p>store a user_id, we also need some kind of unique identifier (uid) which will</p><p>be used as a permalink for that particular form. You can see entire database</p><p>schema in “Designing database” chapter. Let’s add all necessary columns to</p><p>our migration file:</p><p>56 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>class CreateFormsTable extends Migration</p><p>{</p><p>/**</p><p>* Run the migrations.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function up()</p><p>{</p><p>Schema::create('forms', function (Blueprint $table) {</p><p>$table->bigIncrements('id');</p><p>$table->bigInteger('user_id')->nullable();</p><p>$table->string('uid')->nullable();</p><p>$table->string('description')->nullable();</p><p>$table->integer('amount')->nullable();</p><p>$table->string('currency')->nullable();</p><p>$table->tinyInteger('is_active')->unsigned()-></p><p>nullable()->default(0);</p><p>$table->timestamps();</p><p>});</p><p>}</p><p>/**</p><p>* Reverse the migrations.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function down()</p><p>{</p><p>Schema::dropIfExists('forms');</p><p>}</p><p>Then we need to add relations to our models, so we’ll be able to get all User</p><p>forms:</p><p>Max Kostinevich 57</p><p>Building SaaS with Laravel</p><p>class User extends Authenticatable implements MustVerifyEmail</p><p>{</p><p>//...</p><p>// User forms</p><p>public function forms()</p><p>{</p><p>return $this->hasMany('App\Form');</p><p>}</p><p>}</p><p>And vice versa - we need to add a user()method in order to be able to get an</p><p>owner for a particularForm</p><p>class Form extends Model</p><p>{</p><p>// Owner of the form</p><p>public function user()</p><p>{</p><p>return $this->belongsTo('App\User');</p><p>}</p><p>}</p><p>Aswe already have placeholders for all CRUDmethods in our FormController,</p><p>we can add all needed routes to /routes/web.php:</p><p>58 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Route::group(</p><p>[</p><p>'middleware' => ['verified'],</p><p>],</p><p>function () {</p><p>//...</p><p>// Forms</p><p>Route::get('/forms', 'FormController@index')->name('</p><p>forms.index');</p><p>Route::get('/forms/create', 'FormController@create')-></p><p>name('forms.create');</p><p>Route::post('/forms/create', 'FormController@store')-></p><p>name('forms.store');</p><p>Route::get('/forms/{uid}', 'FormController@edit')-></p><p>name('forms.edit');</p><p>Route::patch('/forms/{form}', 'FormController@update')</p><p>->name('forms.update');</p><p>Route::delete('/forms/{uid}', 'FormController@destroy'</p><p>)->name('forms.destroy');</p><p>});</p><p>Then let’s prepare our views. First, we need to copy forms.html from</p><p>/resources/views/_HTML/ to /resources/views/forms/ and rename it</p><p>to index.blade.php. Then let’s apply app layout to this view and update</p><p>index()method in our FormController:</p><p>Max Kostinevich 59</p><p>Building SaaS with Laravel</p><p>class FormController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Display a listing of the resource.</p><p>*</p><p>* @return \Illuminate\Http\Response</p><p>*/</p><p>public function index()</p><p>{</p><p>$forms = auth()->user()->forms()->orderBy('id', 'desc'</p><p>)->paginate(25);</p><p>return view('forms.index', compact('forms'));</p><p>}</p><p>}</p><p>We’ll get back to index.blade.php file a little bit later. Meanwhile let’s pre-</p><p>pare form to create and edit our payment forms: copy form-new.html from</p><p>/resources/views/_HTML/ to /resources/views/forms/ and rename it to</p><p>edit.blade.php. Then apply app layout and update formmethod to POST and</p><p>form action to {{ $form->id ? route('forms.update', $form): route(</p><p>'forms.store')}}. Aswe’re going to use the same form for create andupdate</p><p>our payment forms, we can use the following little trick here:</p><p>@if($form->id)</p><p><input type="hidden" name="_method" value="patch"></p><p>@endif</p><p>If $form->id is exists, we use forms.update route and add a patchmethod</p><p>to update existing payment form, otherwise we use storemethod to create a</p><p>new payment form.</p><p>Let’s update store()method in FormController, in our case it will looks as</p><p>60 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>follow:</p><p>class FormController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Store a newly created resource in storage.</p><p>*</p><p>* @param \Illuminate\Http\Request $request</p><p>* @return \Illuminate\Http\Response</p><p>*/</p><p>public function store(Request $request)</p><p>{</p><p>$form = new Form();</p><p>$form->user_id = auth()->user()->id;</p><p>return $this->update($request, $form);</p><p>}</p><p>//...</p><p>}</p><p>In the method above we create a new Form, assign current logged-in user to it</p><p>and then call update()method, which looks as follow:</p><p>Max Kostinevich 61</p><p>Building SaaS with Laravel</p><p>class FormController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Update the specified resource in storage.</p><p>*</p><p>* @param \Illuminate\Http\Request $request</p><p>* @param \App\Form $form</p><p>* @return \Illuminate\Http\Response</p><p>*/</p><p>public function update(Request $request, Form $form)</p><p>{</p><p>$request->validate([</p><p>'description' => 'required',</p><p>'amount' => 'required|numeric|min:1',</p><p>'currency' => 'required',</p><p>]);</p><p>$form->description = $request->input('description');</p><p>$form->amount = (float)str_replace(',', '', $request-></p><p>input('amount')) * 100;</p><p>$form->currency = $request->input('currency');</p><p>$form->is_active = $request->input('is_active');</p><p>$form->uid = $form->uid ?? uniqid();</p><p>$message = $form->id ? 'Payment form has been updated</p><p>successfully' : 'Payment form has been created</p><p>successfully';</p><p>$form->save();</p><p>return redirect()</p><p>->route('forms.edit', $form)</p><p>->with('status', $message);</p><p>}</p><p>//...</p><p>}</p><p>62 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>First, we validate incomingdata, thenweupdateformattributes. If this is a new</p><p>form - we also generating an uid by using uniqid() function. Then we store</p><p>that form and redirect back to form edit view with a successful message.</p><p>As Stripe stores all amounts in cents, we need to multiple input amount</p><p>by 100. So, for example, if payment amount is $100.00, we need to store</p><p>10000 in our database. To show formatted amount to the user, we can add a</p><p>amountFormatted()method to our Formmodel:</p><p>class Form extends Model</p><p>{</p><p>//...</p><p>// Return formatted amount</p><p>public function amountFormatted()</p><p>{</p><p>return number_format($this->amount / 100, 2, '.', ',')</p><p>;</p><p>}</p><p>//...</p><p>}</p><p>Now let’s get back to our forms/index.blade.php view and show the list of</p><p>user’s forms. As we’re already passing user forms to the view via the index()</p><p>method in FormController, we can use $forms variable to show the forms.</p><p>We can use for, foreach, while or forelse statements for this purpose. Let’s</p><p>use forelse statement, and show No records foundmessage if user haven’t</p><p>created any forms yet:</p><p>Max Kostinevich 63</p><p>Building SaaS with Laravel</p><p><tbody class="font-size-1"></p><p>@forelse($forms as $form)</p><p><tr></p><p><td class="align-middle font-weight-normal"></p><p><a href="#" target="_blank" class="d-block text-{{</p><p>$form->is_active ? 'success' : 'muted' }}</p><p>small"></p><p><span class="fas fa-circle small mr-1"></span></p><p>{{ $form->uid }}</a></p><p></td></p><p><td class="align-middle"></p><p><span class="d-block">{{ $form-></p><p>amountFormattedWithCurrency() }}</span></p><p><span class="d-block text-muted small">{{ $form-></p><p>description }}</span></p><p></td></p><p><td class="align-middle"></p><p><span class="d-block">2,390.00 USD</span></p><p><a href="#" class="link-muted small">10 payments</</p><p>a></p><p></td></p><p><td class="align-middle"></p><p><a href="{{ route('forms.edit', $form->uid) }}"</p><p>class="small mr-3"><span class="fas fa-edit"></</p><p>span> Edit</a></p><p><a href="#" class="small text-danger"></span></p><p>Delete</a></p><p></td></p><p></tr></p><p>@empty</p><p><tr></p><p><td colspan="4" class="align-center"></p><p><strong>No records found</strong><br></p><p></td></p><p></tr></p><p>@endforelse</p><p></tbody></p><p>64 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Notice we added a amountFormattedWithCurrency() to Formmodel to show</p><p>the formatted amount with currency:</p><p>class Form extends Model</p><p>{</p><p>//...</p><p>// Return formatted amount with currency</p><p>public function amountFormattedWithCurrency()</p><p>{</p><p>return $this->amountFormatted() . ' ' . strtoupper(</p><p>$this->currency);</p><p>}</p><p>}</p><p>In this function we’re using amountFormatted()method created in previous</p><p>step.</p><p>We’ll add all missing information such as link to the form frontend, total pay-</p><p>ments and transactions amount in the next steps.</p><p>Meanwhile, let’s also add an option to delete the payment form.</p><p>First, let’s update destroy()method in our FormController:</p><p>Max Kostinevich 65</p><p>Building SaaS with Laravel</p><p>class FormController extends Controller</p><p>{</p><p>//...</p><p>/**</p><p>* Remove the specified resource from storage.</p><p>*/</p><p>public function destroy($uid)</p><p>{</p><p>$form = Form::where('id', $uid)</p><p>->orWhere('uid', $uid)</p><p>->firstOrFail();</p><p>if (auth()->user()->id !=</p><p>$form->user_id) {</p><p>return abort(401);</p><p>}</p><p>$form->delete();</p><p>return redirect()</p><p>->route('forms.index')->with('status', 'Payment</p><p>form has been deleted successfully');</p><p>}</p><p>}</p><p>As you can see, this method is pretty simple: first, we’re trying to find a form</p><p>with provided uid, then we’re checking if logged-in user is the owner of that</p><p>form (in other words - if current logged-in user have rights to delete that form).</p><p>If everything is ok - we’re deleting the form and redirecting user back to forms</p><p>.index view with somemessage.</p><p>Then let’s add delete form to our forms.index view:</p><p>66 Max Kostinevich</p><p>Building SaaS with Laravel</p><p><a href="#" class="small text-danger" onclick="if(confirm('</p><p>Delete this record?')){document.getElementById('delete-</p><p>entity-{{ $form->uid }}').submit();return false;}"><span</p><p>class="far fa-trash-alt"></span> Delete</a></p><p><form id="delete-entity-{{ $form->uid }}" action="{{ route('</p><p>forms.destroy', $form->uid) }}" method="POST"></p><p><input type="hidden" name="_method" value="DELETE"></p><p>@csrf</p><p></form></p><p>We’re doing a little trick here: we added a non-visible formwith id property,</p><p>we also added a javascript event handler to Delete link to trigger that form.</p><p>Alright, we almost finished with payment forms! A few things we need to do:</p><p>• Create a custom pagination component, see /resources/views/</p><p>components/pagination.blade.php for more information;</p><p>• Add links to Payment Forms in our header</p><p>A�er that we can proceed with preparing a frontend view for the payment</p><p>form.</p><p>Payment form frontend</p><p>Related tag: step-3.4</p><p>Let’s prepare view for our customer-facing payment form. First, we need to</p><p>copy payment-form.html from /resources/views/_HTML/ to /resources/</p><p>views/payment-form/ and rename it to show.blade.php.</p><p>Then we need to create a PaymentFormController and add a show()method</p><p>to it:</p><p>Max Kostinevich 67</p><p>Building SaaS with Laravel</p><p>class PaymentFormController extends Controller</p><p>{</p><p>// Show the payment form</p><p>public function show($uid)</p><p>{</p><p>$form = Form::where('id', $uid)</p><p>->orWhere('uid', $uid)</p><p>->where('is_active', 1)</p><p>->firstOrFail();</p><p>return view('payment-form.show', compact('form'));</p><p>}</p><p>}</p><p>In show()method we select an active payment form by id or uid and render</p><p>the payment form view we recently created.</p><p>Then we need to add a new route to /routes/web.php:</p><p>// Payment Form</p><p>Route::get('/p/{uid}', 'PaymentFormController@show')->name('</p><p>form.show');</p><p>Let’s get back for a second to our /resources/views/forms/index.blade.</p><p>php and add a link to front-end payment form:</p><p><!--- ~Line:44 --></p><p><td class="align-middle font-weight-normal"></p><p><a href="{{ route('form.show', $form->uid) }}" target="</p><p>_blank" class="d-block text-{{ $form->is_active ? '</p><p>success' : 'muted' }} small"></p><p><span class="fas fa-circle small mr-1"></span></p><p>{{ route('form.show', $form->uid) }}</a></p><p></td></p><p>Then let’s finish our payment form view, we need to update paths to our assets,</p><p>68 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>the same as we did this for our Homepage:</p><p>• Lang attribute in html tag: <html lang="{{ str_replace('_', '-',</p><p>app()->getLocale())}}"></p><p>• Add csrf-token meta tag: <meta name="csrf-token"content="{{</p><p>csrf_token()}}"></p><p>• Update page title tag: <title>{{ config('app.name', 'Laravel'</p><p>)}} - Make a payment</title></p><p>• Update links to favicon, css, js and images:</p><p>– <link rel="shortcut icon"href="{{ asset('favicon.png')</p><p>}}"></p><p>– <link rel="stylesheet"href="{{ asset('css/app.css')}}"</p><p>></p><p>– <script src="{{ asset('js/app.js')}}"></script></p><p>• Update payment form data (user name, avatar, payment amount, pay-</p><p>ment description, etc).</p><p>Great! In next chapter we start working on accepting payments via Stripe.</p><p>Accepting payments</p><p>Related tag: step-3.5</p><p>In this chapter we are going to add payment processing feature to our app.</p><p>First, we need to install stripe-phppackage, to do this just enter the following</p><p>command to your console:</p><p>composer require stripe/stripe-php</p><p>Max Kostinevich 69</p><p>Building SaaS with Laravel</p><p>As we’re going to use Stripe Connect to make payouts, we need to store Stripe</p><p>Account ID for each of our users. Let’s create a newmigration using the follow-</p><p>ing command:</p><p>php artisan make:migration add_stripe_to_users_table --table=</p><p>users</p><p>And then add a new stripe_account_id field, so our migration will look as</p><p>follows:</p><p>70 Max Kostinevich</p><p>https://stripe.com/connect</p><p>Building SaaS with Laravel</p><p>class AddStripeToUsersTable extends Migration</p><p>{</p><p>/**</p><p>* Run the migrations.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function up()</p><p>{</p><p>Schema::table('users', function (Blueprint $table) {</p><p>$table->string('stripe_account_id')->after('email'</p><p>)->nullable();</p><p>});</p><p>}</p><p>/**</p><p>* Reverse the migrations.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function down()</p><p>{</p><p>Schema::table('users', function (Blueprint $table) {</p><p>$table->dropColumn('stripe_account_id');</p><p>});</p><p>}</p><p>}</p><p>Do not forget to run run this migration using php artisan migrate com-</p><p>mand.</p><p>Then, let’s create a new config file to store our Stripe credentials. To do this</p><p>just create a new stripe.php file in /config directory with the following con-</p><p>tent:</p><p>Max Kostinevich 71</p><p>Building SaaS with Laravel</p><p><?php</p><p>// Stripe Settings</p><p>return [</p><p>// Stripe Publishable Key</p><p>'publishable_key' => env('STRIPE_PUBLISHABLE_KEY', ''),</p><p>// Stripe Secret Key</p><p>'secret' => env('STRIPE_SECRET', ''),</p><p>// Stripe Connect Client ID</p><p>'client_id' => env('STRIPE_CLIENT_ID', ''),</p><p>];</p><p>Nowwecanuseconfig()helper function toget thevalueof config variable, for</p><p>example, to get a Stripe Client ID we would call config('stripe.client_id'</p><p>). As all secret keys and passwords should be stored in .env file, we are using</p><p>env() helper function to pass actual value from our .env file.</p><p>To get your Stripe API keys login to your Stripe Dashboard (or create a new</p><p>account if you haven’t registered yet) and go to Developers -> API keys on</p><p>the le� sidebar menu. Please, note: If you want to get API keys for your devel-</p><p>opment (test) application, do not forget to switch View Test Data option as</p><p>shown on the image below:</p><p>To get Stripe Client ID, you need to go to Settings -> Connect settings as</p><p>shown on the image below:</p><p>72 Max Kostinevich</p><p>https://dashboard.stripe.com/</p><p>Building SaaS with Laravel</p><p>And then copy Client ID value:</p><p>Also at this step you may add a URI Redirect to https://YOURAPP/stripe</p><p>/authenticate (do not forget to replace YOURAPP with your app domain</p><p>name/or with your Ngrok subdomain if you’re developing on your local</p><p>machine). This URI will be used to authenticate users via Stripe Connect</p><p>OAuth, so they will be able to accept the payments directly to their Stripe</p><p>account.</p><p>Next, let’s create a Service Provider where we bind our Stripe API object to</p><p>singleton. Service Providers are usually used to instantiate API wrappers,</p><p>Max Kostinevich 73</p><p>https://laravel.com/docs/master/providers</p><p>Building SaaS with Laravel</p><p>packages and other components used in our app..</p><p>For example, let’s say we’re building application which interacts with Shopify</p><p>via API. Instead of instantiating Shopify API Wrapper object each time, we can</p><p>create Shopify Service Provider and instantiate Shopify API wrapper only once,</p><p>it may looks as follows:</p><p><?php</p><p>namespace App\Providers;</p><p>use Illuminate\Support\ServiceProvider;</p><p>//...</p><p>class ShopifyServiceProvider extends ServiceProvider</p><p>{</p><p>/**</p><p>* Register services.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function register()</p><p>{</p><p>$this->app->singleton('ShopifyAPI', function () {</p><p>$api = new ShopifyAPI();</p><p>$api->setApiKey('Shopify API Key');</p><p>$api->setShop('myawesomestore.myshopify.com');</p><p>return $api;</p><p>});</p><p>}</p><p>//...</p><p>}</p><p>Then in our controller we can use this Shopify API object :</p><p>74 Max Kostinevich</p><p>Building SaaS with Laravel</p><p><?php</p><p>namespace App\Http\Controllers;</p><p>//...</p><p>class OrderController extends Controller</p><p>{</p><p>public function index()</p><p>{</p><p>$shopify = resolve('ShopifyAPI');</p><p>$orders = $shopify->getOrders();</p><p>return orders;</p><p>}</p><p>//..</p><p>}</p><p>Visit laracasts.com if you want to learn more about Service Providers and</p><p>Service Containers, Je�rey Way did a great job explaining them.</p><p>Let’s get back to our StripeServiceProvider. To create a new Service</p><p>Provider, run the following command</p><p>in your console:</p><p>php artisan make:provider StripeServiceProvider</p><p>And theneditregister()method inapp/Providers/StripeServiceProvider</p><p>.php:</p><p>Max Kostinevich 75</p><p>https://laracasts.com/</p><p>Building SaaS with Laravel</p><p><?php</p><p>namespace App\Providers;</p><p>//...</p><p>class StripeServiceProvider extends ServiceProvider</p><p>{</p><p>public function register()</p><p>{</p><p>$this->app->singleton(Stripe::class, function () {</p><p>Stripe::setApiKey(config('stripe.secret'));</p><p>Stripe::setClientId(config('stripe.client_id'));</p><p>return new Stripe();</p><p>});</p><p>}</p><p>//...</p><p>}</p><p>Then add \App\Providers\StripeServiceProvider::class to config/app</p><p>.php to register Stripe Service Provider.</p><p>As Stripe APIWrapper uses staticmethods, wedonot need to store instantiated</p><p>object in a variable:</p><p><?php</p><p>//...</p><p>$charge = \Stripe\Charge::create(</p><p>[</p><p>'amount' => 1000,</p><p>'currency' => 'USD',</p><p>]</p><p>);</p><p>Then we need to create routes for Stripe oAuth in our routes/web.php file:</p><p>76 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Route::group(</p><p>[</p><p>'middleware' => ['verified'],</p><p>],</p><p>function () {</p><p>//...</p><p>// Stripe oAuth</p><p>Route::get('/stripe/oauth', '</p><p>StripeOAuthController@oauth')->name('stripe.oauth')</p><p>;</p><p>Route::get('/stripe/authenticate', '</p><p>StripeOAuthController@authenticate')->name('stripe.</p><p>authenticate');</p><p>Route::post('/stripe/deactivate', '</p><p>StripeOAuthController@deactivate')->name('stripe.</p><p>deactivate');</p><p>//...</p><p>});</p><p>Then let’s update our StripeOAuthController.php controller as follows:</p><p>Max Kostinevich 77</p><p>Building SaaS with Laravel</p><p><?php</p><p>namespace App\Http\Controllers;</p><p>use Illuminate\Http\Request;</p><p>use Stripe\Stripe;</p><p>class StripeOAuthController extends Controller</p><p>{</p><p>/**</p><p>* Create a new controller instance.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function __construct(Stripe $stripe)</p><p>{</p><p>$this->middleware('auth');</p><p>}</p><p>public function oauth()</p><p>{</p><p>$url = \Stripe\OAuth::authorizeUrl([</p><p>'scope' => 'read_write',</p><p>]);</p><p>return redirect($url);</p><p>}</p><p>public function authenticate(Request $request)</p><p>{</p><p>$user = auth()->user();</p><p>if ($request->get('code')) {</p><p>// The user has been redirected back from Stripe</p><p>with an authorization code.</p><p>$code = $request->get('code');</p><p>try {</p><p>$response = \Stripe\OAuth::token([</p><p>'grant_type' => 'authorization_code',</p><p>'code' => $code,</p><p>]);</p><p>} catch (\Stripe\Error\OAuth\OAuthBase $e) {</p><p>exit("Error: " . $e->getMessage());</p><p>}</p><p>$user->stripe_account_id = $response-></p><p>stripe_user_id;</p><p>$user->save();</p><p>return redirect()</p><p>->route('settings.edit')</p><p>->with('status', 'Your Stripe Account has been</p><p>added successfully.');</p><p>} elseif ($request->get('error')) {</p><p>// The user was redirect back from the OAuth form</p><p>with an error.</p><p>$error = $request->get('error');</p><p>$error_description = $request->get('</p><p>error_description');</p><p>return redirect()</p><p>->route('settings.edit')</p><p>->withErrors($error . ' : ' .</p><p>$error_description);</p><p>}</p><p>}</p><p>public function deactivate()</p><p>{</p><p>$user = auth()->user();</p><p>try {</p><p>\Stripe\OAuth::deauthorize([</p><p>'stripe_user_id' => $user->stripe_account_id,</p><p>]);</p><p>} catch (\Stripe\Error\OAuth\OAuthBase $e) {</p><p>exit("Error: " . $e->getMessage());</p><p>}</p><p>$user->stripe_account_id = '';</p><p>$user->save();</p><p>return redirect()</p><p>->route('settings.edit')</p><p>->with('status', 'Your Stripe Account has been</p><p>removed successfully.');</p><p>}</p><p>}</p><p>78 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Then let’s get back toourSettingspageat/resources/views/settings/edit</p><p>.blade.php and add Stripe oAuth authorization/deactivation link:</p><p>Max Kostinevich 79</p><p>Building SaaS with Laravel</p><p><!-- Connect Stripe --></p><p><div class="border-bottom mb-3 pb-3"></p><p><div class="card"></p><p><div class="card-body p-5 text-center"></p><p>@if(auth()->user()->stripe_account_id)</p><p><span class="btn btn-icon btn-soft-success text-</p><p>success rounded-circle m-3"></p><p><span class="btn-icon__inner"><span class="fas</p><p>fa-check"></span></span></p><p></span></p><p><span class="d-block text-muted small">Your Stripe</p><p>Account is connected.</span></p><p><a href="#" class="d-block text-danger small"</p><p>onclick="if(confirm('Deactivate Stripe Account</p><p>?')){document.getElementById('deactivate-stripe</p><p>-account').submit();return false;}">Deactivate</p><p></a></p><p><form id="deactivate-stripe-account" method="post"</p><p>action="{{ route('stripe.deactivate') }}"></p><p>@csrf</p><p></form></p><p>@else</p><p><a href="{{ route('stripe.oauth') }}" class="btn</p><p>btn-primary mb-3">Connect Stripe Account</a></p><p><span class="d-block text-muted small">In order to</p><p>get paid, please connect your</p><p><a href="https://stripe.com"</p><p>target="_blank">Stripe</a></p><p>account</p><p></span></p><p>@endif</p><p></div></p><p></div></p><p></div></p><p><!-- End Connect Stripe --></p><p>80 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Then let’s create Payment model andmigration for it by using the following</p><p>command:</p><p>php artisan make:model Payment -m</p><p>Then let’s add all needed columns to our payment migration, so it will looks</p><p>as follow:</p><p>Max Kostinevich 81</p><p>Building SaaS with Laravel</p><p>class CreatePaymentsTable extends Migration</p><p>{</p><p>/**</p><p>* Run the migrations.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function up()</p><p>{</p><p>Schema::create('payments', function (Blueprint $table)</p><p>{</p><p>$table->bigIncrements('id');</p><p>$table->bigInteger('user_id')->nullable();</p><p>$table->bigInteger('form_id')->nullable();</p><p>$table->string('charge_id')->nullable();</p><p>$table->string('customer_name')->nullable();</p><p>$table->string('customer_email')->nullable();</p><p>$table->integer('amount')->nullable();</p><p>$table->integer('application_fee_amount')-></p><p>nullable();</p><p>$table->string('currency')->nullable();</p><p>$table->string('receipt_url')->nullable();</p><p>$table->tinyInteger('is_refunded')->unsigned()-></p><p>nullable()->default(0);</p><p>$table->timestamps();</p><p>});</p><p>}</p><p>/**</p><p>* Reverse the migrations.</p><p>*</p><p>* @return void</p><p>*/</p><p>public function down()</p><p>{</p><p>Schema::dropIfExists('payments');</p><p>}</p><p>}</p><p>82 Max Kostinevich</p><p>Building SaaS with Laravel</p><p>Then add connection to users to Payments model:</p><p>class Payment extends Model</p><p>{</p><p>// Payment receiver</p><p>public function user()</p><p>{</p><p>return $this->belongsTo('App\User');</p><p>}</p><p>}</p><p>Then connect User:</p><p>class User extends Authenticatable implements MustVerifyEmail</p><p>{</p><p>//...</p><p>// Payments</p><p>public function payments()</p><p>{</p><p>return $this->hasMany('App\Payment');</p><p>}</p><p>}</p><p>and add Formmodel connections:</p><p>class Form extends Model</p><p>{</p><p>//...</p><p>public function payments()</p><p>{</p><p>return $this->hasMany('App\Payment');</p><p>}</p><p>}</p><p>Then let’s proceed to handling the payments.</p><p>Let’s update our payment form in /resources/views/payment-form/show.</p><p>Max Kostinevich 83</p><p>Building SaaS with Laravel</p><p>blade.php. We need to make the following changes:</p><p>• Replace payment-form div with form: <form method="post"id="</p><p>payment-form"class="payment-form"></p><p>• Update form input names, make name and email fields required</p><p>• Adddivwherewe’ll displayanyerrormessages: <div id="card-errors</p><p>"class="text-danger small"></div></p><p>• Add hidden field where we’ll store Stripe token: <input type="hidden</p><p>"name="stripeToken"id="stripeToken"value=""></p><p>• Add javascript function which will generate token using Stripe Elements</p><p>and pass data to PaymentFormController via AJAX:</p><p>84 Max Kostinevich</p><p>https://stripe.com/payments/elements</p><p>Building SaaS with Laravel</p><p>// Initialize Stripe object</p><p>var stripe = Stripe('{{ config('stripe.publishable_key') }}');</p><p>// Create Stripe Element</p><p>var elements = stripe.elements();</p><p>var style = {};</p><p>// Attach card number field to Stripe Element</p><p>var cardNumber = elements.create('cardNumber', {</p><p>'placeholder': '0000 0000 0000 0000',</p><p>'style': style</p><p>});</p><p>cardNumber.mount('#card-number');</p><p>// Attach expiration date field to Stripe Element</p><p>var expDate = elements.create('cardExpiry', {</p><p>'placeholder': 'DD/YY',</p><p>'style': style</p><p>});</p><p>expDate.mount('#card-expiration');</p><p>// Attach CVC field to Stripe Element</p><p>var cardCVC = elements.create('cardCvc', {</p><p>'placeholder': 'CVC',</p><p>'style': style</p><p>});</p><p>cardCVC.mount('#card-cvc');</p><p>// Handle form submission</p><p>$('#payment-form').on('submit', function (e) {</p><p>e.preventDefault();</p><p>// Clear error message</p><p>$('#card-errors').html('');</p><p>// Generate Stripe Token</p><p>stripe.createToken(cardNumber).then(function(result) {</p><p>if (result.error) {</p><p>// Show Card error message</p><p>$('#card-errors').html(result.error.message);</p><p>} else {</p><p>stripeTokenHandler(result.token);</p><p>}</p><p>});</p><p>});</p><p>// Send Payment data with Tokent to Payment Form Controller</p><p>function stripeTokenHandler(token) {</p><p>// Update Stripe Token ID</p>