Normally A/B split testing is about smaller layout adaptions or simple feature changes. But in some cases a complete rewrite of a feature is necessary and should be compared to the old solution. The normal workflow in our team is to create a feature cookie to determine if the user should recieve the A or the B version. For example if the cookie exists the user will recieve version A and otherwise version B. Some sort of randomise mechanism is used to set the required cookie for a certain percentage of users.

But if you have extensive differences between the two version implementations there are a lot of conditions in your backend and template code to be able to serve version A or version B. To avoid this I built a simple router macro to determine if the required cookie is set and bind different controllers to the same route.

The route macro is defined in the AppServiceProvider class inside of the boot function.

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Router::macro('hasFeatureFlag', function($name) {
            $featureCookie = Cookie::get($name);

            if (!is_null($featureCookie)) {
                return optional($this);
            }

            return optional(null);
        });
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register() {}
}

The macro uses the optional helper of Laravel to either return the router instance or null. The Optional implementation ensures that a method call on the Optional instance does not throw an error if the instance is null.

In your routes file you then can define two route definitions with the same path:

// routes.php

Route::get('/feature', 'NormalController@index');
Route::hasFeatureFlag('awesome-feature')
    ->get('/feature', 'FeatureController@index');

It is important to notice, that the order of the route definitions has to be in the shown order because the routes get applied in reverse order.

For demonstration purposes I implemented the two controllers with a cookie toogle, this means that the NormalController sets the cookie and the FeatureController unsets it. This results in an alternating execution of the two controllers.

class NormalController extends Controller
{
    public function index()
    {
        Cookie::queue(Cookie::make('awesome-feature', 1, 60));

        return "this is the normal version";
    }
}
class FeatureController extends Controller
{
    public function index()
    {
        Cookie::queue(Cookie::forget('awesome-fofeatureo'));

        return "this is the feature controller";
    }
}

Conclusion

This solution allows me to clearly seperate the two implementations into two controllers and help me to keep the code easy and clean. In general Laravel macros are an awesome way to extend the framework facades with custom conditions and functionality.

Resources