Laravel

This guide targets Laravel 10 or later on PHP 8.2 or later. The avsbhq/avsb package is a plain PHP SDK with a small Laravel layer: a route middleware that puts an evaluation context on every request, and a facade for static access. By the end you will resolve AvsbServer from the container, read typed flags in controllers, and record conversions.

1

Install

Add the avsbhq/avsb package with Composer.

2

Add your SDK key

Open your A vs B project and click Environments in the sidebar. Copy the SDK key for the environment this deployment talks to, then put it in .env.

3

Create config/avsb.php

The package ships no publishable config stub, so write the file yourself. It is four lines.

4

Register the SDK in the container

Bind AvsbServer as a singleton in a small provider of your own. One instance per request holds the datafile and batches that request's events.

5

Register the route middleware

The shipped middleware builds an EvalContext from the authenticated user (or the session id) and sets it on the request as avsb_context.

6

Read a flag in a controller

Every read takes the context as its third argument and returns a Flag object, never a bare value.

7

Track a conversion

Call track() with the same context. Exposures are automatic, so experiment results appear without any extra call.

8

Test without the network

Build a datafile in the test and construct a server from it. No network, no cache, no events.

Install the package:

Shell
composer require avsbhq/avsb
Shell1 line
Publishing in progress

Version 1.0.1 of avsbhq/avsb is on its way to Packagist with this release. Until it arrives there, composer require reports that it cannot find the package.

Open your A vs B project and click Environments in the sidebar. It sits on its own there, next to Settings, not inside it. Each environment card shows a masked SDK key with Reveal and Copy buttons.

  1. Environments lives in the sidebar on its own, not inside Settings.
  2. Click Reveal to see the full key, then Copy to copy it.

Add your SDK key to .env:

Shell
AVSB_SDK_KEY=sdk_production_ttqm0eaj4vth1krcb2xn
Shell1 line
Your SDK key is public
Your SDK key is a public identifier, not a secret: it is safe to ship in browser and mobile bundles, it can only fetch that environment's flag configuration and send events, and it can never read or change anything in your dashboard. Credentials covers all four A vs B credentials and which one to reach for.

An SDK key is sdk_, then the environment name, then a generated id. The SDK checks that shape when you construct it, so a pasted dashboard URL or a truncated copy is reported at boot instead of turning into a 404 later.

Create config/avsb.php. There is no vendor:publish provider to run: the package ships no config stub, so this file is yours.

PHP
<?phpreturn [    'sdk_key' => env('AVSB_SDK_KEY', ''),    // Optional. Point at a datafile another process keeps fresh (the A vs B    // agent shape). When the file exists it wins and the SDK never fetches.    'datafile_path' => env('AVSB_DATAFILE_PATH', ''),];
PHP9 lines

Bind the SDK in a provider of your own, app/Providers/AvsbServiceProvider.php:

PHP
<?phpnamespace App\Providers;use Avsbhq\Avsb\AvsbServer;use Avsbhq\Avsb\AvsbServerOptions;use Illuminate\Support\ServiceProvider;class AvsbServiceProvider extends ServiceProvider{    public function register(): void    {        $this->app->singleton(AvsbServer::class, function (): AvsbServer {            $sdkKey = (string) config('avsb.sdk_key', '');            $path = (string) config('avsb.datafile_path', '');            if ($path !== '' && is_file($path)) {                $datafile = json_decode(                    (string) file_get_contents($path),                    true,                    512,                    JSON_THROW_ON_ERROR                );                return new AvsbServer($sdkKey, new AvsbServerOptions(datafile: $datafile));            }            // Constructing with an unusable key still resolves: the SDK logs            // why and every flag returns your default, rather than the            // container throwing during boot.            return new AvsbServer($sdkKey);        });    }}
PHP34 lines

Register it the way your Laravel version does. On Laravel 11 and later:

PHP
// bootstrap/providers.phpreturn [    App\Providers\AppServiceProvider::class,    App\Providers\AvsbServiceProvider::class,];
PHP5 lines

On Laravel 10, add the same class to the providers array in config/app.php.

Do not register the package's own ServiceProvider

Avsbhq\Avsb\Middleware\Laravel\ServiceProvider performs the same container binding as the provider above, but it is deliberately framework-free: it does not extend Illuminate\Support\ServiceProvider, so the package never takes a hard dependency on Laravel. Laravel cannot boot a class from the providers array that is not a framework service provider, so list your own wrapper instead.

Register the route middleware. It reads the authenticated user id, falls back to the session id, promotes any avsb_* request parameters to targeting attributes, and sets the result on the request as avsb_context. On Laravel 11 and later:

PHP
// bootstrap/app.phpuse Illuminate\Foundation\Configuration\Middleware;->withMiddleware(function (Middleware $middleware) {    $middleware->alias([        'avsb' => \Avsbhq\Avsb\Middleware\Laravel\Middleware::class,    ]);})
PHP8 lines

On Laravel 10, add the same entry to $middlewareAliases in app/Http/Kernel.php:

PHP
protected $middlewareAliases = [    // ... existing aliases ...    'avsb' => \Avsbhq\Avsb\Middleware\Laravel\Middleware::class,];
PHP4 lines

Then apply it to the routes that need flags:

PHP
Route::middleware('avsb')->get('/checkout', [CheckoutController::class, 'show']);
PHP1 line

Read a flag in a controller. AvsbServer comes from the container, and the context comes off the request:

PHP
<?phpnamespace App\Http\Controllers;use Avsbhq\Avsb\AvsbServer;use Avsbhq\Avsb\EvalContext;use Illuminate\Http\JsonResponse;use Illuminate\Http\Request;class CheckoutController extends Controller{    public function __construct(private readonly AvsbServer $avsb) {}    public function show(Request $request): JsonResponse    {        // Set by the 'avsb' route middleware. Build one yourself on routes        // that do not run it.        $context = $request->attributes->get('avsb_context')            ?? EvalContext::user((string) $request->user()?->getAuthIdentifier());        $checkout = $this->avsb->getBoolFlag('checkout_v2', false, $context);        $theme = $this->avsb->getStringFlag('ui_theme', 'default', $context);        return response()->json([            'showNewCheckout' => $checkout->value,            'variation' => $checkout->variationKey,            'theme' => $theme->value,        ]);    }}
PHP30 lines

The context is not optional in practice. Every caller who omits it buckets into the same variation, which makes experiment data meaningless, so the SDK warns once per process and names the call to add. Build one directly when you need attributes the middleware does not carry:

PHP
use Avsbhq\Avsb\EvalContext;$context = EvalContext::user((string) $user->id, [    'plan' => $user->plan,          // targeting attributes, any JSON value    'betaTester' => true,]);// Any single kind$org = EvalContext::user('acme-corp', ['tier' => 'enterprise'], 'organization');// Several kinds at once$context = EvalContext::multi([    'user' => ['key' => (string) $user->id, 'plan' => $user->plan],    'organization' => ['key' => $user->org_key, 'tier' => 'enterprise'],]);
PHP15 lines

What a read gives you back

Every getter returns a Flag. All of its properties are public and readonly, so there are no getters to remember:

PHP
$flag = $this->avsb->getBoolFlag('checkout_v2', false, $context);$flag->value;          // mixed             the evaluated value, your type$flag->variationKey;   // string|null       the served variation's key; null only for not_found and not_ready$flag->source;         // EvaluationSource  why this value, a backed enum$flag->ruleId;         // string|null       the rule or holdout that decided$flag->reasons;        // list<string>      the decision trail, in order$flag->isEnabled();    // bool              a real decision AND a truthy value$flag->exists();       // bool              false for not_found and not_ready
PHP9 lines

The four getters are getBoolFlag, getStringFlag, getNumberFlag, and getJsonFlag. Each takes (string $key, $defaultValue, ?EvalContext $context = null) and each requires the default. You get that default back when the flag is missing, when the flag is off, or when no datafile ever loaded.

The getters do not coerce and do not throw. $flag->value is whatever the variation holds, so calling getBoolFlag on a flag whose variations hold strings gives you the string. Read $flag->value when you want the value and $flag->isEnabled() when you want a gate.

Optional: the Avsb facade

The package ships a facade for static access. It needs the instance handed to it once, which the boot() method of your provider is the right place for:

PHP
use Avsbhq\Avsb\AvsbServer;use Avsbhq\Avsb\Middleware\Laravel\Facade as AvsbFacade;public function boot(): void{    AvsbFacade::setInstance($this->app->make(AvsbServer::class));}
PHP7 lines

Alias it in the aliases array of config/app.php:

PHP
'aliases' => [    // ... existing aliases ...    'Avsb' => Avsbhq\Avsb\Middleware\Laravel\Facade::class,],
PHP4 lines

Calls forward to the same instance, so the context argument is still required:

PHP
$flag = Avsb::getBoolFlag('checkout_v2', false, $context);
PHP1 line

Tracking conversions

PHP
// AvsbServer::track(string $eventName, EvalContext $context, ?float $revenue = null, ?float $value = null): void$this->avsb->track('signup_completed', $context);$this->avsb->track('checkout_completed', $context, revenue: 49.99);$this->avsb->track('items_per_order', $context, value: 3.0);
PHP4 lines

$eventName is the metric key from your dashboard. An unknown key is stored, so a typo shows up as an empty metric rather than an error. $revenue is money in decimal major units. $value is the number for average-value metrics.

Purchases are typed and send immediately rather than waiting for a batch:

PHP
use Avsbhq\Avsb\PurchaseOrder;$this->avsb->trackPurchase($context, new PurchaseOrder(    orderId: 'ORD-1001',    total: 99.95,    currency: 'USD',    items: [['sku' => 'SKU-1', 'price' => 99.95, 'quantity' => 1]],));
PHP8 lines

Exposures are automatic. Every A/B test, bandit, and holdout decision records one, so nothing extra is needed to make experiment results appear.

Flushing events

Queued events are sent once, after the response, from a PHP shutdown function that the SDK registers on the first queued event. A request that reads no flags adds no shutdown work at all, so there is nothing to wire up.

PHP
$this->avsb->flushEvents();       // send now instead of waiting for shutdown$this->avsb->pendingEventCount(); // events queued and not yet sent
PHP2 lines
Long-lived workers

The PHP SDK has no background refresh thread. It loads the datafile when you construct it, through a cache in the system temp directory with a 60 second time to live, which suits PHP-FPM where each request builds a server and reads a warm file. A process that lives for hours (Laravel Octane, a long-running queue worker) keeps the datafile it loaded at construction. Under Octane, bind with $this->app->scoped(...) instead of singleton(...) so each request gets a fresh instance, or call replaceDatafile() on a schedule you own.

Testing

Build a datafile in the test and construct a server from it, then swap it into the container. InMemoryDatafileLoader returns a server with events and heartbeats switched off, so a suite never touches the network by accident.

PHP
<?phpnamespace Tests\Feature;use Avsbhq\Avsb\AvsbServer;use Avsbhq\Avsb\TestUtils\InMemoryDatafileLoader;use Tests\TestCase;final class CheckoutTest extends TestCase{    private function serverWithCheckoutOn(): AvsbServer    {        return InMemoryDatafileLoader::fromJson(<<<'JSON'        {          "version": 2,          "sdkKey": "sdk_production_xxxxxxxxxxxxxxxx",          "flags": [{            "id": "flag_checkout",            "key": "checkout_v2",            "type": "boolean",            "enabled": true,            "defaultVariationId": "var_off",            "variations": [              {"id": "var_off", "key": "off", "value": false},              {"id": "var_on",  "key": "on",  "value": true}            ],            "overrides": [],            "rules": [{              "id": "rule_all",              "type": "targeted_delivery",              "enabled": true,              "audienceIds": [],              "hashAttribute": "user.key",              "trafficAllocation": null,              "variations": [{"variationId": "var_on", "percentage": 1}]            }]          }],          "audiences": []        }        JSON);    }    public function test_it_serves_the_new_checkout_when_the_rule_matches(): void    {        $this->app->instance(AvsbServer::class, $this->serverWithCheckoutOn());        $this->getJson('/checkout')->assertJson(['showNewCheckout' => true]);    }    public function test_it_serves_the_default_when_the_flag_is_unknown(): void    {        $server = $this->serverWithCheckoutOn();        $flag = $server->getBoolFlag('no_such_flag', false, \Avsbhq\Avsb\EvalContext::user('alice'));        self::assertFalse($flag->value);        self::assertSame('not_found', $flag->source->value);        self::assertFalse($flag->exists());    }}
PHP60 lines

InMemoryDatafileLoader::fromArray, fromJson, and fromFile all return a ready server. loadArray returns the raw datafile array when you want to mutate a fixture before handing it over.

What's next

Was this helpful?