PHPackages                             rasuvaeff/yii3-ab-testing - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. rasuvaeff/yii3-ab-testing

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

rasuvaeff/yii3-ab-testing
=========================

Deterministic A/B testing for Yii3 applications

v1.4.2(3w ago)04784BSD-3-ClausePHPPHP 8.3 - 8.5CI passing

Since Jun 10Pushed 5d agoCompare

[ Source](https://github.com/rasuvaeff/yii3-ab-testing)[ Packagist](https://packagist.org/packages/rasuvaeff/yii3-ab-testing)[ Docs](https://github.com/rasuvaeff/yii3-ab-testing)[ RSS](/packages/rasuvaeff-yii3-ab-testing/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependencies (19)Versions (9)Used By (4)

rasuvaeff/yii3-ab-testing
=========================

[](#rasuvaeffyii3-ab-testing)

[![Stable Version](https://camo.githubusercontent.com/46e32400057d7b5b6c512cf8fa88ac355bdc8f3e1a276801e1c69454bbd36d38/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7261737576616566662f796969332d61622d74657374696e672e737667)](https://packagist.org/packages/rasuvaeff/yii3-ab-testing)[![Total Downloads](https://camo.githubusercontent.com/227c3e3c3e4371fd9a300c3541b7c02f05c69def35305e222210ea6b67b172d0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7261737576616566662f796969332d61622d74657374696e672e737667)](https://packagist.org/packages/rasuvaeff/yii3-ab-testing)[![Build](https://camo.githubusercontent.com/d975973d7b1c300ad75fc844f6d87eb935196e153234c9d3824bdc53df54732f/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7261737576616566662f796969332d61622d74657374696e672f6275696c642e796d6c3f6272616e63683d6d6173746572)](https://github.com/rasuvaeff/yii3-ab-testing/actions)[![Static Analysis](https://camo.githubusercontent.com/aeba8bdb819931602afb2e377729740f12acb35f6eb4e10e1c38554465ec6a0c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7261737576616566662f796969332d61622d74657374696e672f7374617469632d616e616c797369732e796d6c3f6272616e63683d6d6173746572)](https://github.com/rasuvaeff/yii3-ab-testing/actions)[![Psalm Level](https://camo.githubusercontent.com/79b06940f44b19109016852842a33ff5f3e9bf760cc0d73d4d8cc4e3bfe82a85/68747470733a2f2f73686570686572642e6465762f6769746875622f7261737576616566662f796969332d61622d74657374696e672f6c6576656c2e737667)](https://shepherd.dev/github/rasuvaeff/yii3-ab-testing)[![PHP](https://camo.githubusercontent.com/88d3beb4b2a0ffd836dff60407e3662b15e8a8cf7230093e93147ba71bd27f08/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f7261737576616566662f796969332d61622d74657374696e672f706870)](https://packagist.org/packages/rasuvaeff/yii3-ab-testing)[![License](https://camo.githubusercontent.com/cf4865a8c01a79114dba32695cbeb4cab4e6eabe3e77dd1ab0284c937fdad1ed/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f7261737576616566662f796969332d61622d74657374696e672e737667)](https://github.com/rasuvaeff/yii3-ab-testing/blob/master/LICENSE.md)[Русская версия](README.ru.md)

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](llms.txt) has a compact API reference you can pass as context.

Requirements
------------

[](#requirements)

- PHP 8.3+ (64-bit — the hash bucket exceeds `PHP_INT_MAX` on 32-bit builds)

Installation
------------

[](#installation)

```
composer require rasuvaeff/yii3-ab-testing
```

Usage
-----

[](#usage)

### Configure experiments

[](#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

[](#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.
}
```

Assigning an experiment that is not defined throws `Exception\InvalidExperimentException`; forcing a variant the experiment does not have throws `Exception\InvalidVariantException`. The loaded experiment set is inspectable via `$ab->getRegistry()` — an `ExperimentRegistry` with `get()`, `has()`, `all()` and `reset()`. The registry is lazy: the `ExperimentProvider` is queried on first access and memoized afterwards.

### Forced variant (QA)

[](#forced-variant-qa)

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

### Track exposure and conversion

[](#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)

[](#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

[](#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)

[](#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)

[](#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,
];
```

Two ready-made sinks ship in core: `LoggerExposureTracker` / `LoggerConversionTracker` write each event as one structured PSR-3 log record (zero infrastructure, log level configurable). Like every tracker they are not bound by core `config/di.php` (one-source rule) — bind them in your app config:

```
use Psr\Log\LoggerInterface;
use Rasuvaeff\Yii3AbTesting\ExposureTracker;
use Rasuvaeff\Yii3AbTesting\LoggerExposureTracker;

return [
    ExposureTracker::class => static fn (LoggerInterface $logger): ExposureTracker
        => new LoggerExposureTracker($logger),
];
```

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 the built-in `CompositeExposureTracker` / `CompositeConversionTracker`, bound once in your own app config:

```
use Rasuvaeff\Yii3AbTesting\CompositeExposureTracker;
use Rasuvaeff\Yii3AbTesting\ExposureTracker;

return [
    ExposureTracker::class => static fn (): ExposureTracker => new CompositeExposureTracker(
        new ClickHouseExposureTracker(/* ... */),
        new LoggerExposureTracker(/* ... */),
    ),
];
```

Trackers that buffer events (e.g. the ClickHouse adapter) implement `FlushableTracker`; call `flush()` once at request end. The composite trackers implement it too and propagate the flush to every flushable inner tracker, so the application can flush through the bound tracker interface:

```
use Rasuvaeff\Yii3AbTesting\FlushableTracker;

if ($tracker instanceof FlushableTracker) {
    $tracker->flush();
}
```

### Targeting (optional)

[](#targeting-optional)

Restrict an experiment to a subset of subjects by attaching a `TargetingRule`. Subjects that don't match receive the fallback variant with `isFallback === true`and `isTargetingMismatch === true`. `forcedVariant` bypasses targeting.

```
use Rasuvaeff\Yii3AbTesting\AndTargetingRule;
use Rasuvaeff\Yii3AbTesting\AttributeTargetingRule;
use Rasuvaeff\Yii3AbTesting\EnvironmentTargetingRule;
use Rasuvaeff\Yii3AbTesting\Experiment;
use Rasuvaeff\Yii3AbTesting\OrTargetingRule;

$experiment = new Experiment(
    name: 'checkout',
    enabled: true,
    salt: 'checkout-v1',
    fallbackVariant: 'control',
    variants: ['control' => 50, 'green' => 50],
    targeting: new AndTargetingRule(rules: [
        new EnvironmentTargetingRule(environments: ['production']),
        new AttributeTargetingRule(attribute: 'plan', value: 'pro'),
    ]),
);

$assignment = $abTesting->assign(
    experiment: 'checkout',
    subjectId: $userId,
    context: new AssignmentContext(environment: 'production', attributes: ['plan' => 'pro']),
);

if ($assignment->isTargetingMismatch) {
    // subject not in target segment — received fallback
}
```

Available built-in rules:

ClassMatches when`EnvironmentTargetingRule``context->getEnvironment()` is in the given list`AttributeTargetingRule``context->getAttribute($name) === $value` (strict)`AndTargetingRule`all nested rules match (short-circuit)`OrTargetingRule`at least one nested rule matches (short-circuit)### Sticky variants (optional)

[](#sticky-variants-optional)

Deterministic assignment keeps a subject in the same variant only while weights are stable; changing weights or the variant set shifts bucket boundaries and reshuffles subjects. To pin a subject to a variant across such changes, persist the assignment through an `AssignmentStore`:

```
interface AssignmentStore {
    public function get(string $experiment, string $subjectId): ?string;
    public function put(string $experiment, string $subjectId, string $variant): void;
}
```

`AbTesting::assign()` stays pure — sticky resolution is a separate layer. Cookie/session implementations and a `SubjectIdMiddleware` for stable anonymous identity ship in `yii3-ab-testing-web`. An assignment served from a store carries `isSticky = true` so trackers can tell it apart from a fresh deterministic one.

### Worker runtimes (RoadRunner, Swoole)

[](#worker-runtimes-roadrunner-swoole)

The experiment set is memoized per `ExperimentRegistry` instance. In a long-running worker the `AbTesting` service survives across requests, so the core's `config/di.php` registers a `reset` hook for `yiisoft/di`'s `StateResetter`: runtimes that reset container state between requests re-read the `ExperimentProvider` on the next request, and a kill switch flipped in the source takes effect without a worker restart. In classic PHP-FPM nothing changes — the service is rebuilt per request anyway.

Assignment algorithm
--------------------

[](#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

[](#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
--------

[](#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
--------

[](#examples)

See [examples/](examples/) for complete usage scenarios.

Development
-----------

[](#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 testo
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
-------

[](#license)

BSD-3-Clause. See [LICENSE.md](LICENSE.md).

###  Health Score

48

—

FairBetter than 94% of packages

Maintenance97

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community13

Small or concentrated contributor base

Maturity56

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

7

Last Release

26d ago

PHP version history (2 changes)v1.0.0PHP ^8.3

v1.2.0PHP 8.3 - 8.5

### Community

Maintainers

![](https://www.gravatar.com/avatar/b0812d5572a7041dfe36e222d295b2e6dc55833a605350fcde58a51a5965ed30?d=identicon)[rasuvaeff](/maintainers/rasuvaeff)

---

Top Contributors

[![rasuvaeff](https://avatars.githubusercontent.com/u/1352718?v=4)](https://github.com/rasuvaeff "rasuvaeff (21 commits)")

---

Tags

ab-testingdeterministicexperimentationphpsplit-testingyii3yii3split testingfeature flagexperimentab-testing

###  Code Quality

TestsPHPUnit

Static AnalysisPsalm, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/rasuvaeff-yii3-ab-testing/health.svg)

```
[![Health](https://phpackages.com/badges/rasuvaeff-yii3-ab-testing/health.svg)](https://phpackages.com/packages/rasuvaeff-yii3-ab-testing)
```

###  Alternatives

[symfony/lock

Creates and manages locks, a mechanism to provide exclusive access to a shared resource

515139.2M711](/packages/symfony-lock)[matomo/matomo

Matomo is the leading Free/Libre open analytics platform

21.7k38.9k](/packages/matomo-matomo)[ecotone/ecotone

Enterprise architecture layer for Laravel and Symfony — CQRS, Event Sourcing, Durable Workflows (Sagas, Orchestrators), Projections, and Outbox messaging via PHP attributes.

564576.7k54](/packages/ecotone-ecotone)[civicrm/civicrm-core

Open source constituent relationship management for non-profits, NGOs and advocacy organizations.

751291.4k46](/packages/civicrm-civicrm-core)[illuminate/broadcasting

The Illuminate Broadcasting package.

7127.2M221](/packages/illuminate-broadcasting)[dagger/dagger

Dagger PHP SDK

261.1k](/packages/dagger-dagger)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
