A Closer Look at the Innards of Spark

Leo Sjöberg • May 14, 2016

The Series – Table of Contents

An Overview

Introduction to Spark
Spark Up What You Have

The Backend

The Spark Philosophy
A Closer Look at the Innards of Spark
Making Spark Your Own


Laravel Spark comes with a lot of features built in, but how does swap, plan, or the automatic API tokens work, that enable seamless communication between your frontend and your backend.

Swap

Alright, so let's start with something fairly easy, swap, the Spark method that allows you to swap out methods in Spark, and is the preferred way to customising the backend as opposed to rebinding entire repositories in the container.

So first of all, the way swap is primarily used is by specifying a class@method signature, and a closure to call instead. For example:

1<?php
2 
3Spark::swap('UserRepository@create', function (array $data) {
4 $user = User::create($data);
5 
6 event (new UserWasCreated($user));
7 
8 return $user;
9});

What we did above was swap out the default method to create a user, and replaced it with one that would not only create the user, but also emit an event. Now, the question is of course, how does it work? How does Spark know to call this method, and could we make our code cleaner if we wish to swap several methods on a repository?

1<?php
2 
3public static function swap($interaction, $callback)
4{
5 static::$interactions[$interaction] = $callback;
6}

Above is all the code of the swap method. So, just looking at that code, we can see that it doesn't really do much at all; it just adds your custom callback to an array, where the key is the specified class@method declaration. So the only reason swap really works is because most calls in spark are made with the Spark::call method, an alias for spark::interact...

Interact

In the trait Laravel\Spark\Configuration\CallsInteractions we find a static method called interact, which takes two arguments, an $interaction, and an array of $parameters.

So the first thing that happens in the method is that there's a check for whether a method name is specified, and if there isn't, Spark assumes you want to call a handle method on the class. This means Spark::call(Subscribe::class) is equivalent to Spark::call(Subscribe::class.'@handle'). Do note that this class name should be something that can be resolved out of the container, meaning you either need to manually bind it to something specific (not recommended), or specify the fully qualified class name (recommended). This also means that if there is a contract defined rather than a class, you can call that rather than the concrete implementation (that's actually how the UserRepository is called, it's the interface rather than the class itself).

After the first check for a method name, the next thing that happens is that the static::$interactions array is checked for the provided class@method. Do note that at this stage, Spark will check for the fully qualified class name, so in the case of the UserRepository@create, it's checking if you've specified a Spark::swap('Laravel\Spark\Contracts\Repositories\UserRepository@create') first of all. Now, if Spark is unable to find that, it will check for the class basename, e.g simply UserRepository@create. This is why you are able to define just the name of the interface/class, rather than the fully qualified one.

Now, if Spark finds a swapped method, it does something quite interesting; it calls the interact method if you specified a string as the callback. More specifically, this is the function that's called:

1<?php
2 
3protected static function callSwappedInteraction($interaction, array $parameters)
4{
5 if (is_string(static::$interactions[$interaction])) {
6 return static::interact(static::$interactions[$interaction], $parameters);
7 }
8 
9 return call_user_func_array(static::$interactions[$interaction], $parameters);
10}

Now, what this means for you as a developer is that you don't have to define a closure, you can also specify a class@method string, or even an interaction class name. For example, all following three examples are valid uses of swap:

1<?php
2 
3Spark::swap('UserRepository@create', function (array $data) {
4 return User::create($data);
5});
1<?php
2 
3Spark::swap('UserRepository@create', CustomUserRepository::class.'@create');
1<?php
2 
3Spark::swap('UserRepository@create', CreateUser::class);

In the last example, CreateUser would need to have a handle method accepting an array as its argument.

Of course, you can also use Spark::call throughout your own application if you wish, but you will see limited benefit unless you intend to keep some part of the codebase unmodified (such as Spark does).

Automagic API Tokens

Alright, so this is an interesting one, because at first, it sort of feels like magic. If you use Vue for your frontend, you can call this.$http.get('/api/your/endpoint'), and all other vue-resource methods, to a route defined in your api routes, and it will just work, without having to generate an API token for the user. This is one of the most interesting concepts to me, as it is something you might be interested in implementing in other, non-Spark applications, or if you wish to replace the Vue backend with another framework like React or Angular.

So the way this works can be fairly confusing to grasp, because there are a lot of calls made and at some point, you might feel like it's just a method calling a method calling a method calling a method calling a...

It all starts in spark/resources/assets/js/spark.js, the root Spark application. In the created method, we find the following code:

1<?php
2 
3if (Spark.userId && Spark.usesApi) {
4 this.refreshApiTokenEveryFewMinutes();
5}

What really matters in refreshApiTokenEveryFewMinutes() is the call to, you guessed it, refreshApiToken(). Now, this is where it gets interesting, because all the function does is:

1<?php
2 
3refreshApiToken() {
4 this.lastRefreshedApiTokenAt = moment();
5 
6 this.$http.put('/spark/token')
7 .then(response => {
8 console.log('API Token Refreshed.');
9 });
10}

It just makes a PUT request to the backend. What this means is that if you wish to implement this feature yourself in e.g Angular or React, you can simply make a similar setup; just make a call to your own refreshApiToken() every few minutes (Spark does it every 4 minutes, and also checks every 5 seconds if it's been more than 5 minutes since the last refresh, in which case it will refresh it). What happens in the backend is fairly straightforward; the route just leads to a simple controller method:

1<?php
2 
3public function refresh(Request $request)
4{
5 $this->tokens->deleteExpiredTokens($request->user());
6 
7 return response('Refreshed.')->withCookie(
8 $this->tokens->createTokenCookie($request->user())
9 );
10}

This method in turn uses the Laravel\Spark\Repositories\TokenRepository@createTokenCookie, which will just return a cookie named spark_token with a JSON Web Token valid for 5 minutes on the current domain over HTTP only:

1<?php
2 
3public function createTokenCookie($user)
4{
5 $token = JWT::encode([
6 'sub' => $user->id,
7 'xsrf' => csrf_token(),
8 'expiry' => Carbon::now()->addMinutes(5)->getTimestamp(),
9 ]);
10 
11 return cookie(
12 'spark_token', $token, 5, null,
13 config('session.domain'), config('session.secure'), true
14 );
15}

And that's all the magic there's to it. The backend simply returns a cookie, and this is something you wouldn't even have to customise. If you want this feature in a framework other than Vue, then simply make a PUT request to /spark/token every few minutes, and you're covered!

For the backend to then verify this, the auth:api middleware is used, which just tells Laravel to use the api guard, defined in our app/config/auth.php. In that file you'll find that it simply specifies 'driver' => 'spark'. Now, the driver itself is registered in Laravel\Spark\Providers\SparkServiceProvider (this is not the same as App\Providers\SparkServiceProvider) with a simple

1<?php
2 
3Auth::viaRequest('spark', function ($request) {
4 return app(TokenGuard::class)->user($request);
5});

The TokenGuard does, among other things, check in the cookies if there exists a spark_token cookie, and will then extract the token from there, which is used to create a new transient token to be used by the application.

Creating API tokens programatically

But what if you wish to use your Spark application's API for an external application, but still controlled by you, such as a mobile application? Certainly, you wouldn't want to make all your users go into the settings panel and find the API tab. Heck, most of your users might not even know what an API is, or even have access to a web settings panel! So how can we then create a token to be used by our mobile app? Well, there are essentially three options: 1. Change all API auth to use a more suitable authentication scheme such as OAuth 2.0, 2. Generate Spark API tokens programatically, 3. Have OAuth tokens for your other applications, while still using Spark's tokens for your web frontend and to allow third-party applications API access.

Which one you go for is largely up to you and how you intend to consume the API, and I will leave that researching up to you, but suffice it to say that this is one of those times where you should really think about what your options are, and which one will be easiest, and which one will be most future-proof, and make an informed decision with those two metrics in mind. In this section, however, I will not go through how to set up OAuth (because there are already plenty of guides and good packages for that), but I will go through how to generate Spark API tokens programatically, rather than from the settings panel (I'll also go through how to remove the API section of the settings panel if you wish for API tokens to only be internal, and some things to keep in mind if you do decide to allow your users to have direct API access).

Generating an API token is really quite easy, and I believe the best way to get the hang of how it's done is simply going through how Spark does it.

side note: these tokens are permanent until deleted unlike the automatically generated ones

The first thing to note is that Spark specifies a POST route to create an API token:

1<?php
2 
3$router->post('/settings/api/token', 'Settings\API\TokenController@store');

Now, worth noting with this, and all other spark routes, is that it is in the web middleware. This means that if you wish to access it from another application, you will need to pass through a CSRF token, and you will somehow also need to place the user into the session... Now, if you're not aware, APIs aren't great dealing with sessions. You could of course make the user provide a username and password, pass that to the backend, log the user in and then create a token... However, it's probably a lot cleaner to just do it on registration.

Do note that if you wish to generate API tokens at another point, you will have to authenticate the user somehow, as tokens are user-bound. This can be done either by passing the username and password (please use https), or by passing a user id and some application-wide secret token you have specified, so that you can somehow authenticate the user in the backend.

If we take a quick glance at the controller method, we'll see it doesn't do anything very magical:

1<?php
2 
3public function store(CreateTokenRequest $request)
4{
5 $data = count(Spark::tokensCan()) > 0 ? ['abilities' => $request->abilities] : [];
6 
7 return response()->json(['token' => $this->tokens->createToken(
8 $request->user(), $request->name, $data
9 )->token]);
10}

The reference to $this->tokens is merely the TokenRepository, which you are free to use whenever you want. As such, you could simply copy and paste the above code snippet, making sure to dependency inject or otherwise set a protected $tokens on the class to access the TokenRepository.

Summary

This post has focused a lot on interactions and the API, and the reason for that is simply that those are the things that stand out most to me in Spark. However, if you'd like to see something else added, do get in touch with me and I'll do my best to add it to this post, or write a new one, depending on the extent of the topic.