rikudou/unleash-sdk

此软件包已被 废弃 且不再维护。作者建议使用 unleash/client 软件包。

v1.1.180 2021-07-08 15:07 UTC

README

Tests Tests (7.x) Coverage Status

PHP 实现的 Unleash 协议,也称为 GitLab 中的 功能标志

您可能还对此软件包的 Symfony 扩展包 感兴趣。

此实现符合官方 Unleash 标准,并实现了所有 Unleash 功能。

Unleash 允许您在完全发布之前逐步发布应用程序的功能,基于多种策略,例如仅向特定用户发布或向用户总数的百分比发布。在上述链接的文档中了解更多信息。

安装

composer require rikudou/unleash-sdk

需要 PHP 7.3 或更高版本。

您还需要一些 PSR-18PSR-17 的实现,例如 GuzzlePSR-16,例如 Symfony Cache。示例

composer require rikudou/unleash-sdk guzzlehttp/guzzle symfony/cache

或者

composer require rikudou/unleash-sdk symfony/http-client nyholm/psr7 symfony/cache

使用方法

基本用法是获取 Unleash 对象并检查功能

<?php

use Rikudou\Unleash\UnleashBuilder;

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id')
    ->build();

if ($unleash->isEnabled('some-feature-name')) {
    // do something
}

您也可以(在某些情况下必须)提供一个上下文对象。如果功能在服务器上不存在,您将从 isEnabled() 获取 false,但您可以更改默认值到 true

<?php

use Rikudou\Unleash\UnleashBuilder;
use Rikudou\Unleash\Configuration\UnleashContext;

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id')
    ->build();

$context = new UnleashContext(
    currentUserId: 'some-user-id-from-app',
    ipAddress: '127.0.0.1', // will be populated automatically from $_SERVER if needed
    sessionId: 'sess-123456', // will be populated automatically via session_id() if needed
);

// or using pre php 8 style:

$context = (new UnleashContext())
    ->setCurrentUserId('some-user-id-from-app')
    ->setIpAddress('127.0.0.1')
    ->setSessionId('sess-123456');

if ($unleash->isEnabled('some-feature', $context)) {
    // do something
}

// changing the default value for non-existent features
if ($unleash->isEnabled('nonexistent-feature', $context, true)) {
    // do something
}

构建器

构建器包含许多配置选项,建议始终使用构建器来构建 Unleash 实例。构建器是不可变的。

可以使用 create() 静态方法或使用其构造函数来创建构建器对象

<?php

use Rikudou\Unleash\UnleashBuilder;

// both are the same
$builder = new UnleashBuilder();
$builder = UnleashBuilder::create();

必需参数

根据规范,应用程序名称、实例 ID 和应用程序 URL 是必需的。

<?php

use Rikudou\Unleash\UnleashBuilder;

$builder = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id');

如果您正在使用 Unleash v4,您还需要指定授权密钥(API 密钥),您可以通过自定义头来实现。

<?php

use Rikudou\Unleash\UnleashBuilder;

$builder = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id')
    ->withHeader('Authorization', 'my-api-key');

可选参数

可以设置一些可选参数,包括

  • HTTP 客户端实现
  • 请求工厂实现
  • 缓存实现(《PSR-16》)
  • 缓存 TTL
  • 可用策略
  • HTTP 头

如果您使用guzzlehttp/guzzlesymfony/http-client(结合nyholm/psr7),则http客户端和请求工厂将自动创建,否则您需要自行提供实现。

如果您使用symfony/cachecache/filesystem-adapter作为缓存实现,则缓存处理程序将自动创建,否则您需要自行提供一些实现。

<?php

use Cache\Adapter\Filesystem\FilesystemCachePool;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use Rikudou\Unleash\Stickiness\MurmurHashCalculator;
use Rikudou\Unleash\Strategy\DefaultStrategyHandler;
use Rikudou\Unleash\Strategy\GradualRolloutStrategyHandler;
use Rikudou\Unleash\Strategy\IpAddressStrategyHandler;
use Rikudou\Unleash\Strategy\UserIdStrategyHandler;
use Rikudou\Unleash\UnleashBuilder;

$builder = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id')
    // now the optional ones
    ->withHttpClient(new Client())
    ->withRequestFactory(new HttpFactory())
    ->withCacheHandler(new FilesystemCachePool( // example with using cache/filesystem-adapter
        new Filesystem(
            new Local(sys_get_temp_dir()),
        ),
    ), 30) // the second parameter is time to live in seconds
    ->withCacheTimeToLive(60) // you can also set the cache time to live separately
    // if you don't add any strategies, by default all strategies are added
    ->withStrategies( // this example includes all available strategies
        new DefaultStrategyHandler(),
        new GradualRolloutStrategyHandler(new MurmurHashCalculator()),
        new IpAddressStrategyHandler(),
        new UserIdStrategyHandler(),
    )
    // add headers one by one, if you specify a header with the same name multiple times it will be replaced by the
    // latest value
    ->withHeader('My-Custom-Header', 'some-value')
    ->withHeader('Some-Other-Header', 'some-other-value')
    // you can specify multiple headers at the same time, be aware that this REPLACES all the headers
    ->withHeaders([
        'Yet-Another-Header' => 'and-another-value',
    ]);

缓存

每次检查功能是否启用时都执行HTTP请求将会很慢,尤其是在热门应用中。这就是为什么这个库内置了对PSR-16缓存实现的支撑。

如果您没有提供任何实现并且存在默认实现,则使用默认实现,否则您将得到一个异常。您还可以提供TTL,默认为30秒。

开箱即用的缓存实现(意味着您不需要进行任何配置)

<?php

use Cache\Adapter\Filesystem\FilesystemCachePool;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
use Rikudou\Unleash\UnleashBuilder;

$builder = UnleashBuilder::create()
    ->withCacheHandler(new FilesystemCachePool( // example with using cache/filesystem-adapter
        new Filesystem(
            new Local(sys_get_temp_dir()),
        ),
    ))
    ->withCacheTimeToLive(120);

// you can set the cache handler explicitly to null to revert back to autodetection

$builder = $builder
    ->withCacheHandler(null);

策略

Unleash服务器可以使用多个策略来启用或禁用功能。哪个策略被使用是在服务器上定义的。此实现支持所有非弃用的v4策略,除了主机名。《更多信息请参考此处》[https://docs.getunleash.io/user_guide/activation_strategy](https://docs.getunleash.io/user_guide/activation_strategy)。

默认策略

这是最简单的一种,如果功能将其默认策略定义为true,并且不需要任何上下文参数,则始终返回true。

IP地址策略

根据IP地址启用功能。从上下文对象中获取当前用户的IP地址。您可以提供自己的IP地址或使用默认值(`$_SERVER['REMOTE_ADDR']`)。提供自己的IP地址特别有用,如果您位于代理后面,因此`REMOTE_ADDR`将返回您的代理服务器的IP地址。

<?php

use Rikudou\Unleash\UnleashBuilder;
use Rikudou\Unleash\Configuration\UnleashContext;

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id')
    ->build();

// without context, using the auto detected IP
$enabled = $unleash->isEnabled('some-feature');

// with context
$context = new UnleashContext(ipAddress: $_SERVER['HTTP_X_FORWARDED_FOR']);
// or pre php 8 style
$context = (new UnleashContext())
    ->setIpAddress($_SERVER['HTTP_X_FORWARDED_FOR']);
$enabled = $unleash->isEnabled('some-feature', $context);

用户ID策略

根据用户ID启用功能。用户ID可以是任何字符串。您必须始终通过上下文提供自己的用户ID。

<?php

use Rikudou\Unleash\UnleashBuilder;
use Rikudou\Unleash\Configuration\UnleashContext;

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id')
    ->build();

$context = new UnleashContext(currentUserId: 'some-user-id');
$enabled = $unleash->isEnabled('some-feature', $context);

渐进式发布策略

也称为灵活发布。允许您根据用户的用户ID、会话ID或随机选择仅对部分用户启用功能。默认情况下,按以下顺序尝试:用户ID、会话ID、随机。

如果您在Unleash服务器上指定了用户ID类型,您还必须通过上下文提供用户ID,与用户ID策略相同。会话ID也可以通过上下文提供,默认值为当前会话ID,通过`session_id()`调用。

此策略需要一个粘性计算器,该计算器将ID(用户、会话或随机)转换为1到100之间的数字。您可以提供自己的或使用默认的`\Rikudou\Unleash\Stickiness\MurmurHashCalculator`。

<?php

use Rikudou\Unleash\UnleashBuilder;
use Rikudou\Unleash\Configuration\UnleashContext;

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id')
    ->build();

// assume the feature uses the default type which means that it will default to either session id (if session is started)
// or randomly
$unleash->isEnabled('some-feature');

// still using the default strategy but this time with user id (which is the first to be used if present)
$context = new UnleashContext(currentUserId: 'some-user-id');
$unleash->isEnabled('some-feature', $context);

// let's start the session to ensure the session id is used
session_start();
$unleash->isEnabled('some-feature');

// or you can provide your own session id
$context = new UnleashContext(sessionId: 'sess-123456');
$unleash->isEnabled('some-feature', $context);

// assume the feature is set to use the user id, the first call returns false (no context given), the second
// one returns true/false based on the user id
$unleash->isEnabled('some-feature');
$context = new UnleashContext(currentUserId: 'some-user-id');
$unleash->isEnabled('some-feature', $context);

// the same goes for session, assume the session isn't started yet and the feature is set to use the session type
$unleash->isEnabled('some-feature'); // returns false because no session is available

$context = new UnleashContext(sessionId: 'some-session-id');
$unleash->isEnabled('some-feature', $context); // works because you provided the session id manually

session_start();
$unleash->isEnabled('some-feature'); // works because the session is started

// lastly you can force the feature to use the random type which always works
$unleash->isEnabled('some-feature');

注意:此库还实现了某些弃用的策略,即`gradualRolloutRandom`、`gradualRolloutSessionId`和`gradualRolloutUserId`,这些都代理到渐进式发布策略。

自定义策略

要实现自己的策略,您需要创建一个实现了StrategyHandler(或包含一些有用方法的AbstractStrategyHandler)的类。然后,您需要指示构建器使用您的自定义策略。

<?php

use Rikudou\Unleash\Strategy\AbstractStrategyHandler;
use Rikudou\Unleash\DTO\Strategy;
use Rikudou\Unleash\Configuration\Context;
use Rikudou\Unleash\Strategy\DefaultStrategyHandler;

class AprilFoolsStrategy extends AbstractStrategyHandler
{
    public function __construct(private DefaultStrategyHandler $original)
    {
    }
    
    public function getStrategyName() : string
    {
        return 'aprilFools';
    }
    
    public function isEnabled(Strategy $strategy, Context $context) : bool
    {
        $date = new DateTimeImmutable();
        if ((int) $date->format('n') === 4 && (int) $date->format('j') === 1) {
            return (bool) random_int(0, 1);
        }
        
        return $this->original->isEnabled($strategy, $context);
    }
}

现在您必须指示构建器使用您的新策略

<?php

use Rikudou\Unleash\UnleashBuilder;
use Rikudou\Unleash\Strategy\IpAddressStrategyHandler;

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id')
    ->withStrategy(new AprilFoolsStrategy()) // this will append your strategy to the existing list
    ->build();

// if you want to replace all strategies, use withStrategies() instead

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id')
    ->withStrategies(new AprilFoolsStrategy(), new IpAddressStrategyHandler())
    // now the unleash object will have only the two strategies
    ->build();

变体

您可以使用一个功能的多个变体,例如用于A/B测试。如果没有匹配的变体或该功能没有任何变体,将返回默认的一个,该默认变体对isEnabled()返回false。您还可以提供自己的默认变体。

变体可能包含或不包含负载。

<?php

use Rikudou\Unleash\DTO\DefaultVariant;
use Rikudou\Unleash\UnleashBuilder;
use Rikudou\Unleash\Configuration\UnleashContext;
use Rikudou\Unleash\Enum\VariantPayloadType;
use Rikudou\Unleash\DTO\DefaultVariantPayload;

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://some-app-url.com')
    ->withInstanceId('Some instance id')
    ->build();
    
$variant = $unleash->getVariant('nonexistentFeature');
assert($variant->isEnabled() === false);

// getVariant() does isEnabled() call in the background meaning that it will return the default falsy variant
// whenever isEnabled() returns false
$variant = $unleash->getVariant('existingFeatureThatThisUserDoesNotHaveAccessTo');
assert($variant->isEnabled() === false);

$variant = $unleash->getVariant('someFeature', new UnleashContext(currentUserId: '123'));
if ($variant->isEnabled()) {
    $payload = $variant->getPayload();
    if ($payload !== null) {
        if ($payload->getType() === VariantPayloadType::JSON) {
            $jsonData = $payload->fromJson();
        }
        $stringPayload = $payload->getValue();
    }
}

// providing custom default variant

$variant = $unleash->getVariant('nonexistentFeature', fallbackVariant: new DefaultVariant(
    'variantName',
    enabled: true,
    payload: new DefaultVariantPayload(VariantPayloadType::STRING, 'somePayload'),
));
assert($variant->getPayload()->getValue() === 'somePayload');

客户端注册

默认情况下,库会自动将其自己注册为Unleash服务器中的应用程序。如果您想防止这种情况,请在构建器中使用withAutomaticRegistrationEnabled(false)

<?php

use Rikudou\Unleash\UnleashBuilder;

$unleash = UnleashBuilder::create()
    ->withAppName('Some App Name')
    ->withAppUrl('https://somewhere.com')
    ->withInstanceId('some-instance-id')
    ->withAutomaticRegistrationEnabled(false)
    ->build();

// event though the client will not attempt to register, you can still use isEnabled()
$unleash->isEnabled('someFeature');

// if you want to register manually
$unleash->register();

// you can call the register method multiple times, the Unleash server doesn't mind
$unleash->register();
$unleash->register();

指标

默认情况下,此库会发送关于用户是否被授予访问权限的简单统计指标的指标。

当创建的包创建时间超过配置的阈值时,指标将被捆绑并发送一次。默认阈值是30,000毫秒(30秒),这意味着在新的包创建后,它不会在30秒内发送。这并不意味着指标将每30秒发送一次,它仅保证指标不会在30秒内发送。

示例

  1. 用户访问您的网站并触发此sdk,未发送任何指标
  2. 五秒后用户访问另一个页面,再次触发此sdk,未发送指标
  3. 用户在执行任何操作之前等待一分钟,没有其他人访问您的网站
  4. 一分钟后用户访问另一个页面,指标已发送到Unleash服务器

在上面的示例中,由于没有触发代码的人,指标包是在1分钟和5秒后发送的。

<?php

use Rikudou\Unleash\UnleashBuilder;

$unleash = UnleashBuilder::create()
    ->withAppName('Some App Name')
    ->withAppUrl('https://somewhere.com')
    ->withInstanceId('some-instance-id')
    ->withMetricsEnabled(false) // turn off metric sending
    ->withMetricsEnabled(true) // turn on metric sending
    ->withMetricsInterval(10_000) // interval in milliseconds (10 seconds)
    ->build();

// the metric will be collected but not sent immediately
$unleash->isEnabled('test');
sleep(10);
// now the metrics will get sent
$unleash->isEnabled('test');

约束

此SDK支持约束,如果存在,将由Unleash::isEnabled()正确处理。

GitLab特定

  • 在GitLab中,您必须使用提供的实例ID,您不能创建自己的。
  • 不需要授权头。
  • 您需要指定GitLab环境而不是应用程序名称。
    • 为此,您可以在构建器中使用withGitlabEnvironment()方法,它是withAppName()的别名,但更好地传达了意图。
  • GitLab不使用注册系统,您可以将SDK设置为禁用自动注册以节省一个HTTP调用。
  • GitLab不读取指标,您可以将SDK设置为禁用发送它们以节省一些HTTP调用。
<?php

use Rikudou\Unleash\UnleashBuilder;

$gitlabUnleash = UnleashBuilder::createForGitlab()
    ->withInstanceId('H9sU9yVHVAiWFiLsH2Mo') // generated in GitLab
    ->withAppUrl('https://git.example.com/api/v4/feature_flags/unleash/1')
    ->withGitlabEnvironment('Production')
    ->build();

// the above is equivalent to
$gitlabUnleash = UnleashBuilder::create()
    ->withInstanceId('H9sU9yVHVAiWFiLsH2Mo')
    ->withAppUrl('https://git.example.com/api/v4/feature_flags/unleash/1')
    ->withGitlabEnvironment('Production')
    ->withAutomaticRegistrationEnabled(false)
    ->withMetricsEnabled(false)
    ->build();