Categories


Archives


Recent Posts


Categories


Understanding Laravel Spark’s Swap

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

This entry is part 1 of 1 in the series Laravel Spark. This is the first post in the series.
  • 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:

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 swapped 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 swapping 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 swapped 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.

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 22nd June 2017