assertchris.io

Gitstore

Published on 2018-10-31

If you’re been following me on Twitter, you’ve probably seen a bunch of tweets about this thing I’ve been working on; called Gitstore. I’ve grown fond of describing it as a way to sell code that doesn’t entirely suck.

Along with a couple friends, I’ve been building Gitstore to make it easy for maintainers to sell and distribute their code. I won’t go into detail, now, except to say that I am still excited to be working on it. I can’t think of a single project I’ve worked on as much as this, without making a cent, and still been happy…

I bring it up because there are a few things I’m doing, in the codebase, which I haven’t done before. Approaches to design and architecture which are new to me, and may be useful for you.

This post contains strong Laravel. Parental guidance is advised.

Responders vs. Controllers

I’m not the first person to suggest any of this stuff, least of all responders. The R in ADR is this exact thing. Each of my routes are defined like this:

Route::get('/subscriptions', Subscriptions\ShowSubscriptions::class)
    ->name('subscriptions.show');

This is from routes/web.php

Each responder handles a single combination of request method and URI. These classes have an __invoke method, and can define other functions they need to use to get the job done:

namespace App\Http\Controllers\Users;

use App\Http\Controllers\Controller;
use GuzzleHttp\Client;
use Socialite;

class ReauthenticateGithub extends Controller
{
    public function __invoke()
    {
        $user = auth()->user();

        $this->revokeGithubToken($user);
        $this->removeGithubDetails($user);

        request()->session()->put('is_seller', true);
        request()->session()->put('redirect_to_github_sync', true);

        return $this->redirectWithNewPermissions();
    }

    private function revokeGithubToken($user)
    {
        $token = $user->github_token;

        $clientId = config('services.github.client_id');
        $clientSecret = config('services.github.client_secret');

        $client = new Client();

        $client->request('DELETE', "https://api.github.com/applications/{$clientId}/tokens/{$token}", [
            'auth' => [$clientId, $clientSecret],
        ]);
    }

    private function removeGithubDetails($user)
    {
        $user->github_id = null;
        $user->github_handle = null;
        $user->github_token = null;
        $user->github_authed_at = null;
        $user->save();
    }

    private function redirectWithNewPermissions()
    {
        return Socialite::driver('github')
            ->scopes(['repo'])
            ->redirect();
    }
}

This is from app/Http/Controllers/Users/ReauthenticateGithub.php

There’s definitely some improvement to be made to this responder, now that I’m looking at it again. I can inject the Illuminate\Http\Request object, so I don’t need to use request() or auth(). I can inject the Socialite provider, so I don’t need to use the facade.

Still, I think this illustrates how much smaller intricate sets of functionality can be packaged.

Components and Includes

When Shawn join the team, we started to design the UI. Until that point, all the UI was whatever worked at the time I built it. This meant we needed to rebuild lots of front-end markup.

We started to take a component-based approach to design. I found it surprisingly difficult to decide which sub-views should be includes and which should be components. Finally, I settled on a criteria. I ask whether the thing I’m building will be a) reused to build another component, and b) has content that is a natural focus.

Here’s an example of the first:

<div class="flex ml-4 md:ml-0 mt-4 items-center">
    {{ $slot }}
</div>

This is from resources/views/new/components/actions.blade.php

This component holds the buttons beneath each form. Any actions specific to the current page are usually in this area. They look like this:

Here’s an example of the second:

<span class="flex bg-indigo-light text-white text-sm rounded-full px-3 py-1">
    {{ $slot }}
</span>

This is from resources/views/new/components/bubble.blade.php

We often show things like subscription prices (and other meta data), using little round bubbles. They look like this:

If the view doesn’t easily fit into these two categories, I tend to make it an include. It probably needs more than a single piece of data to work. It probably doesn’t need to be reused.

Declarative front-end

Javascript constitutes 0.04% of the codebase. It’s virtually non-existent, here. When I do need it, I follow the kind of approach I used to use with MooTools:

When I want something to happen, on the front-end, I trigger it with custom data attributes in the markup sent from the server. I’m also using a light build of jQuery, so the behaviours look like this:

$("[data-submit-sibling]").on("click", function(e) {
    e.preventDefault();

    $(this)
        .siblings("form")
        .submit();
});

This is from resources/js/behaviors/submit-sibling.js

I use this behaviour to submit the forms which allow method spoofing for DELETE requests. I should probably add a Blade directive to make those forms, but I haven’t got to that yet.

I stitch these behaviours together, in the main Javascript file:

const axios = require("axios");

// ...usual laravel stuff

require("./behaviors/allow-payment");
require("./behaviors/disable-link-on-click");
require("./behaviors/dismiss-status");
require("./behaviors/submit-sibling");
require("./behaviors/show-plan-form");
require("./behaviors/show-subscriber-chart");
require("./behaviors/toggle-header-menu");

This is from resources/js/app.js

I can’t believe I’ve been able to get away with so little Javascript in a commerce app. 257 lines…

Well, that’s all I have to show you today.