- Understanding Laravel Spark’s Swap
As Pulse Storm (the small boutique software consultancy I started and continue to operate) takes a half step back from ecommerce and shifts back into software systems consulting, I’ve found myself doing a lot of initial application prototyping/MVPs for both established businesses as well as less-technical entrepreneurs with a need for software.
Laravel remains the best tool I’ve used for this sort of work. Most recently, I’ve been working on a project that started in Laravel Spark. Laravel Spark is a commercial variation on Laravel that includes a bit more application boilerplate, as well as some Stripe integrations for subscription plans.
Laravel Spark also features a few programming abstractions not available to regular Laravel users. Specifically, Spark::swap
allows you swap out implementations of specific core Spark methods with your own callbacks. While it’s a powerful feature, the current docs give it short shrift.
Today we’re going to explore the Spark::swap
method and its counterpart, Spark::interact
. As Laravel Spark is commercial software, some of the following code may not be open source. I’m operating under the assumption (and with good intent) that my use below falls under fair-use. If you believe otherwise and have a vested interest (Hi Taylor!) please let me know.
The Interact Method
Before we can get to swap
, we’ll need to put ourselves in the shoes of a Laravel Spark team engineer, or a third party systems engineer using Spark to build an extensible system. At some point, we’ll have some code that looks like this
$object = new \Some\Class\File;
$result = $object->someMethod($param1, $param2, $param3);
That is, we’ll instantiate an object from a class (\Some\Class\File
), and call its someMethod
method with some parameters. As a system engineer, we may decide that Some\Class\File::someMethod
is something that a system user (i.e. a client programmer) may want to replace with their own implementation. When that’s the case, we’ll replace the above with the following code
$result = Spark::interact('\Some\Class\File@someMethod`, $param1, $param2, $param3);
The static Spark::interact
method will instantiate a new instance of \Some\Class\File
for us, and then call its someMethod
method. We could also have done the following
$result = Spark::interact('\Some\Class\File`, $param1, $param2, $param3);
With the above code, we’ve omitted @someMethod
. When we use interact
with a class name, but no method, behind the scenes Laravel Spark will run code similar to the following
//simplified -- behind the scenes laravel actually uses
//app('Some\Class\File') to instantiate the object.
$object = new \Some\Class\File;
$result = $object->handle($param1, $param2, $param3);
That is, if we omit a method, Laravel Spark will use a method named handle
as a default. This, in turn, means we can do the following
$result = Spark::interact(\Some\Class\File::class, $param1, $param2, $param3);
That is, instead of using a hard coded string constant, we can use the magic static ::class
property to pass in the class name.
Whatever style we use, the point is this: Spark::interact
instantiates a class, and calls its method for us. Using interact
doesn’t do anything extra for us. However, by using interact
we’re giving developers the ability to replace the method implementation.
Swapping Methods
Alright — we can stop thinking like a systems developer, and get back to being a plain old Laravel Spark programmer. Let’s say we wanted to change how Laravel Spark assigns the default role to a user. This is done in the following method
#File: spark/src/Interactions/Settings/Teams/AddTeamMember.php
public function handle($team, $user, $role = null)
{
$team->users()->attach($user, ['role' => $role ?: Spark::defaultRole()]);
event(new TeamMemberAdded($team, $user));
}
More importantly though, Laravel’s code calls this method via interact
here
#File: spark/src/Interactions/Auth/Register.php
use Laravel\Spark\Contracts\Interactions\Settings\Teams\AddTeamMember
/* ... */
Spark::interact(AddTeamMember::class, [$invitation->team, $user]);
Because the Laravel Spark system engineers call this method via interact
, we (as Laravel Spark client programmers) can use swap
to drop in a replacement implementation. Specifically, if we add the following code to the Laravel Spark service provider (which the installer should have generated for you automatically) we’ll replace the core method with our own function.
#File: app/Providers/SparkServiceProvider.php
use Laravel\Spark\Contracts\Interactions\Settings\Teams\AddTeamMember
/* ... */
public function booted()
{
/* ... */
Spark::swap(
AddTeamMember::class . '@handle',
function($team, $user, $role = null){
//our replacement method here
}
);
/* ... */
}
/* ... */
That is — swap
takes two arguments. The first is the name of the class and method we want to replace as a string. You’ll need to include the full @handle
here — unlike interact
, swap
doesn’t automatically recognize the handle
method as a default.
The second argument is an anonymous PHP function, (sometimes called a closure or callback). This function should have the same arguments as the method you’re replacing.
With the above in place, Laravel Spark will call our callback instead of the AddTeamMember@handle
function. The Spark::swap
method will let you replace any method that a core Spark class calls via Spark::interact
.
Benefits of the Interact/Swap Pattern
There’s a few benefits this interact/swap
pattern brings to the table over traditional monkey-patching or a configurable classes approach. They all boil down to simplicity.
For client programmers, instead of introducing a new configuration approach, interact/swap
allows the programmer to write some simple PHP code that replaces system functionality.
For the systems programmers, instead of dealing with a situation where junior programmers could replace any of their methods, they can specifically say
Hey, this method here is the one you want to use
Additionally — by creating a new system to do this, less junior developers are still free to use traditional code reuse techniques — i.e. there’s still public
and protected
(not private
) methods to use if we need them for something more subtle/advanced.
The Downsides
Like any programming choice, the interact/swap
model presents some tradeoffs.
The cons:
- Client programmers are responsible for maintaining existing functionality
- There’s no way to call the original method
- There’s no way to access the original object’s state
We’re going to take a closer look at some of these. We’re doing this not to bring Laravel core engineers to their knees and force them to apologize for their crimes against programming one particular software engineering style popular in corporate environments. We’re doing this so you can understand the trade-offs and make the choices that are right for your particular project at this particular point in time.
The first two problems are related. Let’s take a look at our previous example
#File: spark/src/Interactions/Settings/Teams/AddTeamMember.php
public function handle($team, $user, $role = null)
{
$team->users()->attach($user, ['role' => $role ?: Spark::defaultRole()]);
event(new TeamMemberAdded($team, $user));
}
The stock AddTeamMember@handle
method does two things — it adds a user’s default role, and issues a TeamMemberAdded
event. If the replacement method fails to call that event
Spark::swap(
AddTeamMember::class . '@handle',
function($team, $user, $role = null){
$team->users()->attach($user, ['role' => 'someOtherRole']);
}
);
This means the event will not fire, and the system will not call listeners. It’s up to each client programmer to make sure their swap
ped in function does anything the original method did (unless, of course, the point of the swapping is they want that behavior to stop). It also means a developer will need to revisit each of their swaps
after any Laravel Spark upgrades to make sure there’s no new behavior for them to duplicate.
Compare this with a configurable class approach, such as Magento 1’s class rewrites or the many (Laravel’s included) dependency injection container systems.
class My_New_Class extends Old_Class
{
protected function someFunction($one, $two, $three)
{
$result = parent::someFunction($one, $two, $three);
//my new stuff;
return $result;
}
}
With these systems it’s possible (via parent::someFunction
) to call the original parent method and preserve the original system’s behavior.
The other tradeoff with swap/interact
is — you don’t have access to the original object’s (or class’s) state. Consider the configureTeamForNewUser
method. Laravel Spark’s engineers have exposed this method to swap
ping via interact
#File: spark/src/Interactions/Auth/Register.php
Spark::interact(self::class.'@configureTeamForNewUser', [$request, $user]);
However, if we look at the configureTeamForNewUser
method
#File: spark/src/Interactions/Auth/Register.php
public function configureTeamForNewUser(RegisterRequest $request, $user)
{
if ($invitation = $request->invitation()) {
Spark::interact(AddTeamMember::class, [$invitation->team, $user]);
self::$team = $invitation->team;
$invitation->delete();
} elseif (Spark::onlyTeamPlans()) {
self::$team = Spark::interact(CreateTeam::class, [
$user, ['name' => $request->team, 'slug' => $request->team_slug]
]);
}
$user->currentTeam();
}
we see there’s a few points where this method assigns values to its class state.
self::$team = $invitation->team;
We’re responsible for doing everything this method does in our swap
ped method, but we won’t have access to self
in our callback. You might think it’s mostly safe to use a hard coded class instead of self
, except $team
is a private variable
#File: spark/src/Interactions/Auth/Register.php
/**
* The team created at registration.
*
* @var \Laravel\Spark\Team
*/
private static $team;
This basically means that, despite Laravel Spark’s core engineers exposing configureTeamForNewUser
via interact
— we can’t safely use it. Failure to assign a value to the private $team
means other system functionality will not behave correctly.
Wrap Up
There’s no one size fits all approach to systems programming, and there’s no one size fits all approach to using programatic systems built by other people. The best we can hope for is to understand how and why the systems we’re using are built. If we understand this, we’ll be in a much better position to make our own decisions about tradeoffs, as well as respond quickly to the eventual systems changes that break our code.