★ Selling digital products using Laravel part 10: Miscellaneous interesting tidbits + outro

Written by / Original link on Oct. 13, 2020

We've already covered a lot of ground in this series. Let's finish by highlighting some miscellaneous interesting tidbits.

This post is a part of a series where we explore the source code of which you'll find in this repo on GitHub
  • Part 10: Miscellaneous interesting tidbits + outro (you are here)

Importing blogpost using RSS #

Let's take a look at our homepage. Several of my colleagues and I write regularly on our blogs. Using the RSS feeds on our blogs, we import the last written post and display it right on the homepage of


Those RSS feeds are imported by a Laminas Framework package: laminas-feed. All feed entries are imported in the local database in the insights table.

Here's the code of the ImportInsightsCommand command that does the import. It's pretty straightforward.

namespace App\Console\Commands;

use App<span class="hljs-title">Models<span class="hljs-title">Insight; use Carbon<span class="hljs-title">Carbon; use Illuminate<span class="hljs-title">Console<span class="hljs-title">Command; use Laminas<span class="hljs-title">Feed<span class="hljs-title">Exception<span class="hljs-title">ExceptionInterface; use Laminas<span class="hljs-title">Feed<span class="hljs-title">Reader<span class="hljs-title">Entry<span class="hljs-title">AbstractEntry; use Laminas<span class="hljs-title">Feed<span class="hljs-title">Reader<span class="hljs-title">Reader; use Laminas<span class="hljs-title">Http<span class="hljs-title">Client<span class="hljs-title">Adapter<span class="hljs-title">Exception<span class="hljs-title">TimeoutException;

class ImportInsightsCommand extends Command { protected $signature = 'import:insights';

<span class="hljs-keyword">protected</span> $description = <span class="hljs-string">'Import the blog posts of team members.'</span>;

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handle</span><span class="hljs-params">()</span>
    <span class="hljs-keyword">$this</span>-&gt;info(<span class="hljs-string">'Syncing insights from RSS feeds...'</span>);

    collect(config(<span class="hljs-string">'services.rss'</span>))
        -&gt;each(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">(string $feedUrl)</span>: <span class="hljs-title">void</span> </span>{
            <span class="hljs-keyword">try</span> {
                $feed = Reader::import($feedUrl);

                <span class="hljs-keyword">foreach</span> ($feed <span class="hljs-keyword">as</span> $entry) {
                    $insight = Insight::updateOrCreate([
                        <span class="hljs-string">'url'</span> =&gt; $entry-&gt;getLink(),
                    ], [
                        <span class="hljs-string">'title'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;sanitizeTitle($entry-&gt;getTitle()),
                        <span class="hljs-string">'created_at'</span> =&gt; <span class="hljs-keyword">new</span> Carbon($entry-&gt;getDateModified()-&gt;format(DATE_ATOM)),
                        <span class="hljs-string">'url'</span> =&gt; $entry-&gt;getLink(),
                        <span class="hljs-string">'website'</span> =&gt; <span class="hljs-keyword">$this</span>-&gt;getWebsite($entry),

                    <span class="hljs-keyword">$this</span>-&gt;info(<span class="hljs-string">"Imported `{$insight-&gt;title}`"</span>);
            } <span class="hljs-keyword">catch</span> (ExceptionInterface | TimeoutException $exception) {
                <span class="hljs-keyword">$this</span>-&gt;error(<span class="hljs-string">"Could not load {$feedUrl}"</span>);

<span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sanitizeTitle</span><span class="hljs-params">(string $title)</span>: <span class="hljs-title">string</span>
    $title = ltrim($title, <span class="hljs-string">'&acirc;&#152;&#133; '</span>);

    $title = htmlspecialchars_decode($title, ENT_QUOTES);

    <span class="hljs-keyword">return</span> $title;

<span class="hljs-keyword">protected</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getWebsite</span><span class="hljs-params">(AbstractEntry $entry)</span>: <span class="hljs-title">string</span>
    $host = parse_url($entry-&gt;getLink(), PHP_URL_HOST);

    $host = ltrim($host, <span class="hljs-string">'www.'</span>);

    <span class="hljs-keyword">return</span> $host;


Running tests via GitHub actions #

I'll be honest with you. We should have more tests for Currently, we only test the action classes and some of the pages. I feel that we now mainly test the happy paths of our code. We will add unhappy path tests soon.

We do run the current suite of tests on each commit via GitHub Actions. You'll find the workflow here. To know more about our testing workflow on GitHub actions, read this blog post I wrote earlier this year.

Automatically fixing code formatting issues #

To format our code, we mostly follow PSR-12. In the repo there's a GitHub workflow that will be executed each time a commit is pushed.

The workflow will run FriendsOfPHP/PHP-CS-Fixer on GitHub actions using the config file included in the repo. The GitHub workflow will commit all code style fixes in this step. To know more about the workflow, read this excellent post by Stefan Zweifel.

Deploying to a server #

Our site is hosted on a Forge provisioned Digital Ocean server. To deploy our site to the server, we use the vastly underrated Envoy.

Using Envoy, small tasks can be defined that should be performed on a local or remote server. You can find the Envoy script we use in this Blade file.

In short, the deploy task will perform a near-zero-downtime deploy. It creates a new release folder on the server and builds the new release by cloning the repo, running Composer and Yarn, and a few things more. After the application is fully prepared, we'll symlink that directory as the current one.

The only downside of the deploy task is that it can be quite slow. We've also added a deploy-code task that just pulls the GitHub repo in the current directory and clears the cache. This task completes much faster and is quite handy if you want only to change a small piece of PHP code.

We've been using the deploy and deploy-code tasks for a few years now on all Spatie projects. They are very robust. In this old blog post, I highlight a few interesting tidbits happening under the hood.

Monitoring #

To monitor our site, we use two services that I've helped building: Oh Dear and Flare.

Oh Dear is an uptime and performance monitoring service. It will notify us via Slack whenever our site is down. It can also measure performance. As you can see in this screenshot, we are more or less always seeing a response time of around 200ms.


Additionally, Oh Dear will also crawl our site and notify us when there is a broken link or mixed content. This is quite nice because we know for sure that our users will not see any 404s when navigating the site.


A final thing that Oh Dear does is keeping an eye on the scheduled tasks. We get a notification whenever a scheduled task did not complete successfully at the expected time or didn't run at all.

To sync the schedule of the Laravel app to Oh Dear we use our homegrown spatie/laravel-schedule-monitor package.

You can see that package in action in this video:

To monitor exceptions occurring in our production environment, we use Flare. This homegrown service is specifically built for Laravel apps. Like most of our other products, we ensured that the UI is very straightforward to use and polished.


In closing #

Congratulations! You've made it to the end. There sure is a lot going on at If you like what you've read in this post, consider sponsoring on GitHub, or purchasing one of our paid products. When you do that, you'll see all the things explained above in action.

I'd also like to emphasize that I did not create this website alone. During the past summer, my colleagues Rias and Alex did a lot of the development. I think they did a fantastic job. Like always, my colleague Willem made everything look beautiful.

Thanks for reading!


« Dorothy Hodgkin - ★ Selling digital products using Laravel part 9: Serving ads on GitHub »