rasuvaeff/yii3-ab-testing

Deterministic A/B testing for Yii3 applications

Maintainers

Package info

github.com/rasuvaeff/yii3-ab-testing

pkg:composer/rasuvaeff/yii3-ab-testing

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-06-10 03:47 UTC

This package is auto-updated.

Last update: 2026-06-10 06:30:53 UTC


README

Stable Version Total Downloads Build Static Analysis Psalm Level PHP License

Deterministic A/B testing for Yii3 applications. Stateless assignment, weighted variants, forced variant for QA, explicit exposure/conversion tracking.

Using an AI coding assistant? llms.txt has a compact API reference you can pass as context.

Requirements

  • PHP 8.3+

Installation

composer require rasuvaeff/yii3-ab-testing

Usage

Configure experiments

use Rasuvaeff\Yii3AbTesting\ConfigExperimentProvider;
use Rasuvaeff\Yii3AbTesting\AbTesting;
use Rasuvaeff\Yii3AbTesting\WeightedHashAssignmentStrategy;

$provider = new ConfigExperimentProvider(config: [
    'checkout-button' => [
        'enabled' => true,
        'salt' => 'checkout-v1',
        'fallbackVariant' => 'control',
        'variants' => ['control' => 50, 'green' => 50],
    ],
]);

$ab = new AbTesting(
    provider: $provider,
    strategy: new WeightedHashAssignmentStrategy(),
);

Experiment definitions come from an ExperimentProvider. ConfigExperimentProvider reads a static array; a storage backend (e.g. yii3-ab-testing-db) supplies a database-backed provider so experiments can be toggled at runtime without a deploy.

Assign variant

$assignment = $ab->assign(experiment: 'checkout-button', subjectId: (string) $userId);

if ($assignment->isVariant('green')) {
    // Show green button.
}

// Quick check:
if ($ab->is(experiment: 'checkout-button', variant: 'green', subjectId: (string) $userId)) {
    // Variant-specific logic.
}

Forced variant (QA)

$assignment = $ab->assign(
    experiment: 'checkout-button',
    subjectId: (string) $userId,
    forcedVariant: 'green',
);

Track exposure and conversion

// assign() does NOT auto-track. Call explicitly:
$ab->trackExposure($assignment);

// On conversion event:
$ab->trackConversion($assignment, goal: 'purchase');

Assignment context (optional)

Pass an AssignmentContext to attribute metrics by environment/segment. It is carried into the returned Assignment (so trackers can read it) but does not change which variant is selected — variant selection stays deterministic.

use Rasuvaeff\Yii3AbTesting\AssignmentContext;

$context = AssignmentContext::forEnvironment('production')
    ->withAttribute('country', 'DE');

$assignment = $ab->assign(
    experiment: 'checkout-button',
    subjectId: (string) $userId,
    context: $context,
);

$assignment->context?->getEnvironment(); // 'production'

Yii3 integration

Package provides config/params.php and config/di.php via config-plugin. Override in your application:

// config/params.php
return [
    'rasuvaeff/yii3-ab-testing' => [
        'experiments' => [
            'checkout-button' => [
                'enabled' => true,
                'salt' => 'checkout-v1',
                'fallbackVariant' => 'control',
                'variants' => ['control' => 50, 'green' => 50],
            ],
        ],
    ],
];

The core wires only the AbTesting facade and the default WeightedHashAssignmentStrategy. It does not bind ExperimentProvider (the experiment source) nor ExposureTracker / ConversionTracker (the event sinks) — those keys are owned by exactly one source each, so installing a storage/tracker backend wires them with no Duplicate key conflict.

Experiment source (required)

AbTesting needs an ExperimentProvider. Without a storage backend, bind ConfigExperimentProvider once in your app config (config/common/di/*.php), reading the experiments params above:

use Rasuvaeff\Yii3AbTesting\ConfigExperimentProvider;
use Rasuvaeff\Yii3AbTesting\ExperimentProvider;

/** @var array $params */

return [
    ExperimentProvider::class => [
        'class' => ConfigExperimentProvider::class,
        '__construct()' => [
            'config' => $params['rasuvaeff/yii3-ab-testing']['experiments'],
        ],
    ],
];

Installing yii3-ab-testing-db binds ExperimentProvider for you (database-backed, runtime-editable) — drop the manual binding then. Bind it from a single source: a backend plus a manual binding reintroduces the yiisoft/config Duplicate key conflict.

Tracking backends (optional)

To persist exposures/conversions, opt in by binding the tracker interface to a real implementation — either from a dedicated adapter package or once in your own app config (config/common/di/*.php):

use Rasuvaeff\Yii3AbTesting\ExposureTracker;
use Rasuvaeff\Yii3AbTesting\ConversionTracker;

return [
    ExposureTracker::class => MyExposureTracker::class,
    ConversionTracker::class => MyConversionTracker::class,
];

Bind each interface from a single source. Installing two adapters that both bind ExposureTracker (or a backend plus a manual binding) reintroduces a yiisoft/config Duplicate key conflict — pick one, or compose them with your own fan-out tracker bound in the application.

Assignment algorithm

digest = sha256(salt + ':' + subjectId)   // 64-char hex
hash   = hexdec(digest[0:8])             // 32-bit unsigned
bucket = hash % totalWeight

Variants sorted by key. Cumulative weight boundaries determine assignment.

Guarantees

  • Same salt + subjectId → same variant, forever.
  • Changing salt = full re-assignment (intentional reset).
  • Changing weights/variants shifts bucket boundaries (partial re-assignment).
  • To freeze a cohort, create new experiment with new salt.

Security

  • Experiment/variant names validated: /^[a-z][a-z0-9_-]*$/.
  • Forced variant must pass allow-list. Unknown variant throws exception.
  • No PII stored. Trackers are developer-controlled.
  • assign()/is() are pure — no side effects.

Examples

See examples/ for complete usage scenarios.

Development

make install       # composer install
make build         # full gate (validate + cs + psalm + test)
make cs-fix        # fix code style
make psalm         # static analysis
make test          # run phpunit
make test-coverage  # run coverage
make mutation       # mutation testing
make release-check  # build + rector + bc-check + mutation

make test-coverage and make mutation bootstrap pcov inside the composer:2 container because the base image has no coverage driver.

License

BSD-3-Clause. See LICENSE.md.