At Spatie we recently launched a new site: guidelines.spatie.be. It contains articles on how we go about setting things up at Spatie and a collection of styleguides. The source code of the site is available on GitHub. In this blogpost I'd like to share why and how we created our guidelines site.

Why did we create this?

Let's start with a quote from Sandi Metz from her excellent post on why we argue about style:

I firmly believe that all of the code that I personally have to examine should come to me in a consistent format. Code is read many more times than it is written, which means that the ultimate cost of code is in its reading. It therefore follows that code should be optimized for readability, which in turn dictates that an application's code should all follow the same style. Adhering to a common style saves you money.

Spatie is web agency. So most of the time we are working on multiple projects at the same time. When the initial project is done, more of than not, we will also maintain it for several years. To keep things manageable we try to code things in a consistent way. That way we can dive into a project and easily reason about it. I'd argue that in addition to adhering to a common code style, setting up your projects in a consistent way will save time and money too.

For years I've been the sole developer at Spatie. It was seemingly easy to keep things consistent because there was nobody else working on the code. But the truth is that at the time I certainly did not notice the little differences in my code style across the projects (PSR-2 wasn't a thing back then). Now that the company is growing (Alex recently joined our team with more new members on the way) it's getting more and more important to keep things consistent.

Like Sandi mentioned in the above quote code should be optimized for readability. Having all code in a common style will save you money because it takes less time to understand the code. But I'd argue having a style guide will save you time writing code as well. Trying to decide if the view names or route names should be camelCases or kebab-cased? Don't waste your time and just check the relevant guidelines.

Most of these rules originate from a discussion of the entire team (or they've been decided by an individual who cared enough to put it in a rule). Having these rules written down avoids discussing them over and over again. Because "best practices" really are nothing more than "current practices" you should still, from time to time, re-evaluate the rules to see if they still make sense.

Behind the screens

The guidelines site itself is build with Laravel. It's completely open source. Here's the repo containing the code that's actually deployed on our server.

We've set up the guidelines site in such a way that everybody can contribute it. At the bottom of every page there's an Edit this page on GitHub link. Clicking it will create a PR against the repo. When we merge in the PR the code will automatically get deployed (we use Forge's auto deploy feature for this).

Because the guidelines are our very opinionated thoughts on how to do things, we're not going to just accept every single PR we get. You can 't change our minds just by PRing a new rule. But we do welcome new rules that we agree with, or a discussion why our existing rules could be bad. And of course we're also grateful for PRs that fix typos.

Storing the content

The content itself is stored as a set of markdown files. To display every page (expect for the home page), the PageController is used:

namespace App\Http\Controllers;

use App\Markdown\MarkdownConverter;

class PageController extends Controller
{
    public function __invoke($url)
    {
        abort_unless($page = app('navigation')->getPage($url), 404);

        return view('page', [
            'title' => $page->title,
            'editUrl' => $page->edit_url,
            'contents' => MarkdownConverter::convert($page->contents),
        ]);
    }
}

Making content private

The aforementioned PageController controller will ask the Navigation class which page should be displayed. When looking at the code of the Navigation class you'll notice that it also tries to get content from a private directory. This private directory contains content that we don't want to share publicly, such as semi sensitive information and custom procedures that are not interesting for the outside world.

The content of that private directory is not in our public repo, but in a separate private repo that's being symlink so Navigation can read it. And of course that private content is only viewable by users who are logged in.

Logging in

At Spatie we use Google Apps and every team member has his or her own email-address on the spatie.be domain. Wouldn't it be nice if anyone with a spatie.be address on Google would be able to login? Then we wouldn't have to create credentials manually whenever we onboard a new team member.

That's why we opted leveraging Laravel Socialite to handle logins. Socialite makes it very easy to authenticate with OAuth providers such as Google, Facebook, Github, ... Installing Socialite is very easy. After it is installed and configured the necessary APIs at Google you don't need too much to code to handle social logins.

This is the entire LoginController:

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\User;
use Socialite;

class LoginController extends Controller
{
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    public function redirectToProvider()
    {
        return Socialite::driver('google')
            ->scopes(['email'])
            ->redirect();
    }

    public function handleProviderCallback()
    {
        $user = Socialite::driver('google')->stateless()->user();

        abort_unless(ends_with($user->getEmail(), '@spatie.be'), 403);

        $authenticatableUser = User::findOrCreate($user);

        auth()->login($authenticatableUser, true);

        return redirect('/');
    }

    public function logout()
    {
        auth()->logout();

        return redirect('/');
    }
}

When visiting /login the app will execute the redirectToProvider method. The user will get redirected to a login page at Google. When users provide the right credentials Google will redirect them back to our site. That response from Google is handled in handleProviderCallback. There we abort unless the email-address of logged in user ends with code>@spatie.be. And then there's a piece of boring code. Because a socialite user isn't directly authenticatable we find (or create) a regular user in the database and log that one in.

In closing

If you're working in a company that doesn't follow any guidelines to keep code and configuration consistent, I highly recommend talking to your team and convincing them of the value of writing such guidelines down. Take the time to discuss and agree on some common rules. I bet nearly everybody on your team will find such guidelines helpful and it'll make it easy for newcomers to your team.

You could opt to just follow our guidelines or create your own set of rules. If you choose the latter, maybe you could use the code of our guidelines site to set up your own.

The site and the writing down of the guidelines was largely spearheaded my colleague Sebastian, who, as always, did an excellent job.

That site isn't the first thing we've open sourced. Take a look at the opensource pages on our company site to find out the Laravel, PHP and JavaScript packages we previously created.