countxvat/launchdarkly-bundle

LaunchDarkly integration for Symfony

2.0.0 2023-03-10 22:22 UTC

This package is auto-updated.

Last update: 2024-04-11 12:06:30 UTC


README

tests

Introduction

A Symfony bundle to integrate the PHP client for LaunchDarkly, a Feature flag management service.

Documentation for the PHP Client library can be found at https://launchdarkly.github.io/php-server-sdk

Installation

$ composer require countxvat/launchdarkly-bundle

The flags need to be fetched somehow. The simplest option is using Guzzle to make HTTP requests. A more complicated but more performant option is to use the ld-daemon
to store the flags in Redis and then access them from there. There are further dependencies for each of these options:

Guzzle

$ composer require "guzzlehttp/guzzle:^7"
$ composer require "kevinrob/guzzle-cache-middleware": "1.4.1"

Redis and ld-bundle

$ composer require "predis/predis": "1.0.*"

The setup of ld-daemon also needs to be done.

Simplest config and example

The bundle needs to be registered in AppKernel:

// app/AppKernel.php

public function registerBundles()
{
    $bundles = [
        //...
        new new countX\LaunchDarklyBundle\countXLaunchDarklyBundle(),
    ];
}

The minimum config needed is to provide your API Key:

#app/config/config.yml

countx_launch_darkly:
    api_key: APIKEY

Note: In practice different API keys are used for different environments so making it a parameter will be the way to go.

A flag can then be checked using a flag service:

if ($this->get('countx_launchdarkly.client')->variation('my-flag')) {
    //new feature
}

Flag in code

Checking flags in conditionals is the simplest approach to get started with. It may not be the best approach for maintainability though and other options are provided below.

Passing default

A default value can be passed which will be used if an error is encountered, for example, if the feature flag key doesn't exist or the user doesn't have a key specified. This defaults to false.

if ($this->get('countx_launchdarkly.client')->variation('my-flag', true)) {
    //new feature
}

Service

You can inject the service into other classes as well as retrieving it from the container in a controller using the usual Symfony service configuration:

<service id="my-service" class="MyClass">
	<argument type="service" id="countx_launchdarkly.client"/>
</service>

Static Access

There is also static access to check a flag, whilst this is generally frowned upon it may be preferable to the boilerplate of injecting the service and the associated config since this is temporary code that should be removed once the flag value is no longer changed:

if (\countX\LaunchDarklyBundle\Client\StaticClient::variation('my-flag', true)) {
    //new feature
}

Twig extension

There is a twig function which allows you to check flags from templates:

{% if isFlagOn("new-template-content") %}
    <p>the new template content</p>
{% else %}
    <p>the old template content</p>
{% endif %}

or passing a default value:

{% if isFlagOn("new-template-content", true) %}
    <p>the new template content</p>
{% else %}
    <p>the old template content</p>
{% endif %}

Service configuration

Rather than including conditionals in code you may prefer to inject alternative versions of services (and then remove the old service altogether once the flag is always on).

This can be done at the level of defining arguments for a service using an expression language function:

 countx_launchdarkly.test_service:
        class: OuterService
        arguments: ["@=toggle('new-service-content', 'countx_launchdarkly.new_test_service', 'countx_launchdarkly.old_test_service')"]

Where the first argument (new-service-content) is the flag, the second (countx_launchdarkly.new_test_service) is the service to inject if it is on and the third (countx_launchdarkly.old_test_service) is the service to use if the flag is off.

Alternatively you can change which service a service id points at, similar to how aliases work but determined by a flag value. This can be done using a tag:

countx_launchdarkly.test_service:
    class: TestService
    tags:
        - name: countx_launchdarkly.toggle
          flag: new-service-content
          active-id: countx_launchdarkly.new_test_service
          inactive-id: countx_launchdarkly.old_test_service

Here the countx_launchdarkly.test_service service id will be an alias for the countx_launchdarkly.new_test_service if the new-service-content flag is on and an alias for the countx_launchdarkly.old_test_service if it is off. This allows you to define the change in a single place and have it affect all places where the countx_launchdarkly.test_service service is used.

A shorter but hackier (the way it is processed by the bundle is hackier) alternative is to use:

countx_launchdarkly.test_service:
        alias: "new-aliased-service-content?countx_launchdarkly.new_test_service:countx_launchdarkly.old_test_service"

User id provider service

The user id/key used when requesting a flag's value is provided by a service which needs implement the \countX\LaunchDarklyBundle\User\KeyProvider Interface:

interface KeyProvider
{
    public function userKey();
}

This should return a string value representing the user uniquely, for example their username or if anonymous their session id. The default implementation uses the session id. To use an alternative service create the service definition as normal using whatever id you would like and then set that as the value for the user_key_provider_service key in the bundle config:

countx_launch_darkly:
    api_key: MYAPIKEY
    user_key_provider_service: my_user_key_provider_sevice

User factory service

Only the id/key returned from the user id provider service will be sent to LaunchDarkly by default. You can send further information which allows you to fine tune which user the flag is on for. This can be done by providing an alternative implementation of the user factory service which needs to implement the \countX\LaunchDarklyBundle\User\UserFactory interface:

interface UserFactory
{
    public function create($key);
}

This should return an \LaunchDarkly\LDUser object. There is a builder (\LaunchDarkly\LDUserBuilder) for the LDUser which can be used to simplify this. The service then needs a definition creating as usual and the id provided as the value for the user_factory_service key in the config:

countx_launch_darkly:
    api_key: MYAPIKEY
    user_factory_service: my_user_factory_sevice

For example, if we wanted to send the ip address of the user as well we could create an implementation like this:

use countX\LaunchDarklyBundle\User\UserFactory;use LaunchDarkly\LDUserBuilder;

class IPUserFactory implements UserFactory
{
    public function create($key)
    {
        $ip = $_SERVER['REMOTE_ADDR'];
        return (new LDUserBuilder($key))->ip($ip)->build();
    }
    
}

Passing the user key explicitly

If the user is not available implicitly from the session (or some other means) then you can pass it directly using the

countx_launchdarkly.user_client

service. For example:

if ($this->get('countx_launchdarkly.user_client')->variation('my-flag', new LDClient('the-user-id'))) {
    //new feature
}

or with an alternative static client:

if (\countX\LaunchDarklyBundle\Client\ExplicitUser\StaticClient::variation('my-flag', new LDClient('the-user-id'))) {
    //new feature
}

When using these versions of the client the User id provider service and User factory service will not be used.

Debug toolbar

The flags requested, and whether they were requested in a template, as a service or in code is captured and shown in the debug toolbar.

Full Configuration

The full configuration is below. The base_uri, timeout, connect_timeout, events and defaults keys are all settings for the LaunchDarkly client itself. See http://docs.launchdarkly.com/docs/php-sdk-reference for details.

# Default configuration for extension with alias: "countx_launch_darkly"
countx_launch_darkly:  # Required
    api_key:              ~ # Required
    base_uri:             ~
    user_factory_service:  countx_launchdarkly.simple_user_factory
    user_key_provider_service:  countx_launchdarkly.session_key_provider
    feature_requester_class:  ~
    timeout:              ~
    connect_timeout:      ~
    capacity:             ~
    events:               ~
    defaults:

        # Prototype
        flag:                 ~

Full client

You can also use the full client service to access other methods on the LDClient (see http://docs.launchdarkly.com/docs/php-sdk-reference). This has a service id of countx_launchdarkly.inner_client.

Todo

  • A user id provider service that integrates with the security user
  • Integration with routing to handle routes with different controllers dependant on flag value
  • Integration with event listeners/subscribers to allow registering dependant on flag value
  • Integration with event listeners/subscribers to allow registering dependant on flag value
  • Integration with security to allow things with roles to be dependant on flag values if that makes sense as a useful thing