承接 rasuvaeff/yii3-ab-testing 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

rasuvaeff/yii3-ab-testing

最新稳定版本:v1.3.0

Composer 安装命令:

composer require rasuvaeff/yii3-ab-testing

包简介

Deterministic A/B testing for Yii3 applications

README 文档

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+ (64-bit — the hash bucket exceeds PHP_INT_MAX on 32-bit builds)

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.
}

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)

$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,
];

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();
}

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)

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

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.

统计信息

  • 总下载量: 10
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 9
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: BSD-3-Clause
  • 更新时间: 2026-06-10

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固