rasuvaeff / yii3-ab-testing
Deterministic A/B testing for Yii3 applications
Requires
- php: ^8.3
Requires (Dev)
- ergebnis/composer-normalize: ^2.51
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.29
- maglnet/composer-require-checker: ^4.17
- phpunit/phpunit: ^11.5
- rector/rector: ^2.4
- roave/backward-compatibility-check: ^8.0
- vimeo/psalm: ^6.16
This package is auto-updated.
Last update: 2026-06-10 06:30:53 UTC
README
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.