├── .gitignore
├── docs
├── images
│ ├── cookie.png
│ └── profiler.png
├── attribute.md
├── twig.md
├── route_condition.md
├── profiler.md
├── environment.md
├── install.md
├── activator.md
├── cookie.md
├── usage.md
├── database.md
├── route.md
└── constraint.md
├── UPGRADE-5.0.md
├── phpcs.xml
├── UPGRADE-4.0.md
├── src
├── Attribute
│ └── Feature.php
├── Resources
│ ├── views
│ │ ├── collector
│ │ │ └── icon.svg
│ │ └── profiler
│ │ │ └── layout.html.twig
│ └── config
│ │ ├── configurators.yml
│ │ └── services.yml
├── Route
│ └── IsFeature.php
├── FlagceptionBundle.php
├── DependencyInjection
│ ├── CompilerPass
│ │ ├── ExpressionProviderPass.php
│ │ ├── ActivatorPass.php
│ │ └── ContextDecoratorPass.php
│ ├── Configurator
│ │ ├── ActivatorConfiguratorInterface.php
│ │ ├── ArrayConfigurator.php
│ │ ├── EnvironmentConfigurator.php
│ │ ├── ConstraintConfigurator.php
│ │ ├── CookieConfigurator.php
│ │ └── DatabaseConfigurator.php
│ ├── FlagceptionExtension.php
│ └── Configuration.php
├── Activator
│ └── TraceableChainActivator.php
├── Twig
│ └── ToggleExtension.php
├── Listener
│ ├── RoutingMetadataSubscriber.php
│ └── AttributeSubscriber.php
└── Profiler
│ └── FeatureDataCollector.php
├── UPGRADE-6.0.md
├── tests
├── Fixtures
│ └── Helper
│ │ └── AttributeTestClass.php
├── DependencyInjection
│ ├── ConfigurationTest.php
│ ├── CompilerPass
│ │ ├── ActivatorPassTest.php
│ │ ├── ExpressionProviderPassTest.php
│ │ └── ContextDecoratorPassTest.php
│ ├── FlagceptionExtensionTest.php
│ └── Configurator
│ │ ├── ArrayConfiguratorTest.php
│ │ ├── EnvironmentConfiguratorTest.php
│ │ ├── ConstraintConfiguratorTest.php
│ │ ├── CookieConfiguratorTest.php
│ │ └── DatabaseConfiguratorTest.php
├── FlagceptionBundleTest.php
├── Route
│ └── IsFeatureTest.php
├── Twig
│ └── ToggleExtensionTest.php
├── Activator
│ └── TraceableChainActivatorTest.php
├── Listener
│ ├── AttributeSubscriberTest.php
│ └── RoutingMetadataSubscriberTest.php
└── Profiler
│ └── FeatureDataCollectorTest.php
├── LICENSE
├── phpunit.xml
├── .github
└── workflows
│ ├── php_coverage_badge.yml
│ └── php.yml
├── composer.json
├── UPGRADE-3.0.md
├── README.md
└── CHANGELOG.md
/.gitignore:
--------------------------------------------------------------------------------
1 | ./vendor
2 | composer.lock
3 | build/logs/*
4 | !build/logs/.gitkeep
5 |
--------------------------------------------------------------------------------
/docs/images/cookie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/playox/flagception-bundle/HEAD/docs/images/cookie.png
--------------------------------------------------------------------------------
/docs/images/profiler.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/playox/flagception-bundle/HEAD/docs/images/profiler.png
--------------------------------------------------------------------------------
/UPGRADE-5.0.md:
--------------------------------------------------------------------------------
1 | # Upgrade from 4.x to 5.0
2 | Minimum requirement for PHP has been increased to 8.0. No interfaces or services changed.
3 |
4 | Have fun with PHP 8 :-)
5 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | src/
8 | tests/
9 |
10 |
--------------------------------------------------------------------------------
/UPGRADE-4.0.md:
--------------------------------------------------------------------------------
1 | # Upgrade from 3.x to 4.0
2 | Version 4 only removes compatibility for Symfony 2, Symfony 3 and Symfony <= 4.3. No interfaces or services changed.
3 | Please notice that the support for [Contentful Activator](https://packagist.org/packages/flagception/contentful-activator) was dropped.
4 |
5 | Have fun with Symfony 5 :-)
6 |
--------------------------------------------------------------------------------
/src/Attribute/Feature.php:
--------------------------------------------------------------------------------
1 | name = $name;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Resources/views/collector/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/attribute.md:
--------------------------------------------------------------------------------
1 | Attribute
2 | -------------------------
3 | We recommend to use the route attribute solution.
4 | A `NotFoundHttpException` will be thrown if you request an action or class with inactive feature flag.
5 |
6 |
7 | ```php
8 | # FooController.php
9 |
10 | use Flagception\Bundle\FlagceptionBundle\Attribute\Feature;
11 |
12 | #[Feature("feature_123")]
13 | class FooController
14 | {
15 |
16 | #[Feature("feature_789")]
17 | public function barAction()
18 | {
19 | }
20 |
21 | public function fooAction()
22 | {
23 | }
24 | }
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/twig.md:
--------------------------------------------------------------------------------
1 | Twig
2 | -------------------------
3 | You can check the feature flag state with the following twig methods.
4 |
5 | Simple check:
6 | ```twig
7 | {% if feature('feature_123') %}
8 | {# ... #}
9 | {% endif %}
10 | ```
11 |
12 | Same check with other syntax:
13 | ```twig
14 | {% if 'feature_123' is active feature %}
15 | {# ... #}
16 | {% endif %}
17 | ```
18 |
19 | Check with context data (see [constraint documentation](constraint.md))
20 | ```twig
21 | {% if feature('feature_123', {'role': 'ROLE_ADMIN'}) %}
22 | {# ... #}
23 | {% endif %}
24 | ```
25 |
--------------------------------------------------------------------------------
/src/Route/IsFeature.php:
--------------------------------------------------------------------------------
1 | featureManager = $featureManager;
16 | }
17 |
18 | public function __invoke($feature, ?Context $context = null): bool
19 | {
20 | return $this->featureManager->isActive($feature, $context);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/docs/route_condition.md:
--------------------------------------------------------------------------------
1 | Route condition
2 | -------------------------
3 | You can use route attributes for checking the feature state in controllers. This is activated.
4 |
5 | ```php
6 | // src/AppBundle/Controller/BlogController.php
7 | // src/Controller/BlogController.php
8 | namespace AppBundle\Controller;
9 |
10 | use Symfony\Bundle\FrameworkBundle\Controller\Controller;
11 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
12 |
13 | class BlogController extends Controller
14 | {
15 | /**
16 | * @Route("/blog/{page}", condition="is_feature('feature_123')")
17 | */
18 | public function listAction($page)
19 | {
20 | // ...
21 | }
22 |
23 | /**
24 | * @Route("/blog/{slug}")
25 | */
26 | public function showAction($slug)
27 | {
28 | // ...
29 | }
30 | }
31 | ```
--------------------------------------------------------------------------------
/UPGRADE-6.0.md:
--------------------------------------------------------------------------------
1 | # Upgrade from 5.x to 6.0
2 | Support for annotations has been removed. Use PHP 8 attributes instead. In addition, the Flagception SDK and Flagception Database Activator has been updated to version 2.0.
3 | The updated SDK added some types for the interfaces. The updated Database Activator added support for DBAL 4.0 and dropped support for DBAL 3.5 and below.
4 |
5 | ## Upgrade steps
6 | ### Custom Activators
7 | If you have created custom activators, you need to update interface implementations to match the new SDK version. Only types are added.
8 |
9 | ### Database Activator
10 | The database activator now requires DBAL 3.6 or higher. If you are using DBAL 3.5 or below, you need to update your dependencies.
11 | In addition, a PDO instance is no longer supported. You need to pass the connection options as an array, string or DBAL instance.
12 |
13 | Have fun with Flagception :-)
14 |
--------------------------------------------------------------------------------
/tests/Fixtures/Helper/AttributeTestClass.php:
--------------------------------------------------------------------------------
1 |
11 | * @package Flagception\Tests\FlagceptionBundle\Fixtures\Helper
12 | */
13 | #[Feature("feature_abc")]
14 | class AttributeTestClass
15 | {
16 | /**
17 | * Normal test method
18 | *
19 | * @return void
20 | */
21 | public function normalMethod()
22 | {
23 | }
24 |
25 | /**
26 | * Valid test method
27 | *
28 | * @return void
29 | */
30 | public function validMethod()
31 | {
32 | }
33 |
34 | /**
35 | * Method with feature flag
36 | */
37 | #[Feature("feature_def")]
38 | public function invalidMethod()
39 | {
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/docs/profiler.md:
--------------------------------------------------------------------------------
1 | Profiler
2 | -------------------------
3 |
4 | Take a look at our new profiler tab:
5 |
6 | 
7 |
8 | You can see which feature was activated by which activator. In addition, you can see how many activators were asked until it came to a conclusion.
9 | Here is a small listing which activator belongs to which config:
10 |
11 | ```yml
12 | # config.yml
13 |
14 | flagception:
15 | features:
16 | feature_123:
17 |
18 | # The default property belongs to 'array'
19 | default: false
20 |
21 | # The constraint property belongs to 'constraint'
22 | constraint: 'user_id === 12'
23 |
24 | # The env property belongs to 'environment'
25 | env: 'FEATURE_123'
26 |
27 | # The cookie property belongs to 'cookie'
28 | cookie: true
29 |
30 | # Contentful fields are defined in Contentful and not in your config
31 | # The activator called "contentful"
32 | ```
33 |
--------------------------------------------------------------------------------
/src/FlagceptionBundle.php:
--------------------------------------------------------------------------------
1 |
15 | * @package Flagception\Bundle\FlagceptionBundle
16 | */
17 | class FlagceptionBundle extends Bundle
18 | {
19 | /**
20 | * {@inheritdoc}
21 | */
22 | public function build(ContainerBuilder $container): void
23 | {
24 | parent::build($container);
25 |
26 | $container->addCompilerPass(new ActivatorPass());
27 | $container->addCompilerPass(new ContextDecoratorPass());
28 | $container->addCompilerPass(new ExpressionProviderPass());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 best it GmbH & Co. KG
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ./src
7 |
8 |
9 | src/*Bundle/Resources
10 | src/*/*Bundle/Resources
11 | src/*/Bundle/*Bundle/Resources
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ./tests
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/DependencyInjection/CompilerPass/ExpressionProviderPass.php:
--------------------------------------------------------------------------------
1 |
13 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection\CompilerPass
14 | */
15 | class ExpressionProviderPass implements CompilerPassInterface
16 | {
17 | /**
18 | * {@inheritdoc}
19 | */
20 | public function process(ContainerBuilder $container): void
21 | {
22 | $factory = $container->getDefinition('flagception.factory.expression_language_factory');
23 |
24 | foreach ($container->findTaggedServiceIds('flagception.expression_language_provider') as $id => $tags) {
25 | foreach ($tags as $attributes) {
26 | $factory->addMethodCall('addProvider', [new Reference($id)]);
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Resources/config/configurators.yml:
--------------------------------------------------------------------------------
1 | services:
2 | flagception.configurator.array_configurator:
3 | class: Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator\ArrayConfigurator
4 | tags:
5 | - { name: flagception.configurator }
6 |
7 | flagception.configurator.constraint_configurator:
8 | class: Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator\ConstraintConfigurator
9 | tags:
10 | - { name: flagception.configurator }
11 |
12 | flagception.configurator.environment_configurator:
13 | class: Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator\EnvironmentConfigurator
14 | tags:
15 | - { name: flagception.configurator }
16 |
17 | flagception.configurator.cookie_configurator:
18 | class: Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator\CookieConfigurator
19 | tags:
20 | - { name: flagception.configurator }
21 |
22 | flagception.configurator.database_configurator:
23 | class: Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator\DatabaseConfigurator
24 | tags:
25 | - { name: flagception.configurator }
26 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configurator/ActivatorConfiguratorInterface.php:
--------------------------------------------------------------------------------
1 |
12 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator
13 | */
14 | interface ActivatorConfiguratorInterface
15 | {
16 | /**
17 | * Get configurator key
18 | *
19 | * @return string
20 | */
21 | public function getKey();
22 |
23 | /**
24 | * Add activator
25 | *
26 | * @param ContainerBuilder $container
27 | * @param array $config
28 | * @param array $features
29 | *
30 | * @return void
31 | */
32 | public function addActivator(ContainerBuilder $container, array $config, array $features);
33 |
34 | /**
35 | * Add configuration to node
36 | *
37 | * @param ArrayNodeDefinition $node
38 | *
39 | * @return void
40 | */
41 | public function addConfiguration(ArrayNodeDefinition $node);
42 | }
43 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/ConfigurationTest.php:
--------------------------------------------------------------------------------
1 |
17 | * @package Flagception\Tests\FlagceptionBundle\DependencyInjection
18 | */
19 | class ConfigurationTest extends TestCase
20 | {
21 | /**
22 | * Test builder (only if a tree returned)
23 | *
24 | * @return void
25 | */
26 | public function testBuilder()
27 | {
28 | static::assertInstanceOf(TreeBuilder::class, (new Configuration([
29 | new ArrayConfigurator(),
30 | new ConstraintConfigurator(),
31 | new CookieConfigurator(),
32 | new EnvironmentConfigurator()
33 | ]))->getConfigTreeBuilder());
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/docs/environment.md:
--------------------------------------------------------------------------------
1 | Environment variables
2 | -------------------------
3 | Maybe you want to set a feature flag by an environment variable. In Symfony 3.4 you can set and cast env variables
4 | very simple. But in symfony versions before that, it's not that easy. Therefore, you can set in the config whether the
5 | active status is to be pulled from an environment variable.
6 |
7 | Just give the variable name in the `env` parameter:
8 |
9 | ```yml
10 | # config.yml
11 |
12 | flagception:
13 | features:
14 |
15 | # This feature check the env var 'FEATURE_NAME_FROM_ENV'
16 | # setenv('FEATURE_NAME_FROM_ENV=false')
17 | feature_123:
18 | env: FEATURE_NAME_FROM_ENV
19 | ```
20 |
21 | You can combine all parameter together. First the `default` value is checked.
22 | If the value is false, the value from `env` is checked. If this also returns false, the constraints are checked.
23 |
24 | ```yml
25 | # config.yml
26 |
27 | flagception:
28 | features:
29 | feature_123:
30 | default: false
31 | env: FEATURE_NAME_FROM_ENV
32 | constraint: 'user_role == ROLE_ADMIN'
33 | ```
34 |
35 | As alternative, you can use the `%env()%` syntax for the default field:
36 |
37 | ```yml
38 | # config.yml
39 |
40 | flagception:
41 | features:
42 | feature_123:
43 | default: '%env(FEATURE_NAME_FROM_ENV)%'
44 | ```
45 |
46 |
--------------------------------------------------------------------------------
/tests/FlagceptionBundleTest.php:
--------------------------------------------------------------------------------
1 |
16 | * @package Flagception\Tests\FlagceptionBundle
17 | */
18 | class FlagceptionBundleTest extends TestCase
19 | {
20 | /**
21 | * Test build method
22 | *
23 | * @return void
24 | */
25 | public function testBuild()
26 | {
27 | $bundle = new FlagceptionBundle();
28 |
29 | $builder = $this->createMock(ContainerBuilder::class);
30 | $builder
31 | ->expects(static::exactly(3))
32 | ->method('addCompilerPass')
33 | ->withConsecutive(
34 | [static::isInstanceOf(ActivatorPass::class)],
35 | [static::isInstanceOf(ContextDecoratorPass::class)],
36 | [static::isInstanceOf(ExpressionProviderPass::class)]
37 | );
38 |
39 | $bundle->build($builder);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Activator/TraceableChainActivator.php:
--------------------------------------------------------------------------------
1 |
12 | * @package Flagception\Bundle\FlagceptionBundle\Activator
13 | */
14 | class TraceableChainActivator extends ChainActivator
15 | {
16 | /**
17 | * Trace of this chain activator
18 | *
19 | * @var array
20 | */
21 | private $trace = [];
22 |
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public function isActive($name, Context $context): bool
27 | {
28 | $stack = [];
29 | $result = false;
30 | foreach ($this->getActivators() as $activator) {
31 | if ($activator->isActive($name, $context) === true) {
32 | $result = $stack[$activator->getName()] = true;
33 |
34 | break;
35 | }
36 |
37 | $stack[$activator->getName()] = $result;
38 | }
39 |
40 | $this->trace[] = [
41 | 'feature' => $name,
42 | 'context' => $context,
43 | 'result' => $result,
44 | 'stack' => $stack
45 | ];
46 |
47 | return $result;
48 | }
49 |
50 | /**
51 | * Get trace
52 | *
53 | * @return array
54 | */
55 | public function getTrace(): array
56 | {
57 | return $this->trace;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.github/workflows/php_coverage_badge.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | jobs:
7 | coverage:
8 | strategy:
9 | matrix:
10 | php: ['8.3']
11 | symfony: ['7.4.0']
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v3
17 |
18 | - uses: shivammathur/setup-php@v2
19 | with:
20 | php-version: ${{ matrix.php }}
21 | tools: composer
22 | coverage: xdebug
23 | - name: Setup Dependencies
24 | run: composer require "symfony/symfony:${{ matrix.symfony }}" --no-update
25 | - name: Install dependencies
26 | run: composer install --prefer-dist --no-progress
27 |
28 | - name: run tests
29 | run: ./vendor/bin/simple-phpunit
30 |
31 | - name: Make code coverage badge
32 | uses: timkrase/phpunit-coverage-badge@v1.2.1
33 | with:
34 | coverage_badge_path: output/coverage.svg
35 | push_badge: false
36 |
37 | - name: Git push to image-data branch
38 | uses: peaceiris/actions-gh-pages@v3
39 | with:
40 | publish_dir: ./output
41 | publish_branch: image-data
42 | github_token: ${{ secrets.GITHUB_TOKEN }}
43 | user_name: 'github-actions[bot]'
44 | user_email: 'github-actions[bot]@users.noreply.github.com'
45 |
--------------------------------------------------------------------------------
/tests/Route/IsFeatureTest.php:
--------------------------------------------------------------------------------
1 | createMock(FeatureManagerInterface::class);
22 | $manager
23 | ->expects(static::once())
24 | ->method('isActive')
25 | ->with('feature_abc')
26 | ->willReturn(false);
27 |
28 | $isFeature = new IsFeature($manager);
29 | $this->assertFalse($isFeature->__invoke('feature_abc'));
30 | }
31 |
32 | /**
33 | * Test feature is active
34 | *
35 | * @return void
36 | */
37 | public function testFeatureIsActive()
38 | {
39 | $manager = $this->createMock(FeatureManagerInterface::class);
40 | $manager
41 | ->expects(static::once())
42 | ->method('isActive')
43 | ->with('feature_abc')
44 | ->willReturn(true);
45 |
46 | $isFeature = new IsFeature($manager);
47 | $this->assertTrue($isFeature->__invoke('feature_abc'));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/docs/install.md:
--------------------------------------------------------------------------------
1 | Download the Bundle
2 | ---------------------------
3 |
4 | Open a command console, enter your project directory and execute the
5 | following command to download the latest stable version of this bundle:
6 |
7 | ```console
8 | $ composer require flagception/flagception-bundle
9 | ```
10 |
11 | This command requires you to have Composer installed globally, as explained
12 | in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
13 | of the Composer documentation.
14 |
15 | Enable the Bundle (Symfony 2.x / 3.x)
16 | -------------------------
17 |
18 | Then, enable the bundle by adding it to the list of registered bundles
19 | in the `app/AppKernel.php` file of your project:
20 |
21 | ```php
22 | ['all' => true],
57 | ];
58 | ```
59 |
60 | Use the bundle
61 | -------------------------
62 | That's all. You can now [use feature flags](usage.md).
63 |
--------------------------------------------------------------------------------
/src/DependencyInjection/CompilerPass/ActivatorPass.php:
--------------------------------------------------------------------------------
1 |
13 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection\CompilerPass
14 | */
15 | class ActivatorPass implements CompilerPassInterface
16 | {
17 | /**
18 | * {@inheritdoc}
19 | */
20 | public function process(ContainerBuilder $container): void
21 | {
22 | $bag = $container->getDefinition('flagception.activator.chain_activator');
23 |
24 | $collection = [];
25 | foreach ($container->findTaggedServiceIds('flagception.activator') as $id => $tags) {
26 | foreach ($tags as $attributes) {
27 | $collection[] = [
28 | 'service' => new Reference($id),
29 | 'priority' => isset($attributes['priority']) ? $attributes['priority'] : 0
30 | ];
31 | }
32 | }
33 |
34 | // Sorting services
35 | usort($collection, function ($a, $b) {
36 | return ($b['priority'] < $a['priority']) ? -1 : (($b['priority'] > $a['priority']) ? 1 : 0);
37 | });
38 |
39 | // At least ... add ordered list
40 | foreach ($collection as $item) {
41 | $bag->addMethodCall('add', [$item['service']]);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/DependencyInjection/CompilerPass/ContextDecoratorPass.php:
--------------------------------------------------------------------------------
1 |
13 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection\CompilerPass
14 | */
15 | class ContextDecoratorPass implements CompilerPassInterface
16 | {
17 | /**
18 | * {@inheritdoc}
19 | */
20 | public function process(ContainerBuilder $container): void
21 | {
22 | $bag = $container->getDefinition('flagception.decorator.chain_decorator');
23 |
24 | $collection = [];
25 | foreach ($container->findTaggedServiceIds('flagception.context_decorator') as $id => $tags) {
26 | foreach ($tags as $attributes) {
27 | $collection[] = [
28 | 'service' => new Reference($id),
29 | 'priority' => isset($attributes['priority']) ? $attributes['priority'] : 0
30 | ];
31 | }
32 | }
33 |
34 | // Sorting services
35 | usort($collection, function ($a, $b) {
36 | return ($b['priority'] < $a['priority']) ? -1 : (($b['priority'] > $a['priority']) ? 1 : 0);
37 | });
38 |
39 | // At least ... add ordered list
40 | foreach ($collection as $item) {
41 | $bag->addMethodCall('add', [$item['service']]);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flagception/flagception-bundle",
3 | "type": "symfony-bundle",
4 | "description": "Feature toggle bundle on steroids.",
5 | "keywords": ["feature", "feature-toggle", "feature-flags", "flags", "flagception", "symfony", "bundle", "rollout", "toggle"],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Michel Chowanski",
10 | "email": "michel.chowanski@bestit-online.de"
11 | }
12 | ],
13 | "require": {
14 | "php": "^8.0",
15 | "symfony/dependency-injection": "^4.4 | ^5.0 | ^6.0 | ^7.0 | ^8.0",
16 | "symfony/yaml": "^4.4 | ^5.0 | ^6.0 | ^7.0 | ^8.0",
17 | "symfony/config": "^4.4 | ^5.0 | ^6.0 | ^7.0 | ^8.0",
18 | "symfony/http-kernel": "^4.4 | ^5.0 | ^6.0 | ^7.0 | ^8.0",
19 | "twig/twig": "^1.18|^2.0|^3.0",
20 | "flagception/flagception": "^2.0"
21 | },
22 | "require-dev": {
23 | "symfony/phpunit-bridge": "^5.0 | ^6.0 | ^7.0 | ^8.0",
24 | "symfony/twig-bridge": "^4.4 | ^5.0 | ^6.0 | ^7.0 | ^8.0",
25 | "flagception/database-activator": "^2.0",
26 | "squizlabs/php_codesniffer": "^3.3.1",
27 | "php-coveralls/php-coveralls": "^2.0"
28 | },
29 | "autoload": {
30 | "psr-4": {
31 | "Flagception\\Bundle\\FlagceptionBundle\\": "src/"
32 | }
33 | },
34 | "autoload-dev": {
35 | "psr-4": {
36 | "Flagception\\Tests\\FlagceptionBundle\\": "tests"
37 | }
38 | },
39 | "extra": {
40 | "thanks": {
41 | "name": "flagception/flagception",
42 | "url": "https://github.com/playox/flagception-sdk"
43 | }
44 | },
45 | "suggest": {
46 | "flagception/database-activator": "Fetch feature flags from a database."
47 | },
48 | "scripts": {
49 | "tests": [
50 | "./vendor/bin/phpcs",
51 | "./vendor/bin/simple-phpunit --coverage-text"
52 | ]
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Twig/ToggleExtension.php:
--------------------------------------------------------------------------------
1 |
15 | * @package Flagception\Bundle\FlagceptionBundle\Twig
16 | */
17 | class ToggleExtension extends AbstractExtension
18 | {
19 | /**
20 | * The manager
21 | *
22 | * @var FeatureManagerInterface
23 | */
24 | private $manager;
25 |
26 | /**
27 | * ToggleExtension constructor.
28 | *
29 | * @param FeatureManagerInterface $manager
30 | */
31 | public function __construct(FeatureManagerInterface $manager)
32 | {
33 | $this->manager = $manager;
34 | }
35 |
36 | /**
37 | * Is feature name active
38 | *
39 | * @param string $name
40 | * @param array $contextValues
41 | *
42 | * @return bool
43 | */
44 | public function isActive(string $name, array $contextValues = []): bool
45 | {
46 | $context = new Context();
47 | foreach ($contextValues as $contextKey => $contextValue) {
48 | $context->add($contextKey, $contextValue);
49 | }
50 |
51 | return $this->manager->isActive($name, $context);
52 | }
53 |
54 | /**
55 | * {@inheritDoc}
56 | */
57 | public function getFunctions(): array
58 | {
59 | return [
60 | new TwigFunction('feature', [$this, 'isActive']),
61 | ];
62 | }
63 |
64 | /**
65 | * {@inheritDoc}
66 | */
67 | public function getTests(): array
68 | {
69 | return [
70 | new TwigTest('active feature', [$this, 'isActive']),
71 | ];
72 | }
73 |
74 | /**
75 | * Returns the name
76 | * (needed for supporting twig <1.26)
77 | *
78 | * @return string
79 | */
80 | public function getName(): string
81 | {
82 | return 'flagception';
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/CompilerPass/ActivatorPassTest.php:
--------------------------------------------------------------------------------
1 |
15 | * @package Flagception\Tests\FlagceptionBundle\DependencyInjection\CompilerPass
16 | */
17 | class ActivatorPassTest extends TestCase
18 | {
19 | /**
20 | * Test process
21 | *
22 | * @return void
23 | */
24 | public function testProcess()
25 | {
26 | $container = new ContainerBuilder();
27 |
28 | $bag = $this->createMock(Definition::class);
29 | $container->setDefinition('flagception.activator.chain_activator', $bag);
30 |
31 | $bag
32 | ->expects(static::exactly(3))
33 | ->method('addMethodCall')
34 | ->withConsecutive(
35 | [static::equalTo('add'), static::equalTo([new Reference('foo')])],
36 | [static::equalTo('add'), static::equalTo([new Reference('bazz')])],
37 | [static::equalTo('add'), static::equalTo([new Reference('bar')])]
38 | );
39 |
40 | $container->setDefinition(
41 | 'foo',
42 | (new Definition(__CLASS__))->addTag('flagception.activator', [
43 | 'priority' => 255
44 | ])
45 | );
46 | $container->setDefinition(
47 | 'bar',
48 | (new Definition(__CLASS__))->addTag('flagception.activator')
49 | );
50 | $container->setDefinition(
51 | 'bazz',
52 | (new Definition(__CLASS__))->addTag('flagception.activator', [
53 | 'priority' => 25
54 | ])
55 | );
56 |
57 | (new ActivatorPass())->process($container);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/CompilerPass/ExpressionProviderPassTest.php:
--------------------------------------------------------------------------------
1 |
15 | * @package Flagception\Tests\FlagceptionBundle\DependencyInjection\CompilerPass
16 | */
17 | class ExpressionProviderPassTest extends TestCase
18 | {
19 | /**
20 | * Test process
21 | *
22 | * @return void
23 | */
24 | public function testProcess()
25 | {
26 | $container = new ContainerBuilder();
27 |
28 | $factory = $this->createMock(Definition::class);
29 | $container->setDefinition('flagception.factory.expression_language_factory', $factory);
30 |
31 | $factory
32 | ->expects(static::exactly(3))
33 | ->method('addMethodCall')
34 | ->withConsecutive(
35 | [static::equalTo('addProvider'), static::equalTo([new Reference('foo')])],
36 | [static::equalTo('addProvider'), static::equalTo([new Reference('bar')])],
37 | [static::equalTo('addProvider'), static::equalTo([new Reference('bazz')])]
38 | );
39 |
40 | $container->setDefinition(
41 | 'foo',
42 | (new Definition(__CLASS__))->addTag('flagception.expression_language_provider')
43 | );
44 | $container->setDefinition(
45 | 'bar',
46 | (new Definition(__CLASS__))->addTag('flagception.expression_language_provider')
47 | );
48 | $container->setDefinition(
49 | 'bazz',
50 | (new Definition(__CLASS__))->addTag('flagception.expression_language_provider')
51 | );
52 |
53 | (new ExpressionProviderPass())->process($container);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/CompilerPass/ContextDecoratorPassTest.php:
--------------------------------------------------------------------------------
1 |
15 | * @package Flagception\Tests\FlagceptionBundle\DependencyInjection\CompilerPass
16 | */
17 | class ContextDecoratorPassTest extends TestCase
18 | {
19 | /**
20 | * Test process
21 | *
22 | * @return void
23 | */
24 | public function testProcess()
25 | {
26 | $container = new ContainerBuilder();
27 |
28 | $bag = $this->createMock(Definition::class);
29 | $container->setDefinition('flagception.decorator.chain_decorator', $bag);
30 |
31 | $bag
32 | ->expects(static::exactly(3))
33 | ->method('addMethodCall')
34 | ->withConsecutive(
35 | [static::equalTo('add'), static::equalTo([new Reference('foo')])],
36 | [static::equalTo('add'), static::equalTo([new Reference('bazz')])],
37 | [static::equalTo('add'), static::equalTo([new Reference('bar')])]
38 | );
39 |
40 | $container->setDefinition(
41 | 'foo',
42 | (new Definition(__CLASS__))->addTag('flagception.context_decorator', [
43 | 'priority' => 255
44 | ])
45 | );
46 | $container->setDefinition(
47 | 'bar',
48 | (new Definition(__CLASS__))->addTag('flagception.context_decorator')
49 | );
50 | $container->setDefinition(
51 | 'bazz',
52 | (new Definition(__CLASS__))->addTag('flagception.context_decorator', [
53 | 'priority' => 25
54 | ])
55 | );
56 |
57 | (new ContextDecoratorPass())->process($container);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Listener/RoutingMetadataSubscriber.php:
--------------------------------------------------------------------------------
1 |
15 | * @package Flagception\Bundle\FlagceptionBundle\Listener
16 | */
17 | class RoutingMetadataSubscriber implements EventSubscriberInterface
18 | {
19 | /**
20 | * Feature key for routing annotation
21 | *
22 | * @var string
23 | */
24 | public const FEATURE_KEY = '_feature';
25 |
26 | /**
27 | * The feature manager
28 | *
29 | * @var FeatureManagerInterface
30 | */
31 | private $manager;
32 |
33 | /**
34 | * RoutingMetadataSubscriber constructor.
35 | *
36 | * @param FeatureManagerInterface $manager
37 | */
38 | public function __construct(FeatureManagerInterface $manager)
39 | {
40 | $this->manager = $manager;
41 | }
42 |
43 | /**
44 | * Filter by routing metadata
45 | *
46 | * @param ControllerEvent $event
47 | *
48 | * @return void
49 | * @throws NotFoundHttpException
50 | */
51 | public function onKernelController(ControllerEvent $event)
52 | {
53 | if (!$event->getRequest()->attributes->has(static::FEATURE_KEY)) {
54 | return;
55 | }
56 |
57 | $featureNames = (array) $event->getRequest()->attributes->get(static::FEATURE_KEY);
58 | foreach ($featureNames as $featureName) {
59 | if (!$this->manager->isActive($featureName)) {
60 | throw new NotFoundHttpException('Feature for this class is not active.');
61 | }
62 | }
63 | }
64 |
65 | /**
66 | * {@inheritdoc}
67 | */
68 | public static function getSubscribedEvents(): array
69 | {
70 | return [
71 | KernelEvents::CONTROLLER => 'onKernelController',
72 | ];
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configurator/ArrayConfigurator.php:
--------------------------------------------------------------------------------
1 |
14 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator
15 | */
16 | class ArrayConfigurator implements ActivatorConfiguratorInterface
17 | {
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public function getKey()
22 | {
23 | return 'array';
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function addActivator(ContainerBuilder $container, array $config, array $features)
30 | {
31 | if ($config['enable'] === false) {
32 | return;
33 | }
34 |
35 | $definition = new Definition(ArrayActivator::class);
36 | $definition->addArgument(
37 | array_combine(array_keys($features), array_column($features, 'default'))
38 | );
39 |
40 | $definition->addTag('flagception.activator', [
41 | 'priority' => $config['priority']
42 | ]);
43 |
44 | $container->setDefinition('flagception.activator.array_activator', $definition);
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | public function addConfiguration(ArrayNodeDefinition $node)
51 | {
52 | $node
53 | ->addDefaultsIfNotSet(['enable' => true])
54 | ->children()
55 | ->booleanNode('enable')
56 | ->beforeNormalization()
57 | ->ifString()
58 | ->then(function ($value) {
59 | return filter_var($value, FILTER_VALIDATE_BOOLEAN);
60 | })
61 | ->end()
62 | ->defaultTrue()
63 | ->end()
64 | ->integerNode('priority')
65 | ->defaultValue(255)
66 | ->end()
67 | ->end();
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/docs/activator.md:
--------------------------------------------------------------------------------
1 | Activators
2 | -------------------------
3 | We use 'activators' for resolve the feature state. This bundle contains the `ConfigActivator` which fetch
4 | your features from the config.yml. But sometimes you want to fetch the feature state from another source. eg. from a remote server. You can create your own
5 | activators with a few lines.
6 |
7 | Just create a service class, implements `FeatureActivatorInterface`, tag it with `flagception.activator` and an optional
8 | priority tag. The feature manager iterate through all activators and check the state with the `isActive` method until one activator
9 | returns true. If an activator returns true, no further activators will be requested.
10 |
11 | This bundle supports [autoconfiguration](https://symfony.com/blog/new-in-symfony-3-3-service-autoconfiguration) for `FeatureActivatorInterface` from Symfony 3.3.
12 |
13 | Example class to activate all features for admins:
14 | ```php
15 | # AdminActivator.php
16 |
17 | class AdminActivator implements FeatureActivatorInterface
18 | {
19 | /**
20 | * {@inheritdoc}
21 | */
22 | public function getName()
23 | {
24 | # Return an unqiue name for this activator
25 | return 'admin';
26 | }
27 |
28 | /**
29 | * @var string $name The requested feature name (eg. 'feature_123')
30 | * @var Context $context The context object which all key / values
31 | */
32 | public function isActive($name, Context $context)
33 | {
34 | # Always return true if the user role contain 'ROLE_ADMIN'
35 | return in_array('ROLE_ADMIN', $context->get('user_roles'), true);
36 | }
37 | }
38 | ```
39 |
40 | The service declaration:
41 | ```yml
42 | flagception.activator.config_activator:
43 | class: Flagception\Bundle\FlagceptionBundle\Activator\ConfigActivator
44 | arguments:
45 | - '@flagception.constraint.constraint_resolver'
46 | tags:
47 | - { name: flagception.activator, priority: 100 }
48 | ```
49 |
50 | Now we declare one feature in our config (ConfigActivator) and disabled it. The manager will check the ConfigActivator
51 | which return false (see config.yml). After that, the manager will call the AdminActivator - which return true for admins.
52 | ```yml
53 | # config.yml
54 |
55 | flagception:
56 | features:
57 | feature_123:
58 | default: false
59 | ```
60 |
--------------------------------------------------------------------------------
/tests/Twig/ToggleExtensionTest.php:
--------------------------------------------------------------------------------
1 |
14 | * @package Flagception\Tests\FlagceptionBundle\Twig
15 | */
16 | class ToggleExtensionTest extends TestCase
17 | {
18 | /**
19 | * Test functions
20 | *
21 | * @return void
22 | */
23 | public function testFunctions()
24 | {
25 | $extension = new ToggleExtension($this->createMock(FeatureManagerInterface::class));
26 |
27 | static::assertEquals('feature', $extension->getFunctions()[0]->getName());
28 | static::assertEquals('active feature', $extension->getTests()[0]->getName());
29 | }
30 |
31 | /**
32 | * Test is active
33 | *
34 | * @return void
35 | */
36 | public function testIsActive()
37 | {
38 | $extension = new ToggleExtension($manager = $this->createMock(FeatureManagerInterface::class));
39 |
40 | $manager
41 | ->method('isActive')
42 | ->with('feature_foo')
43 | ->willReturn(true);
44 |
45 | static::assertEquals(true, $extension->isActive('feature_foo'));
46 | }
47 |
48 | /**
49 | * Test is active with context
50 | *
51 | * @return void
52 | */
53 | public function testIsActiveWithContext()
54 | {
55 | $extension = new ToggleExtension($manager = $this->createMock(FeatureManagerInterface::class));
56 |
57 | $context = new Context();
58 | $context->add('role', 'ROLE_ADMIN');
59 |
60 | $manager
61 | ->method('isActive')
62 | ->with('feature_foo', $context)
63 | ->willReturn(true);
64 |
65 | static::assertEquals(true, $extension->isActive('feature_foo', ['role' => 'ROLE_ADMIN']));
66 | }
67 |
68 | /**
69 | * Test get name
70 | *
71 | * @return void
72 | */
73 | public function testGetName()
74 | {
75 | $extension = new ToggleExtension($this->createMock(FeatureManagerInterface::class));
76 | static::assertEquals('flagception', $extension->getName());
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/UPGRADE-3.0.md:
--------------------------------------------------------------------------------
1 | # Upgrade from 2.x to 3.0
2 | The new feature toggle bundle is a complete rework but the public interfaces doesn't change so much.
3 | It use the new [Flagception](https://packagist.org/packages/flagception/flagception) library under the hood.
4 |
5 | Generally, the namespace changed to `Flagception\` or `Flagception\Bundle\FlagceptionBundle`.
6 | So all namespaces and service id's are renamed.
7 |
8 | Additional, the composer package renamed from `bestit/feature-toggle-bundle` to `flagception/flagception-bundle`.
9 |
10 | FeatureManager
11 | ---------------------------
12 | The feature manager interface renamed from `BestIt\FeatureToggleBundle\Manager\FeatureManagerInterface` to `Flagception\Manager\FeatureManagerInterface`.
13 | The default feature manager (`Flagception\Manager\FeatureManager`) is accessible via the service id `flagception.manager.feature_manager`.
14 | Instead of bags it expects a `FeatureActivatorInterface` and a optional `ContextDecoratorInterface` as constructor argument.
15 |
16 | Stashes (Activators)
17 | ---------------------------
18 | The stashes are renamed to "activators" and implement the `Flagception\Activator\FeatureActivatorInterface` instead of the
19 | `BestIt\FeatureToggleBundle\Stash\StashInterface`.
20 |
21 | The compiler pass tag also changed from `best_it_feature_toggle.stash` to `flagception.activator`. The stash bag was removed - a `ChainActivator` holds
22 | all activator (stashes) now.
23 |
24 | Decorators
25 | ---------------------------
26 | Decorators implement the renamed `Flagception\Decorator\ContextDecoratorInterface` now (old: `BestIt\FeatureToggleBundle\Decorator\ContextDecoratorInterface`).
27 | The compiler pass tag changed from `best_it_feature_toggle.context_decorator` to `flagception.context_decorator`.
28 | The decorator bag was removed - a `ChainDecorator` holds all decorators now.
29 |
30 | Events
31 | ---------------------------
32 | All events are removed.
33 |
34 | Config
35 | ---------------------------
36 | A few fields have been renamed.
37 |
38 | For example - the old config:
39 | ```yml
40 | best_it_feature_toggle:
41 | features:
42 | feature_123:
43 | active: true
44 | ```
45 |
46 | The new config (`active` to `default`):
47 | ```yml
48 | flagception:
49 | features:
50 | feature_123:
51 | default: true
52 | ```
53 |
54 | I think every single point listed here will be more confusing than helping. Just have a look at the new and detailed [readme](README.md).
55 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize]
6 |
7 | jobs:
8 | build:
9 | strategy:
10 | matrix:
11 | php: ['8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
12 | symfony: ['5.4.50', '6.4.29', '7.4', '8.0']
13 | exclude:
14 | - php: 8.0
15 | symfony: 6.4.29
16 | - php: 8.0
17 | symfony: 7.4
18 | - php: 8.1
19 | symfony: 7.4
20 | - php: 8.0
21 | symfony: 8.0
22 | - php: 8.1
23 | symfony: 8.0
24 | - php: 8.2
25 | symfony: 8.0
26 | - php: 8.3
27 | symfony: 8.0
28 |
29 | runs-on: ubuntu-latest
30 |
31 | steps:
32 | - uses: actions/checkout@v3
33 |
34 | - uses: shivammathur/setup-php@v2
35 | with:
36 | php-version: ${{ matrix.php }}
37 | tools: composer
38 | coverage: none
39 | - name: Setup Dependencies
40 | run: composer require "symfony/symfony:${{ matrix.symfony }}" --no-update
41 | - name: Install dependencies
42 | run: composer install --prefer-dist --no-progress
43 |
44 | - name: run tests
45 | run: ./vendor/bin/simple-phpunit
46 | coverage:
47 | strategy:
48 | matrix:
49 | php: ['8.3']
50 | symfony: ['7.4.0']
51 |
52 | runs-on: ubuntu-latest
53 |
54 | steps:
55 | - uses: actions/checkout@v3
56 |
57 | - uses: shivammathur/setup-php@v2
58 | with:
59 | php-version: ${{ matrix.php }}
60 | tools: composer
61 | coverage: xdebug
62 | - name: Setup Dependencies
63 | run: composer require "symfony/symfony:${{ matrix.symfony }}" --no-update
64 | - name: Install dependencies
65 | run: composer install --prefer-dist --no-progress
66 |
67 | - name: run tests
68 | run: ./vendor/bin/simple-phpunit
69 |
70 | - name: Make code coverage badge
71 | uses: timkrase/phpunit-coverage-badge@v1.2.1
72 | with:
73 | coverage_badge_path: output/coverage.svg
74 | push_badge: false
--------------------------------------------------------------------------------
/src/DependencyInjection/Configurator/EnvironmentConfigurator.php:
--------------------------------------------------------------------------------
1 |
14 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator
15 | */
16 | class EnvironmentConfigurator implements ActivatorConfiguratorInterface
17 | {
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public function getKey()
22 | {
23 | return 'environment';
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function addActivator(ContainerBuilder $container, array $config, array $features)
30 | {
31 | if ($config['enable'] === false) {
32 | return;
33 | }
34 |
35 | $filteredFeatures = array_filter($features, function ($feature) {
36 | return $feature['env'] !== false;
37 | });
38 |
39 | $environmentVariables = [];
40 | foreach ($filteredFeatures as $name => $value) {
41 | $environmentVariables[$name] = $value['env'];
42 | }
43 |
44 | $definition = new Definition(EnvironmentActivator::class);
45 | $definition->addArgument($environmentVariables);
46 |
47 | $definition->addTag('flagception.activator', [
48 | 'priority' => $config['priority']
49 | ]);
50 |
51 | $container->setDefinition('flagception.activator.environment_activator', $definition);
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function addConfiguration(ArrayNodeDefinition $node)
58 | {
59 | $node
60 | ->addDefaultsIfNotSet(['enable' => true])
61 | ->children()
62 | ->booleanNode('enable')
63 | ->beforeNormalization()
64 | ->ifString()
65 | ->then(function ($value) {
66 | return filter_var($value, FILTER_VALIDATE_BOOLEAN);
67 | })
68 | ->end()
69 | ->defaultTrue()
70 | ->end()
71 | ->integerNode('priority')
72 | ->defaultValue(230)
73 | ->end()
74 | ->end();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configurator/ConstraintConfigurator.php:
--------------------------------------------------------------------------------
1 |
15 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator
16 | */
17 | class ConstraintConfigurator implements ActivatorConfiguratorInterface
18 | {
19 | /**
20 | * {@inheritdoc}
21 | */
22 | public function getKey()
23 | {
24 | return 'constraint';
25 | }
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | public function addActivator(ContainerBuilder $container, array $config, array $features)
31 | {
32 | if ($config['enable'] === false) {
33 | return;
34 | }
35 |
36 | $filteredFeatures = array_filter($features, function ($feature) {
37 | return $feature['constraint'] !== false;
38 | });
39 |
40 | $constraintVariables = [];
41 | foreach ($filteredFeatures as $name => $value) {
42 | $constraintVariables[$name] = $value['constraint'];
43 | }
44 |
45 | $definition = new Definition(ConstraintActivator::class);
46 | $definition->addArgument(new Reference('flagception.constraint.constraint_resolver'));
47 | $definition->addArgument($constraintVariables);
48 |
49 | $definition->addTag('flagception.activator', [
50 | 'priority' => $config['priority']
51 | ]);
52 |
53 | $container->setDefinition('flagception.activator.constraint_activator', $definition);
54 | }
55 |
56 | /**
57 | * {@inheritdoc}
58 | */
59 | public function addConfiguration(ArrayNodeDefinition $node)
60 | {
61 | $node
62 | ->addDefaultsIfNotSet(['enable' => true])
63 | ->children()
64 | ->booleanNode('enable')
65 | ->beforeNormalization()
66 | ->ifString()
67 | ->then(function ($value) {
68 | return filter_var($value, FILTER_VALIDATE_BOOLEAN);
69 | })
70 | ->end()
71 | ->defaultTrue()
72 | ->end()
73 | ->integerNode('priority')
74 | ->defaultValue(210)
75 | ->end()
76 | ->end();
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Listener/AttributeSubscriber.php:
--------------------------------------------------------------------------------
1 |
19 | * @package Flagception\Bundle\FlagceptionBundle\Listener
20 | */
21 | class AttributeSubscriber implements EventSubscriberInterface
22 | {
23 | private FeatureManagerInterface $manager;
24 |
25 | public function __construct(
26 | FeatureManagerInterface $manager,
27 | ) {
28 | $this->manager = $manager;
29 | }
30 |
31 | /**
32 | * Filter on controller / method
33 | *
34 | * @param ControllerEvent $event
35 | *
36 | * @return void
37 | *
38 | * @throws NotFoundHttpException
39 | * @throws ReflectionException
40 | */
41 | public function onKernelController(ControllerEvent $event): void
42 | {
43 | $eventController = $event->getController();
44 |
45 | $controller = is_array($eventController) === false && method_exists($eventController, '__invoke')
46 | ? [$eventController, '__invoke']
47 | : $eventController;
48 |
49 | /*
50 | * $controller passed can be either a class or a Closure.
51 | * This is not usual in Symfony2 but it may happen.
52 | * If it is a class, it comes in array format
53 | */
54 | if (!is_array($controller)) {
55 | return;
56 | }
57 |
58 | $object = new ReflectionClass($controller[0]);
59 | foreach ($object->getAttributes(Feature::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
60 | if (!$this->manager->isActive($attribute->newInstance()->name)) {
61 | throw new NotFoundHttpException('Feature for this class is not active.');
62 | }
63 | }
64 |
65 | $method = $object->getMethod($controller[1]);
66 | foreach ($method->getAttributes(Feature::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
67 | if (!$this->manager->isActive($attribute->newInstance()->name)) {
68 | throw new NotFoundHttpException('Feature for this method is not active.');
69 | }
70 | }
71 | }
72 |
73 | /**
74 | * {@inheritdoc}
75 | */
76 | public static function getSubscribedEvents(): array
77 | {
78 | return [
79 | KernelEvents::CONTROLLER => 'onKernelController',
80 | ];
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/docs/cookie.md:
--------------------------------------------------------------------------------
1 | Cookies
2 | -------------------------
3 | You can test your features with cookies. This is by default
4 | disabled - so you have to enabled it in your config. You can also set a cookie name and a separator.
5 |
6 | ```yml
7 | # config.yml
8 |
9 | flagception:
10 | features:
11 | feature_123:
12 | default: false
13 |
14 | activators:
15 | # Cookie settings
16 | cookie:
17 |
18 | # Enable cookie activator (default: false)
19 | enable: true
20 |
21 | # Cookie name - should be a secret key (default: 'flagception')
22 | name: 'flagception'
23 |
24 | # Cookie value separator for using with mutiple features (default: ',')
25 | separator: ','
26 | ```
27 |
28 | Now you can set a cookie (eg. in chrome, firefox etc) and set the feature names (with separator) as value:
29 |
30 | 
31 |
32 | No matter what is set in the config - if the feature name exists in the cookie, the feature is enabled for you. Notice that features
33 | can only be enabled by cookie if they exists in your config.yml. In addition, you can explicitly disable some features for cookies:
34 |
35 | ```yml
36 | # config.yml
37 |
38 | flagception:
39 | features:
40 |
41 | # Activatable via cookie
42 | feature_123:
43 | default: false
44 |
45 | # Not activatable via cookie
46 | feature_456:
47 | cookie: false
48 |
49 | # Feature "feature_wyz" isn't activatable via cookie because the feature isn't defined in your config.yml
50 |
51 | # Cookie settings
52 | cookie:
53 |
54 | # Enable cookie activator (default: false)
55 | enable: true
56 |
57 | # Cookie name - should be a secret key (default: 'flagception')
58 | name: 'flagception'
59 |
60 | # Cookie value separator for using with mutiple features (default: ',')
61 | separator: ','
62 | ```
63 |
64 | Normally this acts as whitelist. That means, that only features listed in your config.yml can be enabled
65 | by cookie (due security reasons). But you can change this behavior to a blacklist. Then you can enable
66 | every feature by cookie unless you explicitly disabled it in your config.yml:
67 |
68 | ```yml
69 | # config.yml
70 |
71 | flagception:
72 | features:
73 |
74 | # Activatable via cookie
75 | feature_123:
76 | default: false
77 |
78 | # Not activatable via cookie
79 | feature_456:
80 | cookie: false
81 |
82 | # Feature "feature_wyz" isn't defined in this config.yml but is activatable by cookie
83 |
84 | # Cookie settings
85 | cookie:
86 | enable: true
87 | mode: 'blacklist'
88 |
89 | ```
90 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configurator/CookieConfigurator.php:
--------------------------------------------------------------------------------
1 |
14 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator
15 | */
16 | class CookieConfigurator implements ActivatorConfiguratorInterface
17 | {
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public function getKey()
22 | {
23 | return 'cookie';
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function addActivator(ContainerBuilder $container, array $config, array $features)
30 | {
31 | if ($config['enable'] === false) {
32 | return;
33 | }
34 |
35 | $cookieFeatures = [];
36 |
37 | // Add only features which are allowed
38 | if ($config['mode'] === CookieActivator::WHITELIST) {
39 | $cookieFeatures = array_keys(array_filter($features, function ($feature) {
40 | return $feature['cookie'] === true;
41 | }));
42 | }
43 |
44 | // Add only features which are disallowed
45 | if ($config['mode'] === CookieActivator::BLACKLIST) {
46 | $cookieFeatures = array_keys(array_filter($features, function ($feature) {
47 | return $feature['cookie'] === false;
48 | }));
49 | }
50 |
51 | $definition = new Definition(CookieActivator::class);
52 | $definition->addArgument($cookieFeatures);
53 | $definition->addArgument($config['name']);
54 | $definition->addArgument($config['separator']);
55 | $definition->addArgument($config['mode']);
56 |
57 | $definition->addTag('flagception.activator', [
58 | 'priority' => $config['priority']
59 | ]);
60 |
61 | $container->setDefinition('flagception.activator.cookie_activator', $definition);
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | public function addConfiguration(ArrayNodeDefinition $node)
68 | {
69 | $node
70 | ->children()
71 | ->booleanNode('enable')
72 | ->beforeNormalization()
73 | ->ifString()
74 | ->then(function ($value) {
75 | return filter_var($value, FILTER_VALIDATE_BOOLEAN);
76 | })
77 | ->end()
78 | ->defaultFalse()
79 | ->end()
80 | ->integerNode('priority')
81 | ->defaultValue(200)
82 | ->end()
83 | ->scalarNode('name')
84 | ->defaultValue('flagception')
85 | ->cannotBeEmpty()
86 | ->end()
87 | ->scalarNode('separator')
88 | ->defaultValue(',')
89 | ->cannotBeEmpty()
90 | ->end()
91 | ->enumNode('mode')
92 | ->values([CookieActivator::WHITELIST, CookieActivator::BLACKLIST])
93 | ->defaultValue(CookieActivator::WHITELIST)
94 | ->end()
95 | ->end();
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/DependencyInjection/FlagceptionExtension.php:
--------------------------------------------------------------------------------
1 |
19 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection
20 | */
21 | class FlagceptionExtension extends Extension
22 | {
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public function load(array $configs, ContainerBuilder $container): void
27 | {
28 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
29 | $loader->load('configurators.yml');
30 | $loader->load('services.yml');
31 |
32 | $configurators = $this->getConfigurators($container);
33 | $configuration = new Configuration($configurators);
34 | $config = $this->processConfiguration($configuration, $configs);
35 |
36 | // Enable / disable routing metadata subscriber
37 | if ($config['routing_metadata']['enable'] === false) {
38 | $container->removeDefinition('flagception.listener.routing_metadata_subscriber');
39 | }
40 |
41 | // Enable / disable activators
42 | foreach ($configurators as $name => $configurator) {
43 | if (!array_key_exists($name, $config['activators'])) {
44 | continue;
45 | }
46 |
47 | $configurator->addActivator($container, $config['activators'][$name], $config['features']);
48 | }
49 |
50 | if ($container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug')) {
51 | $chainDefinition = $container->getDefinition('flagception.activator.chain_activator');
52 | $chainDefinition->setClass(TraceableChainActivator::class);
53 | }
54 |
55 | if (method_exists($container, 'registerForAutoconfiguration') === true) {
56 | $container
57 | ->registerForAutoconfiguration(FeatureActivatorInterface::class)
58 | ->addTag('flagception.activator');
59 |
60 | $container
61 | ->registerForAutoconfiguration(ContextDecoratorInterface::class)
62 | ->addTag('flagception.context_decorator');
63 | }
64 | }
65 |
66 | public function getConfiguration(array $config, ContainerBuilder $container): Configuration
67 | {
68 | return new Configuration($this->getConfigurators($container));
69 | }
70 |
71 | /**
72 | * Get configurators
73 | *
74 | * @param ContainerBuilder $container
75 | *
76 | * @return ActivatorConfiguratorInterface[]
77 | *
78 | * @throws Exception
79 | */
80 | private function getConfigurators(ContainerBuilder $container): array
81 | {
82 | $configurators = [];
83 |
84 | $services = $container->findTaggedServiceIds('flagception.configurator');
85 | foreach (array_keys($services) as $id) {
86 | $configurator = $container->get($id);
87 | $configurators[str_replace('-', '_', $configurator->getKey())] = $configurator;
88 | }
89 |
90 | return $configurators;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Resources/config/services.yml:
--------------------------------------------------------------------------------
1 | services:
2 | flagception.manager.feature_manager:
3 | class: Flagception\Manager\FeatureManager
4 | arguments:
5 | - '@flagception.activator.chain_activator'
6 | - '@flagception.decorator.chain_decorator'
7 | public: true
8 |
9 | Flagception\Manager\FeatureManagerInterface: '@flagception.manager.feature_manager'
10 |
11 | flagception.expression_language:
12 | class: Symfony\Component\ExpressionLanguage\ExpressionLanguage
13 | factory: ['@flagception.factory.expression_language_factory', 'create']
14 | public: false
15 |
16 | flagception.twig.toggle_extension:
17 | class: Flagception\Bundle\FlagceptionBundle\Twig\ToggleExtension
18 | arguments:
19 | - '@flagception.manager.feature_manager'
20 | tags:
21 | - { name: twig.extension }
22 | public: false
23 |
24 | flagception.profiler.feature_data_collector:
25 | class: Flagception\Bundle\FlagceptionBundle\Profiler\FeatureDataCollector
26 | arguments:
27 | - '@flagception.activator.chain_activator'
28 | - '@flagception.decorator.chain_decorator'
29 | tags:
30 | -
31 | name: data_collector
32 | template: '@Flagception/profiler/layout.html.twig'
33 | id: 'flagception'
34 | public: false
35 |
36 | flagception.constraint.constraint_resolver:
37 | class: Flagception\Constraint\ConstraintResolver
38 | arguments:
39 | - '@flagception.expression_language'
40 | public: false
41 |
42 | flagception.factory.expression_language_factory:
43 | class: Flagception\Factory\ExpressionLanguageFactory
44 | public: false
45 |
46 | flagception.constraint_provider.date_provider:
47 | class: Flagception\Constraint\Provider\DateProvider
48 | tags:
49 | - { name: flagception.expression_language_provider }
50 | public: false
51 |
52 | flagception.constraint_provider.match_provider:
53 | class: Flagception\Constraint\Provider\MatchProvider
54 | tags:
55 | - { name: flagception.expression_language_provider }
56 | public: false
57 |
58 | flagception.activator.chain_activator:
59 | class: Flagception\Activator\ChainActivator
60 | public: false
61 |
62 | flagception.decorator.chain_decorator:
63 | class: Flagception\Decorator\ChainDecorator
64 | public: false
65 |
66 | # Maybe removed by your configuration
67 | flagception.listener.routing_metadata_subscriber:
68 | class: Flagception\Bundle\FlagceptionBundle\Listener\RoutingMetadataSubscriber
69 | arguments:
70 | - '@flagception.manager.feature_manager'
71 | tags:
72 | - { name: kernel.event_subscriber }
73 | public: true
74 |
75 | flagception.listener.attribute_subscriber:
76 | class: Flagception\Bundle\FlagceptionBundle\Listener\AttributeSubscriber
77 | arguments:
78 | - '@flagception.manager.feature_manager'
79 | tags:
80 | - { name: kernel.event_subscriber }
81 | public: true
82 |
83 | flagception.route.is_feature:
84 | class: Flagception\Bundle\FlagceptionBundle\Route\IsFeature
85 | arguments:
86 | - '@flagception.manager.feature_manager'
87 |
88 | flagception.route.expression_language_function.is_feature:
89 | class: \Closure
90 | factory: [ \Closure, 'fromCallable' ]
91 | arguments: [ [ '@flagception.route.is_feature', '__invoke' ] ]
92 | tags:
93 | - { name: 'routing.expression_language_function', function: 'is_feature' }
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flagception
2 | **Feature toggle bundle on steroids!** Flagception is a simple and powerful feature toggle system for php.
3 | This bundle integrates the [Flagception PHP Library](https://packagist.org/packages/flagception/flagception) for symfony 2.7 to 7.* (and php 5.6 to php 8.*).
4 |
5 | [](https://packagist.org/packages/flagception/flagception-bundle)
6 | 
7 | [](https://github.com/playox/flagception-bundle/actions)
8 | [](https://packagist.org/packages/flagception/flagception-bundle)
9 | [](https://packagist.org/packages/flagception/flagception-bundle)
10 |
11 | | Bundle Version (Tag) | Support Symfony |
12 | |----------------------|-----------------|
13 | | <=3 | 2.7 - 4.4 |
14 | | >=4 | 4.4 - 7.4 |
15 | | >=5 | 4.4 - 7.4 |
16 | | >=6 | 4.4 - current |
17 |
18 | ```console
19 | $ composer require flagception/flagception-bundle
20 | ```
21 |
22 | Documentation
23 | ---------------------------
24 | * [Installation](docs/install.md)
25 | * [Upgrade from 2.x](UPGRADE-3.0.md)
26 | * [Upgrade from 3.x](UPGRADE-4.0.md)
27 | * [Upgrade from 4.x](UPGRADE-5.0.md)
28 | * [Upgrade from 5.x](UPGRADE-6.0.md)
29 | * [Usage](docs/usage.md)
30 | * [Twig flags](docs/twig.md)
31 | * [Route flags](docs/route.md)
32 | * [Route condition flags](docs/route_condition.md)
33 | * [Attribute flags](docs/attribute.md)
34 | * [Annotation flags](docs/annotation.md)
35 | * [Constraints](docs/constraint.md)
36 | * [Environment variables](docs/environment.md)
37 | * [Cookies](docs/cookie.md)
38 | * [Database](docs/database.md)
39 | * [Activators](docs/activator.md)
40 | * [Profiler](docs/profiler.md)
41 |
42 | Quick example
43 | ---------------------------
44 | Set some feature in your config (or use your own [activator](docs/activator.md) for fetching features from wherever you want) ...
45 |
46 | ```yml
47 | flagception:
48 |
49 | # Your Features (optional you left it empty)
50 | features:
51 |
52 | # Feature name as key
53 | feature_123:
54 | # Default flag if inactive or active (default: false)
55 | default: true
56 |
57 | # Feature state from an environment variable
58 | feature_abc:
59 | env: FEATURE_ENV_ABC
60 |
61 | # Feature with constraint (active if user id is 12 OR it is between 8 am and 6 pm)
62 | feature_def:
63 | constraint: 'user_id == 12 or (date("H") > 8 and date("H") < 18)'
64 |
65 | # All togther (chain)
66 | feature_def:
67 | default: false
68 | env: FEATURE_ENV_ABC
69 | constraint: 'user_id == 12 or (date("H") > 8 and date("H") < 18)'
70 | ```
71 |
72 | ... and use it in controller, services or twig:
73 |
74 | ```twig
75 | {% if feature('feature_123') %}
76 | {# Execute if feature is active ... #}
77 | {% endif %}
78 | ```
79 |
80 | See [usage documentation](docs/usage.md) for detailed examples.
81 |
82 | Profiler
83 | ---------------------------
84 | This bundle ships a profiler tab, where you can see how often a feature was requested, which results it returns (active or inactive) and
85 | the given context.
86 |
87 | 
88 |
89 | Credits
90 | -------------------------
91 | Profiler icon from https://github.com/tabler/tabler-icons
92 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
15 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection
16 | */
17 | class Configuration implements ConfigurationInterface
18 | {
19 | /**
20 | * Configurators
21 | *
22 | * @var ActivatorConfiguratorInterface[]
23 | */
24 | private $configurators;
25 |
26 | /**
27 | * Configuration constructor.
28 | *
29 | * @param ActivatorConfiguratorInterface[] $configurators
30 | */
31 | public function __construct(array $configurators)
32 | {
33 | $this->configurators = $configurators;
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function getConfigTreeBuilder(): TreeBuilder
40 | {
41 | $treeBuilder = new TreeBuilder('flagception');
42 | $rootNode = $treeBuilder->getRootNode();
43 |
44 | $rootNode
45 | ->children()
46 | ->arrayNode('features')
47 | ->useAttributeAsKey('name')
48 | ->prototype('array')
49 | ->children()
50 | ->scalarNode('default')
51 | ->defaultFalse()
52 | ->end()
53 | ->scalarNode('env')
54 | ->defaultFalse()
55 | ->end()
56 | ->booleanNode('cookie')
57 | ->beforeNormalization()
58 | ->ifString()
59 | ->then(function ($value) {
60 | return filter_var($value, FILTER_VALIDATE_BOOLEAN);
61 | })
62 | ->end()
63 | ->defaultTrue()
64 | ->end()
65 | ->scalarNode('constraint')
66 | ->defaultFalse()
67 | ->end()
68 | ->end()
69 | ->end()
70 | ->defaultValue([])
71 | ->end()
72 | ->arrayNode('routing_metadata')
73 | ->addDefaultsIfNotSet()
74 | ->children()
75 | ->booleanNode('enable')
76 | ->beforeNormalization()
77 | ->ifString()
78 | ->then(function ($value) {
79 | return filter_var($value, FILTER_VALIDATE_BOOLEAN);
80 | })
81 | ->end()
82 | ->defaultTrue()
83 | ->end()
84 | ->end()
85 | ->end()
86 | ->append($this->appendActivators())
87 | ->end();
88 |
89 | return $treeBuilder;
90 | }
91 |
92 | /**
93 | * Add activators
94 | *
95 | * @return NodeDefinition
96 | */
97 | public function appendActivators(): NodeDefinition
98 | {
99 | $builder = new TreeBuilder('activators');
100 | $node = $builder->getRootNode();
101 | $node->addDefaultsIfNotSet();
102 |
103 | $activatorNodeBuilder = $node->children();
104 |
105 | foreach ($this->configurators as $name => $configurator) {
106 | $configuratorNode = $activatorNodeBuilder->arrayNode($name)->canBeUnset();
107 | $configurator->addConfiguration($configuratorNode);
108 | }
109 |
110 | return $node;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/FlagceptionExtensionTest.php:
--------------------------------------------------------------------------------
1 |
17 | * @package Flagception\Tests\FlagceptionBundle\DependencyInjection
18 | */
19 | class FlagceptionExtensionTest extends TestCase
20 | {
21 | /**
22 | * The container
23 | *
24 | * @var ContainerBuilder
25 | */
26 | private $container;
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function setUp(): void
32 | {
33 | $container = new ContainerBuilder();
34 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../../src/Resources/config'));
35 | $loader->load('configurators.yml');
36 |
37 | $this->container = $container;
38 | }
39 |
40 | /**
41 | * Test that routing metadata subscriber is disabled
42 | *
43 | * @return void
44 | */
45 | public function testRoutingMetadataSubscriberDisabled()
46 | {
47 | $config = [
48 | [
49 | 'routing_metadata' => [
50 | 'enable' => false
51 | ]
52 | ]
53 | ];
54 |
55 | $extension = new FlagceptionExtension();
56 | $extension->load($config, $this->container);
57 |
58 | static::assertFalse($this->container->hasDefinition('flagception.listener.routing_metadata_subscriber'));
59 | }
60 |
61 | /**
62 | * Test that routing metadata subscriber is enabled
63 | *
64 | * @return void
65 | */
66 | public function testRoutingMetadataSubscriberEnabled()
67 | {
68 | $config = [];
69 |
70 | $extension = new FlagceptionExtension();
71 | $extension->load($config, $this->container);
72 |
73 | $definition = $this->container->getDefinition('flagception.listener.routing_metadata_subscriber');
74 | static::assertTrue($definition->hasTag('kernel.event_subscriber'));
75 | }
76 |
77 | /**
78 | * Test that routing metadata subscriber is enabled by string
79 | *
80 | * @return void
81 | */
82 | public function testRoutingMetadataSubscriberEnabledByString()
83 | {
84 | $config = [
85 | [
86 | 'routing_metadata' => [
87 | 'enable' => 'true'
88 | ]
89 | ]
90 | ];
91 |
92 | $extension = new FlagceptionExtension();
93 | $extension->load($config, $this->container);
94 |
95 | $definition = $this->container->getDefinition('flagception.listener.routing_metadata_subscriber');
96 | static::assertTrue($definition->hasTag('kernel.event_subscriber'));
97 | }
98 |
99 | /**
100 | * Test that annotation subscriber is disabled
101 | *
102 | * @return void
103 | */
104 | public function testAutConfiguration()
105 | {
106 | if (method_exists($this->container, 'registerForAutoconfiguration') === false) {
107 | $this->markTestSkipped('Only since Symfony 3.3');
108 | }
109 |
110 | $config = [];
111 |
112 | $extension = new FlagceptionExtension();
113 | $extension->load($config, $this->container);
114 |
115 | $activatorChildDefinition = $this->container->getAutoconfiguredInstanceof()[FeatureActivatorInterface::class];
116 | static::assertEquals([
117 | 'flagception.activator' => [[]]
118 | ], $activatorChildDefinition->getTags());
119 |
120 | $contextChildDefinition = $this->container->getAutoconfiguredInstanceof()[ContextDecoratorInterface::class];
121 | static::assertEquals([
122 | 'flagception.context_decorator' => [[]]
123 | ], $contextChildDefinition->getTags());
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/docs/usage.md:
--------------------------------------------------------------------------------
1 | Quickstart
2 | -------------------------
3 | You can define your features in your config files. Just give your feature a name and the active or inactive flag.
4 |
5 | Minimal example with two features:
6 |
7 | ```yml
8 | flagception:
9 |
10 | # Your Features (optional you left it empty)
11 | features:
12 |
13 | # Feature name as key
14 | feature_123:
15 | # Default flag if inactive or active (default: false)
16 | default: true
17 |
18 | feature_abc:
19 | default: false
20 | ```
21 |
22 | Now you can check the feature state in twig templates, controllers or services.
23 |
24 | ##### Twig usage
25 | ```twig
26 | {% if feature('feature_123') %}
27 | {# Execute if feature is active ... #}
28 | {% endif %}
29 | ```
30 |
31 | ##### Service usage
32 | ```php
33 | # FooService.php
34 |
35 | class FooService
36 | {
37 | /**
38 | * @var FeatureManagerInterface
39 | */
40 | private $manager;
41 |
42 | /**
43 | * @param FeatureManagerInterface $manager
44 | * Service id: flagception.manager.feature_manager
45 | */
46 | public function __construct(FeatureManagerInterface $manager)
47 | {
48 | $this->manager = $manager;
49 | }
50 |
51 | public function do()
52 | {
53 | // ...
54 | if ($this->manager->isActive('feature_123')) {
55 | // ...
56 | }
57 | // ...
58 | }
59 | }
60 | ```
61 |
62 | ##### Controller usage
63 | ```php
64 | // src/AppBundle/Controller/BlogController.php
65 | namespace AppBundle\Controller;
66 |
67 | use Symfony\Bundle\FrameworkBundle\Controller\Controller;
68 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
69 |
70 | class BlogController extends Controller
71 | {
72 | /**
73 | * @Route("/blog/{page}", name="blog_list", defaults={"_feature": "feature_123"})
74 | */
75 | public function listAction($page)
76 | {
77 | // ...
78 | }
79 |
80 | /**
81 | * @Route("/blog/{slug}", name="blog_show")
82 | */
83 | public function showAction($slug)
84 | {
85 | // ...
86 | }
87 | }
88 | ```
89 |
90 | ##### Attribute usage
91 | ```php
92 | # FooController.php
93 |
94 | use Flagception\Bundle\FlagceptionBundle\Attribute\Feature;
95 |
96 | #[Feature("feature_123")]
97 | class FooController
98 | {
99 | #[Feature("feature_789")]
100 | public function barAction()
101 | {
102 | }
103 |
104 | public function fooAction()
105 | {
106 | }
107 | }
108 | ```
109 |
110 | Take a look to the detail documentation for [Twig](twig.md), [Route](route.md) or [Attribute](attribute.md) usage.
111 |
112 | ##### Feature names
113 | You can name your features as you like. But we recommend using [snake case](https://en.wikipedia.org/wiki/Snake_case).
114 | Especially because Symfony normalizes values from your YML or XML into snake case (see [here](http://symfony.com/doc/current/components/config/definition.html#normalization)).
115 | Your feature "flag-a" becomes "flag_a". So you have to check for "flag_a" everywhere in your code, even if you've
116 | actually maintained "flag-a" in your YML / XML. Consider this with your feature naming.
117 |
118 | Constraint usage
119 | -------------------------
120 | In some cases will you need more instead of a simple true / false. So you can define constraints to enable or disable a feature.
121 | A constraint should return true or false.
122 |
123 | An example:
124 |
125 | ```yml
126 | # config.yml
127 |
128 | flagception:
129 | features:
130 |
131 | # This feature will only be active, if the current user has id 12
132 | feature_123:
133 | default: false
134 | constraint: 'user_id === 12'
135 |
136 | # This feature will only be active, if the user_role array contains "ROLE_ADMIN"
137 | feature_abc:
138 | default: false
139 | constraint: '"ROLE_ADMIN" in user_role'
140 |
141 | # This feature will only be active between 8am and 6pm.
142 | # OR if the user_role array contains "ROLE_ADMIN"
143 | feature_abc:
144 | default: false
145 | constraint: '(date("H") > 8 and date("H") < 18) or "ROLE_ADMIN" in user_role'
146 | ```
147 |
148 | You can extend constraints with your own variables and functions. Read the [constraint documentation](constraint.md) for more details.
149 |
--------------------------------------------------------------------------------
/tests/Activator/TraceableChainActivatorTest.php:
--------------------------------------------------------------------------------
1 |
18 | * @package Flagception\Tests\FlagceptionBundle\Activator
19 | */
20 | class TraceableChainActivatorTest extends TestCase
21 | {
22 | /**
23 | * Test implement interface
24 | *
25 | * @return void
26 | */
27 | public function testImplementInterface()
28 | {
29 | $activator = new TraceableChainActivator();
30 | static::assertInstanceOf(FeatureActivatorInterface::class, $activator);
31 | }
32 |
33 | /**
34 | * Test extends chain activator
35 | *
36 | * @return void
37 | */
38 | public function testExtendsChainActivator()
39 | {
40 | $activator = new TraceableChainActivator();
41 | static::assertInstanceOf(ChainActivator::class, $activator);
42 | }
43 |
44 | /**
45 | * Test name
46 | *
47 | * @return void
48 | */
49 | public function testName()
50 | {
51 | $activator = new TraceableChainActivator();
52 | static::assertEquals('chain', $activator->getName());
53 | }
54 |
55 | /**
56 | * Test with no activators
57 | *
58 | * @return void
59 | */
60 | public function testNoActivators()
61 | {
62 | $activator = new TraceableChainActivator();
63 | static::assertFalse($activator->isActive('feature_abc', new Context()));
64 | }
65 |
66 | /**
67 | * Test no activators return true
68 | *
69 | * @return void
70 | */
71 | public function testNoActivatorsReturnTrue()
72 | {
73 | $activator = new TraceableChainActivator();
74 | $activator->add(new ArrayActivator([
75 | 'feature_def'
76 | ]));
77 | $activator->add(new CookieActivator([
78 | 'feature_ghi'
79 | ]));
80 |
81 | static::assertFalse($activator->isActive('feature_abc', $context = new Context()));
82 |
83 | static::assertEquals([
84 | [
85 | 'feature' => 'feature_abc',
86 | 'context' => $context,
87 | 'result' => false,
88 | 'stack' => [
89 | 'array' => false,
90 | 'cookie' => false
91 | ]
92 | ]
93 | ], $activator->getTrace());
94 | }
95 |
96 | /**
97 | * Test one activator return true
98 | *
99 | * @return void
100 | */
101 | public function testOneActivatorsReturnTrue()
102 | {
103 | $activator = new TraceableChainActivator();
104 | $activator->add(new CookieActivator([
105 | 'feature_def'
106 | ]));
107 | $activator->add(new ArrayActivator([
108 | 'feature_abc'
109 | ]));
110 | $activator->add(new EnvironmentActivator([
111 | 'feature_hij'
112 | ]));
113 |
114 | static::assertTrue($activator->isActive('feature_abc', $context = new Context()));
115 |
116 | static::assertEquals([
117 | [
118 | 'feature' => 'feature_abc',
119 | 'context' => $context,
120 | 'result' => true,
121 | 'stack' => [
122 | 'cookie' => false,
123 | 'array' => true
124 | ]
125 | ]
126 | ], $activator->getTrace());
127 | }
128 |
129 | /**
130 | * Test add and get activators
131 | *
132 | * @return void
133 | */
134 | public function testAddAndGet()
135 | {
136 | $decorator = new TraceableChainActivator();
137 | $decorator->add($fakeActivator1 = new ArrayActivator());
138 | $decorator->add($fakeActivator2 = new ArrayActivator([]));
139 |
140 | // Should be the same sorting
141 | static::assertSame($fakeActivator1, $decorator->getActivators()[0]);
142 | static::assertSame($fakeActivator2, $decorator->getActivators()[1]);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/docs/database.md:
--------------------------------------------------------------------------------
1 | Database
2 | -------------------------
3 | You can manage your feature toggles via a database. The following database vendors are currently supported:
4 | * MySQL
5 | * Oracle
6 | * Microsoft SQL Server
7 | * PostgreSQL
8 | * SAP Sybase SQL Anywhere
9 | * SQLite
10 |
11 | The bundle will use the [database activator](https://packagist.org/packages/flagception/database-activator).
12 |
13 | Download the the activator
14 | ---------------------------
15 | The `DatabaseActivator` is not included by default. Therefore, you must first insert this via Composer as a dependency.
16 |
17 | Open a command console, enter your project directory and execute the
18 | following command to download the latest stable version of this bundle:
19 |
20 | ```console
21 | $ composer require flagception/database-activator
22 | ```
23 |
24 | This command requires you to have Composer installed globally, as explained
25 | in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
26 | of the Composer documentation.
27 |
28 | Enable the activator
29 | -------------------------
30 | Then, enable the activator in your config and set database credentials.
31 |
32 | ```yml
33 | # config.yml
34 |
35 | flagception:
36 | activators:
37 | database:
38 |
39 | # Enable database activator (default: false)
40 | enable: true
41 |
42 | # Connection string
43 | url: 'pdo-mysql://user:secret@localhost/mydb'
44 | ```
45 |
46 | You can fill a connection string (url), a PDO instance, a DBAL instance or old-fashioned the individual
47 | credentials fields.
48 |
49 | ###### Connection string:
50 | ```yml
51 | # config.yml
52 |
53 | flagception:
54 | activators:
55 | database:
56 |
57 | # Enable database activator (default: false)
58 | enable: true
59 |
60 | # Connection string
61 | url: 'pdo-mysql://user:secret@localhost/mydb'
62 | ```
63 |
64 | ###### DBAL instance:
65 | ```yml
66 | # config.yml
67 |
68 | flagception:
69 | activators:
70 | database:
71 |
72 | # Enable database activator (default: false)
73 | enable: true
74 |
75 | # By dbal instance
76 | dbal: 'dbal.service.id'
77 | ```
78 |
79 | ###### Credential fields:
80 | ```yml
81 | # config.yml
82 |
83 | flagception:
84 | activators:
85 | database:
86 |
87 | # Enable database activator (default: false)
88 | enable: true
89 |
90 | # By credentials field
91 | credentials:
92 | dbname: 'mydb',
93 | user: 'user',
94 | password: 'secret', # You can use env too (%env(MYSQL_DATABASE)%)
95 | host: 'localhost',
96 | driver: 'pdo_mysql'
97 | ```
98 |
99 | Table
100 | -------------------------
101 | The `DatabaseActivator` will automatically create a table for storing the feature states if this not already exists.
102 | By default, the table name is `flagception_features` which contains the columns `feature` and `state`.
103 |
104 | You can change the tables and / or column names in your option config:
105 | ```yml
106 | # config.yml
107 |
108 | flagception:
109 | activators:
110 | database:
111 |
112 | # Enable database activator (default: false)
113 | enable: true
114 |
115 | # Connection string
116 | url: 'mysql://user:secret@localhost/mydb'
117 |
118 | # Rename table and columns
119 | options:
120 | db_table: 'my_table'
121 | db_column_feature: 'my_cool_feature_name'
122 | db_column_state: 'my_current_feature_state'
123 | ```
124 |
125 | Enable caching
126 | -------------------------
127 | This queries status from a database, which _can_ negatively impact performance. Therefore, you can set up a cache for
128 | this activator. Identical feature queries are then loaded from the cache instead of being retrieved from the
129 | database. All you have to do is specify a cache pool and cache interval.
130 |
131 | By default, the cache is disabled.
132 |
133 | Example:
134 |
135 | ```yml
136 | # config.yml
137 |
138 | flagception:
139 | activators:
140 | database:
141 | enable: true
142 | # ...
143 | cache:
144 |
145 | # Enable the cache option (default: false)
146 | enable: true
147 |
148 | # Set cache pool (default: cache.app)
149 | pool: cache.app
150 |
151 | # Set lifetime for cache in seconds (default: 3600)
152 | lifetime: 3600
153 | ```
154 |
--------------------------------------------------------------------------------
/docs/route.md:
--------------------------------------------------------------------------------
1 | Route
2 | -------------------------
3 | You can use route attributes for checking the feature state in controllers. This is per default activated.
4 | You can enable or disable it via the config.
5 |
6 | ```yml
7 | # config.yml
8 |
9 | flagception:
10 | features:
11 | feature_123:
12 | default: true
13 |
14 | # Use route attributes? (optional)
15 | routing_metadata:
16 |
17 | # Enable controller annotation (default: true)
18 | enable: true
19 | ```
20 |
21 | If route metadata is enabled, you can define the feature name in your route attributes.
22 | A `NotFoundHttpException` will be thrown if you request an action or class with inactive feature flag.
23 |
24 | ```php
25 | // src/AppBundle/Controller/BlogController.php
26 | // src/Controller/BlogController.php
27 | namespace AppBundle\Controller;
28 |
29 | use Symfony\Bundle\FrameworkBundle\Controller\Controller;
30 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
31 |
32 | class BlogController extends Controller
33 | {
34 | /**
35 | * @Route("/blog/{page}", defaults={"_feature": "feature_123"})
36 | */
37 | public function listAction($page)
38 | {
39 | // ...
40 | }
41 |
42 | /**
43 | * @Route("/blog/{slug}")
44 | */
45 | public function showAction($slug)
46 | {
47 | // ...
48 | }
49 | }
50 | ```
51 | or via yml
52 |
53 | ```yml
54 | # app/config/routing.yml
55 | blog_list:
56 | path: /blog/{page}
57 | defaults: { _controller: AppBundle:Blog:list, _feature: 'feature_789' }
58 |
59 | # Symfony 3.4 / 4.0
60 | blog_list:
61 | path: /blog/{page}
62 | controller: AppBundle:Blog:list
63 | defaults: { _feature: 'feature_789' }
64 |
65 | blog_show:
66 | ```
67 |
68 | or via xml
69 |
70 | ```xml
71 |
72 |
73 |
77 |
78 |
79 | AppBundle:Blog:list
80 | feature_123
81 |
82 |
83 |
84 |
85 | AppBundle:Blog:list
86 | feature_123
87 |
88 |
89 |
90 |
91 | ```
92 |
93 | To check for multiple features, pass them as an array to the route definition.
94 |
95 | ```php
96 | // src/AppBundle/Controller/BlogController.php
97 | // src/Controller/BlogController.php
98 | namespace AppBundle\Controller;
99 |
100 | use Symfony\Bundle\FrameworkBundle\Controller\Controller;
101 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
102 |
103 | class BlogController extends Controller
104 | {
105 | /**
106 | * @Route("/blog/{page}", defaults={"_feature": {"feature_123", "feature_456"}})
107 | */
108 | public function listAction($page)
109 | {
110 | // ...
111 | }
112 |
113 | /**
114 | * @Route("/blog/{slug}")
115 | */
116 | public function showAction($slug)
117 | {
118 | // ...
119 | }
120 | }
121 | ```
122 | or via yml
123 |
124 | ```yml
125 | # app/config/routing.yml
126 | blog_list:
127 | path: /blog/{page}
128 | defaults: { _controller: AppBundle:Blog:list, _feature: ['feature_456', 'feature_789'] }
129 |
130 | # Symfony 3.4 / 4.0
131 | blog_list:
132 | path: /blog/{page}
133 | controller: AppBundle:Blog:list
134 | defaults: { _feature: ['feature_456', 'feature_789'] }
135 |
136 | blog_show:
137 | ```
138 |
139 | or via xml
140 |
141 | ```xml
142 |
143 |
144 |
148 |
149 |
150 | AppBundle:Blog:list
151 |
152 |
153 | feature_123
154 | feature_456
155 |
156 |
157 |
158 |
159 |
160 |
161 | AppBundle:Blog:list
162 |
163 |
164 | feature_123
165 | feature_456
166 |
167 |
168 |
169 |
170 |
171 |
172 | ```
173 |
--------------------------------------------------------------------------------
/docs/constraint.md:
--------------------------------------------------------------------------------
1 | Constraint
2 | -------------------------
3 | You have to defining variables and functions if you want to use it in your constraint.
4 | You can define variables locally or globally. We recommend to define variables always globally.
5 |
6 | We use the symfony [expression language](https://symfony.com/doc/current/components/expression_language.html) for
7 | parsing the constraints.
8 |
9 | ##### Define locally variable
10 | Just fill the second argument of your twig or service method with an array. Beware that the variable only exists
11 | for this one feature request.
12 |
13 | Given we use following constraint:
14 | ```yml
15 | # config.yml
16 |
17 | flagception:
18 | features:
19 |
20 | # This feature will only be active, if the current user has id 12
21 | feature_123:
22 | default: false
23 | constraint: 'user_id == 12'
24 | ```
25 |
26 | For defining the `user_id`, we just add this as second argument.
27 |
28 | In twig:
29 | ```twig
30 | {% if feature('feature_123', {'user_id': '12', 'user_role': 'ROLE_ADMIN'}) %}
31 | {# ... #}
32 | {% endif %}
33 | ```
34 |
35 | In a service:
36 | ```php
37 | # FooService.php
38 |
39 | class FooService
40 | {
41 | /**
42 | * @var FeatureManagerInterface
43 | */
44 | private $manager;
45 |
46 | /**
47 | * @param FeatureManagerInterface $manager
48 | * Service id: flagception.manager.feature_manager
49 | */
50 | public function __construct(FeatureManagerInterface $manager)
51 | {
52 | $this->manager = $manager;
53 | }
54 |
55 | public function do()
56 | {
57 | // ...
58 | $context = new Context();
59 | $context->add('user_id', 12);
60 | $context->add('user_role', 'ROLE_ADMIN');
61 |
62 | if ($this->manager->isActive('feature_123', $context)) {
63 | // ...
64 | }
65 | // ...
66 | }
67 | }
68 | ```
69 |
70 | ##### Define globally variable
71 | For adding a global variable, just create a ContextDecorator class and implement `ContextDecoratorInterface`.
72 | You have to create two methods. The `getName` method return the ContextDecorator name and the `decorate` method
73 | will extend the context data with your variables. Remember to tag the service with `flagception.context_decorator`.
74 |
75 | This bundle supports [autoconfiguration](https://symfony.com/blog/new-in-symfony-3-3-service-autoconfiguration) for `ContextDecoratorInterface` from Symfony 3.3.
76 |
77 | As the feature manager may serializes context data in future (eg. for caching),
78 | you should not store objects that cannot be serialized (like PDO objects) or you need to provide your own serialize() method.
79 |
80 | Example for adding the `user_id`:
81 | ```php
82 | # UserContextDecorator.php
83 |
84 | class UserContextDecorator implements ContextDecoratorInterface
85 | {
86 | private $user;
87 |
88 | public function __construct(User $user)
89 | {
90 | $this->user = $user;
91 | }
92 |
93 | public function getName(): string
94 | {
95 | return 'user_context_decorator';
96 | }
97 |
98 | public function decorate(Context $context): Context
99 | {
100 | $context->add('user_id', $this->user->getId);
101 | $context->add('user_role', $this->user->getRole());
102 |
103 | return $context;
104 | }
105 | }
106 | ```
107 |
108 | ##### Methods for constraints
109 | We have some methods you can use for your constraint expression. But you can always create own methods if you like (see below).
110 |
111 | | Method | Description | Example call |
112 | |--------|----------------------------------------------|----------------------------------|
113 | | date | Use php `date` function with current timestamp | `date("H") > 8 and date("H") < 18` |
114 | | match | Use php `preg_match` with pattern and value. This method return true or false | `match("/foo/i", "FOO") == true` |
115 | | ratio | Random true / false value based on your given ratio between 0 - 1 | `ratio(0.5) == true` |
116 |
117 |
118 | ##### Define own methods
119 | All what you have to do is to [create a provider](http://symfony.com/doc/current/components/expression_language/extending.html)
120 | for the expression language (like you already know from symfony).
121 | Then tag your provider with the `flagception.expression_language_provider` and you can use your function in constraints.
122 |
123 | Example provider for creating a format date:
124 | ```php
125 | // DateProvider.php
126 |
127 | class DateProvider implements ExpressionFunctionProviderInterface
128 | {
129 | /**
130 | * {@inheritdoc}
131 | */
132 | public function getFunctions()
133 | {
134 | return [
135 | new ExpressionFunction(
136 | 'date',
137 | function ($value) {
138 | return sprintf('date(%1$s, time())', $value);
139 | },
140 | function ($arguments, $str) {
141 | return date($str, time());
142 | }
143 | ],
144 | );
145 | }
146 | }
147 | ```
148 |
149 | Now you can use `date`:
150 | ```yml
151 | flagception:
152 | features:
153 | feature_abc:
154 | default: false
155 | constraint: 'date("H") > 8 and date("H") < 18'
156 | ```
157 |
--------------------------------------------------------------------------------
/tests/Listener/AttributeSubscriberTest.php:
--------------------------------------------------------------------------------
1 |
20 | * @package Flagception\Tests\FlagceptionBundle\Listener
21 | */
22 | class AttributeSubscriberTest extends TestCase
23 | {
24 | /**
25 | * Test implement interface
26 | *
27 | * @return void
28 | */
29 | public function testImplementInterface()
30 | {
31 | $manager = $this->createMock(FeatureManagerInterface::class);
32 | $subscriber = new AttributeSubscriber($manager);
33 |
34 | static::assertInstanceOf(EventSubscriberInterface::class, $subscriber);
35 | }
36 |
37 | /**
38 | * Test subscribed events
39 | *
40 | * @return void
41 | */
42 | public function testSubscribedEvents()
43 | {
44 | static::assertEquals(
45 | [KernelEvents::CONTROLLER => 'onKernelController',],
46 | AttributeSubscriber::getSubscribedEvents()
47 | );
48 | }
49 |
50 | /**
51 | * Test controller is closure
52 | *
53 | * @return void
54 | */
55 | public function testControllerIsClosure()
56 | {
57 | $manager = $this->createMock(FeatureManagerInterface::class);
58 | $manager->expects(static::never())->method('isActive');
59 |
60 | $event = $this->createControllerEvent(
61 | function () {
62 | }
63 | );
64 |
65 | $subscriber = new AttributeSubscriber($manager);
66 | $subscriber->onKernelController($event);
67 | }
68 |
69 | /**
70 | * Test on class with active feature
71 | *
72 | * @return void
73 | */
74 | public function testOnClassIsActive()
75 | {
76 | $manager = $this->createMock(FeatureManagerInterface::class);
77 | $manager->method('isActive')->with('feature_abc')->willReturn(true);
78 |
79 | $event = $this->createControllerEvent([
80 | new AttributeTestClass(),
81 | 'normalMethod'
82 | ]);
83 |
84 | $subscriber = new AttributeSubscriber($manager);
85 | $subscriber->onKernelController($event);
86 | }
87 |
88 | /**
89 | * Test on class with inactive feature
90 | *
91 | * @return void
92 | */
93 | public function testOnClassIsInactive()
94 | {
95 | $this->expectException(NotFoundHttpException::class);
96 |
97 | $manager = $this->createMock(FeatureManagerInterface::class);
98 | $manager->method('isActive')->with('feature_abc')->willReturn(false);
99 |
100 | $event = $this->createControllerEvent([
101 | new AttributeTestClass(),
102 | 'normalMethod'
103 | ]);
104 |
105 | $subscriber = new AttributeSubscriber($manager);
106 | $subscriber->onKernelController($event);
107 | }
108 |
109 | /**
110 | * Test on method with active feature
111 | *
112 | * @return void
113 | */
114 | public function testOnMethodIsActive()
115 | {
116 | $manager = $this->createMock(FeatureManagerInterface::class);
117 | $manager
118 | ->method('isActive')
119 | ->withConsecutive(['feature_abc'], ['feature_def'])
120 | ->willReturnOnConsecutiveCalls(true, true);
121 |
122 | $event = $this->createControllerEvent([
123 | new AttributeTestClass(),
124 | 'invalidMethod'
125 | ]);
126 |
127 | $subscriber = new AttributeSubscriber($manager);
128 | $subscriber->onKernelController($event);
129 | }
130 |
131 | /**
132 | * Test on method with inactive feature
133 | *
134 | * @return void
135 | */
136 | public function testOnMethodIsInactive()
137 | {
138 | $this->expectException(NotFoundHttpException::class);
139 |
140 | $manager = $this->createMock(FeatureManagerInterface::class);
141 | $manager
142 | ->method('isActive')
143 | ->withConsecutive(['feature_abc'], ['feature_def'])
144 | ->willReturnOnConsecutiveCalls(true, false);
145 |
146 | $event = $this->createControllerEvent([
147 | new AttributeTestClass(),
148 | 'invalidMethod'
149 | ]);
150 |
151 | $subscriber = new AttributeSubscriber($manager);
152 | $subscriber->onKernelController($event);
153 | }
154 |
155 | /**
156 | * Create ControllerEvent
157 | *
158 | * @param $controller
159 | *
160 | * @return ControllerEvent
161 | */
162 | private function createControllerEvent($controller): ControllerEvent
163 | {
164 | return new ControllerEvent(
165 | $this->createMock(HttpKernelInterface::class),
166 | $controller,
167 | new Request(),
168 | HttpKernelInterface::MAIN_REQUEST
169 | );
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/Configurator/ArrayConfiguratorTest.php:
--------------------------------------------------------------------------------
1 |
16 | * @package Flagception\Tests\FlagceptionBundle\DependencyInjection\Configurator
17 | */
18 | class ArrayConfiguratorTest extends TestCase
19 | {
20 | /**
21 | * The container
22 | *
23 | * @var ContainerBuilder
24 | */
25 | private $container;
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function setUp(): void
31 | {
32 | $container = new ContainerBuilder();
33 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../../../src/Resources/config'));
34 | $loader->load('configurators.yml');
35 |
36 | $this->container = $container;
37 | }
38 |
39 | /**
40 | * Test key
41 | *
42 | * @return void
43 | */
44 | public function testKey()
45 | {
46 | static::assertEquals('array', (new ArrayConfigurator())->getKey());
47 | }
48 |
49 | /**
50 | * Test activator default state
51 | *
52 | * @return void
53 | */
54 | public function testActivatorDefaultState()
55 | {
56 | $config = [];
57 | $extension = new FlagceptionExtension();
58 | $extension->load($config, $this->container);
59 |
60 | static::assertTrue($this->container->hasDefinition('flagception.activator.array_activator'));
61 | }
62 |
63 | /**
64 | * Test activator default state
65 | *
66 | * @return void
67 | */
68 | public function testActivatorDefaultPriority()
69 | {
70 | $config = [
71 | [
72 | 'activators' => [
73 | 'array' => [
74 | 'enable' => true
75 | ]
76 | ]
77 | ]
78 | ];
79 | $extension = new FlagceptionExtension();
80 | $extension->load($config, $this->container);
81 |
82 | $definition = $this->container->getDefinition('flagception.activator.array_activator');
83 | static::assertEquals(255, $definition->getTag('flagception.activator')[0]['priority']);
84 | }
85 |
86 | /**
87 | * Test activator can be disabled
88 | *
89 | * @return void
90 | */
91 | public function testActivatorCanByDisabled()
92 | {
93 | $config = [
94 | [
95 | 'activators' => [
96 | 'array' => [
97 | 'enable' => false
98 | ]
99 | ]
100 | ]
101 | ];
102 | $extension = new FlagceptionExtension();
103 | $extension->load($config, $this->container);
104 |
105 | static::assertFalse($this->container->hasDefinition('flagception.activator.array_activator'));
106 | }
107 |
108 | /**
109 | * Test activator can be disabled by string
110 | *
111 | * @return void
112 | */
113 | public function testActivatorCanByDisabledByString()
114 | {
115 | $config = [
116 | [
117 | 'activators' => [
118 | 'array' => [
119 | 'enable' => 'false'
120 | ]
121 | ]
122 | ]
123 | ];
124 | $extension = new FlagceptionExtension();
125 | $extension->load($config, $this->container);
126 |
127 | static::assertFalse($this->container->hasDefinition('flagception.activator.array_activator'));
128 | }
129 |
130 | /**
131 | * Test set activator priority
132 | *
133 | * @return void
134 | */
135 | public function testActivatorSetPriority()
136 | {
137 | $config = [
138 | [
139 | 'activators' => [
140 | 'array' => [
141 | 'enable' => true,
142 | 'priority' => 10
143 | ]
144 | ]
145 | ]
146 | ];
147 | $extension = new FlagceptionExtension();
148 | $extension->load($config, $this->container);
149 |
150 | $definition = $this->container->getDefinition('flagception.activator.array_activator');
151 | static::assertEquals(10, $definition->getTag('flagception.activator')[0]['priority']);
152 | }
153 |
154 | /**
155 | * Test add features
156 | *
157 | * @return void
158 | */
159 | public function testAddFeatures()
160 | {
161 | $config = [
162 | [
163 | 'features' => [
164 | 'feature_foo' => [
165 | 'default' => true
166 | ],
167 | 'feature_bar' => [
168 | 'default' => false
169 | ],
170 | 'feature_bazz' => [
171 | 'default' => 'true'
172 | ]
173 | ]
174 | ]
175 | ];
176 |
177 | $extension = new FlagceptionExtension();
178 | $extension->load($config, $this->container);
179 |
180 | static::assertEquals(
181 | [
182 | 'feature_foo' => true,
183 | 'feature_bar' => false,
184 | 'feature_bazz' => 'true'
185 | ],
186 | $this->container->getDefinition('flagception.activator.array_activator')->getArgument(0)
187 | );
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/Configurator/EnvironmentConfiguratorTest.php:
--------------------------------------------------------------------------------
1 |
16 | * @package Flagception\Tests\FlagceptionBundle\DependencyInjection\Configurator
17 | */
18 | class EnvironmentConfiguratorTest extends TestCase
19 | {
20 | /**
21 | * The container
22 | *
23 | * @var ContainerBuilder
24 | */
25 | private $container;
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | protected function setUp(): void
31 | {
32 | $container = new ContainerBuilder();
33 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../../../src/Resources/config'));
34 | $loader->load('configurators.yml');
35 |
36 | $this->container = $container;
37 | }
38 |
39 | /**
40 | * Test key
41 | *
42 | * @return void
43 | */
44 | public function testKey()
45 | {
46 | static::assertEquals('environment', (new EnvironmentConfigurator())->getKey());
47 | }
48 |
49 | /**
50 | * Test activator default state
51 | *
52 | * @return void
53 | */
54 | public function testActivatorDefaultState()
55 | {
56 | $config = [];
57 | $extension = new FlagceptionExtension();
58 | $extension->load($config, $this->container);
59 |
60 | static::assertTrue($this->container->hasDefinition('flagception.activator.environment_activator'));
61 | }
62 |
63 | /**
64 | * Test activator default state
65 | *
66 | * @return void
67 | */
68 | public function testActivatorDefaultPriority()
69 | {
70 | $config = [
71 | [
72 | 'activators' => [
73 | 'environment' => [
74 | 'enable' => true
75 | ]
76 | ]
77 | ]
78 | ];
79 | $extension = new FlagceptionExtension();
80 | $extension->load($config, $this->container);
81 |
82 | $definition = $this->container->getDefinition('flagception.activator.environment_activator');
83 | static::assertEquals(230, $definition->getTag('flagception.activator')[0]['priority']);
84 | }
85 |
86 | /**
87 | * Test activator can be disabled
88 | *
89 | * @return void
90 | */
91 | public function testActivatorCanByDisabled()
92 | {
93 | $config = [
94 | [
95 | 'activators' => [
96 | 'environment' => [
97 | 'enable' => false
98 | ]
99 | ]
100 | ]
101 | ];
102 | $extension = new FlagceptionExtension();
103 | $extension->load($config, $this->container);
104 |
105 | static::assertFalse($this->container->hasDefinition('flagception.activator.environment_activator'));
106 | }
107 |
108 | /**
109 | * Test activator can be disabled by string
110 | *
111 | * @return void
112 | */
113 | public function testActivatorCanByDisabledByString()
114 | {
115 | $config = [
116 | [
117 | 'activators' => [
118 | 'environment' => [
119 | 'enable' => 'false'
120 | ]
121 | ]
122 | ]
123 | ];
124 | $extension = new FlagceptionExtension();
125 | $extension->load($config, $this->container);
126 |
127 | static::assertFalse($this->container->hasDefinition('flagception.activator.environment_activator'));
128 | }
129 |
130 | /**
131 | * Test set activator priority
132 | *
133 | * @return void
134 | */
135 | public function testActivatorSetPriority()
136 | {
137 | $config = [
138 | [
139 | 'activators' => [
140 | 'environment' => [
141 | 'enable' => true,
142 | 'priority' => 10
143 | ]
144 | ]
145 | ]
146 | ];
147 | $extension = new FlagceptionExtension();
148 | $extension->load($config, $this->container);
149 |
150 | $definition = $this->container->getDefinition('flagception.activator.environment_activator');
151 | static::assertEquals(10, $definition->getTag('flagception.activator')[0]['priority']);
152 | }
153 |
154 | /**
155 | * Test add features
156 | *
157 | * @return void
158 | */
159 | public function testAddFeatures()
160 | {
161 | $config = [
162 | [
163 | 'features' => [
164 | 'feature_foo' => [
165 | 'env' => $env1 = uniqid()
166 | ],
167 | 'feature_bar' => [
168 | 'env' => false
169 | ],
170 | 'feature_bazz' => [
171 | 'env' => $env2 = uniqid()
172 | ],
173 | 'feature_foobazz' => []
174 | ]
175 | ]
176 | ];
177 |
178 | $extension = new FlagceptionExtension();
179 | $extension->load($config, $this->container);
180 |
181 | static::assertEquals(
182 | [
183 | 'feature_foo' => $env1,
184 | 'feature_bazz' => $env2
185 | ],
186 | $this->container->getDefinition('flagception.activator.environment_activator')->getArgument(0)
187 | );
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/tests/Listener/RoutingMetadataSubscriberTest.php:
--------------------------------------------------------------------------------
1 |
20 | * @package Flagception\Tests\FlagceptionBundle\Listener
21 | */
22 | class RoutingMetadataSubscriberTest extends TestCase
23 | {
24 | /**
25 | * Test implement interface
26 | *
27 | * @return void
28 | */
29 | public function testImplementInterface()
30 | {
31 | $subscriber = new RoutingMetadataSubscriber($this->createMock(FeatureManagerInterface::class));
32 | static::assertInstanceOf(EventSubscriberInterface::class, $subscriber);
33 | }
34 |
35 | /**
36 | * Test subscribed events
37 | *
38 | * @return void
39 | */
40 | public function testSubscribedEvents()
41 | {
42 | static::assertEquals(
43 | [KernelEvents::CONTROLLER => 'onKernelController'],
44 | RoutingMetadataSubscriber::getSubscribedEvents()
45 | );
46 | }
47 |
48 | /**
49 | * Test request has no feature
50 | *
51 | * @return void
52 | */
53 | public function testRequestHasNoFeature()
54 | {
55 | $request = new Request();
56 | $event = $this->createControllerEvent($request);
57 |
58 | $manager = $this->createMock(FeatureManagerInterface::class);
59 | $manager
60 | ->expects(static::never())
61 | ->method('isActive');
62 |
63 | $subscriber = new RoutingMetadataSubscriber($manager);
64 | $subscriber->onKernelController($event);
65 | }
66 |
67 | /**
68 | * Test feature is not active
69 | *
70 | * @return void
71 | */
72 | public function testFeatureIsNotActive()
73 | {
74 | $this->expectException(NotFoundHttpException::class);
75 |
76 | $request = new Request([], [], ['_feature' => 'feature_abc']);
77 | $event = $this->createControllerEvent($request);
78 |
79 | $manager = $this->createMock(FeatureManagerInterface::class);
80 | $manager
81 | ->expects(static::once())
82 | ->method('isActive')
83 | ->with('feature_abc')
84 | ->willReturn(false);
85 |
86 | $subscriber = new RoutingMetadataSubscriber($manager);
87 | $subscriber->onKernelController($event);
88 | }
89 |
90 | /**
91 | * Test feature is active
92 | *
93 | * @return void
94 | */
95 | public function testFeatureIsActive()
96 | {
97 | $request = new Request([], [], ['_feature' => 'feature_abc']);
98 | $event = $this->createControllerEvent($request);
99 |
100 | $manager = $this->createMock(FeatureManagerInterface::class);
101 | $manager
102 | ->expects(static::once())
103 | ->method('isActive')
104 | ->with('feature_abc')
105 | ->willReturn(true);
106 |
107 | $subscriber = new RoutingMetadataSubscriber($manager);
108 | $subscriber->onKernelController($event);
109 | }
110 |
111 | /**
112 | * Create ControllerEvent
113 | *
114 | * @param $controller
115 | *
116 | * @return ControllerEvent
117 | */
118 | private function createControllerEvent($request): ControllerEvent
119 | {
120 | return new ControllerEvent(
121 | $this->createMock(HttpKernelInterface::class),
122 | [$this, 'testFeatureIsActive'],
123 | $request,
124 | 1 /* HttpKernelInterface::MAIN_REQUEST */
125 | );
126 | }
127 |
128 | /**
129 | * Test features are not active
130 | *
131 | * @return void
132 | */
133 | public function testRouteWithMultipleFeaturesIsNotActive()
134 | {
135 | $this->expectException(NotFoundHttpException::class);
136 |
137 | $request = new Request([], [], ['_feature' => ['feature_abc', 'feature_def']]);
138 |
139 | $event = new ControllerEvent(
140 | $this->createMock(HttpKernelInterface::class),
141 | function () {
142 | return new Response();
143 | },
144 | $request,
145 | 1 /* HttpKernelInterface::MAIN_REQUEST */
146 | );
147 |
148 | $manager = $this->createMock(FeatureManagerInterface::class);
149 | $manager
150 | ->expects(static::exactly(2))
151 | ->method('isActive')
152 | ->withConsecutive(['feature_abc'], ['feature_def'])
153 | ->willReturnOnConsecutiveCalls(true, false);
154 |
155 | $subscriber = new RoutingMetadataSubscriber($manager);
156 | $subscriber->onKernelController($event);
157 | }
158 |
159 | /**
160 | * Test features are active
161 | *
162 | * @return void
163 | */
164 | public function testRouteWithMultipleFeaturesIsActive()
165 | {
166 | $request = new Request([], [], ['_feature' => ['feature_abc', 'feature_def']]);
167 |
168 | $event = new ControllerEvent(
169 | $this->createMock(HttpKernelInterface::class),
170 | function () {
171 | return new Response();
172 | },
173 | $request,
174 | 1 /* HttpKernelInterface::MAIN_REQUEST */
175 | );
176 |
177 | $manager = $this->createMock(FeatureManagerInterface::class);
178 | $manager
179 | ->expects(static::exactly(2))
180 | ->method('isActive')
181 | ->withConsecutive(['feature_abc'], ['feature_def'])
182 | ->willReturnOnConsecutiveCalls(true, true);
183 |
184 | $subscriber = new RoutingMetadataSubscriber($manager);
185 | $subscriber->onKernelController($event);
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/Configurator/ConstraintConfiguratorTest.php:
--------------------------------------------------------------------------------
1 |
17 | * @package Flagception\Tests\FlagceptionBundle\DependencyInjection\Configurator
18 | */
19 | class ConstraintConfiguratorTest extends TestCase
20 | {
21 | /**
22 | * The container
23 | *
24 | * @var ContainerBuilder
25 | */
26 | private $container;
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function setUp(): void
32 | {
33 | $container = new ContainerBuilder();
34 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../../../src/Resources/config'));
35 | $loader->load('configurators.yml');
36 |
37 | $this->container = $container;
38 | }
39 |
40 | /**
41 | * Test key
42 | *
43 | * @return void
44 | */
45 | public function testKey()
46 | {
47 | static::assertEquals('constraint', (new ConstraintConfigurator())->getKey());
48 | }
49 |
50 | /**
51 | * Test activator default state
52 | *
53 | * @return void
54 | */
55 | public function testActivatorDefaultState()
56 | {
57 | $config = [];
58 | $extension = new FlagceptionExtension();
59 | $extension->load($config, $this->container);
60 |
61 | static::assertTrue($this->container->hasDefinition('flagception.activator.constraint_activator'));
62 | }
63 |
64 | /**
65 | * Test activator default state
66 | *
67 | * @return void
68 | */
69 | public function testActivatorDefaultPriority()
70 | {
71 | $config = [
72 | [
73 | 'activators' => [
74 | 'constraint' => [
75 | 'enable' => true
76 | ]
77 | ]
78 | ]
79 | ];
80 | $extension = new FlagceptionExtension();
81 | $extension->load($config, $this->container);
82 |
83 | $definition = $this->container->getDefinition('flagception.activator.constraint_activator');
84 | static::assertEquals(210, $definition->getTag('flagception.activator')[0]['priority']);
85 | }
86 |
87 | /**
88 | * Test activator can be disabled
89 | *
90 | * @return void
91 | */
92 | public function testActivatorCanByDisabled()
93 | {
94 | $config = [
95 | [
96 | 'activators' => [
97 | 'constraint' => [
98 | 'enable' => false
99 | ]
100 | ]
101 | ]
102 | ];
103 | $extension = new FlagceptionExtension();
104 | $extension->load($config, $this->container);
105 |
106 | static::assertFalse($this->container->hasDefinition('flagception.activator.constraint_activator'));
107 | }
108 |
109 | /**
110 | * Test activator can be disabled by string
111 | *
112 | * @return void
113 | */
114 | public function testActivatorCanByDisabledByString()
115 | {
116 | $config = [
117 | [
118 | 'activators' => [
119 | 'constraint' => [
120 | 'enable' => 'false'
121 | ]
122 | ]
123 | ]
124 | ];
125 | $extension = new FlagceptionExtension();
126 | $extension->load($config, $this->container);
127 |
128 | static::assertFalse($this->container->hasDefinition('flagception.activator.constraint_activator'));
129 | }
130 |
131 | /**
132 | * Test set activator priority
133 | *
134 | * @return void
135 | */
136 | public function testActivatorSetPriority()
137 | {
138 | $config = [
139 | [
140 | 'activators' => [
141 | 'constraint' => [
142 | 'enable' => true,
143 | 'priority' => 10
144 | ]
145 | ]
146 | ]
147 | ];
148 | $extension = new FlagceptionExtension();
149 | $extension->load($config, $this->container);
150 |
151 | $definition = $this->container->getDefinition('flagception.activator.constraint_activator');
152 | static::assertEquals(10, $definition->getTag('flagception.activator')[0]['priority']);
153 | }
154 |
155 | /**
156 | * Test add features
157 | *
158 | * @return void
159 | */
160 | public function testAddFeatures()
161 | {
162 | $config = [
163 | [
164 | 'features' => [
165 | 'feature_foo' => [
166 | 'constraint' => 'false == true'
167 | ],
168 | 'feature_bar' => [
169 | 'constraint' => false
170 | ],
171 | 'feature_bazz' => [
172 | 'constraint' => 'true == false'
173 | ],
174 | 'feature_foobazz' => []
175 | ]
176 | ]
177 | ];
178 |
179 | $extension = new FlagceptionExtension();
180 | $extension->load($config, $this->container);
181 |
182 | static::assertEquals(
183 | new Reference('flagception.constraint.constraint_resolver'),
184 | $this->container->getDefinition('flagception.activator.constraint_activator')->getArgument(0)
185 | );
186 |
187 | static::assertEquals(
188 | [
189 | 'feature_foo' => 'false == true',
190 | 'feature_bazz' => 'true == false'
191 | ],
192 | $this->container->getDefinition('flagception.activator.constraint_activator')->getArgument(1)
193 | );
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/Profiler/FeatureDataCollector.php:
--------------------------------------------------------------------------------
1 |
17 | * @package Flagception\Bundle\FlagceptionBundle\Profiler
18 | */
19 | class FeatureDataCollector extends DataCollector
20 | {
21 | /**
22 | * The profiler chain activator
23 | *
24 | * @var TraceableChainActivator
25 | */
26 | private $chainActivator;
27 |
28 | /**
29 | * The chain decorator
30 | *
31 | * @var ChainDecorator
32 | */
33 | private $chainDecorator;
34 |
35 | /**
36 | * FeatureDataCollector constructor.
37 | *
38 | * @param TraceableChainActivator $chainActivator
39 | * @param ChainDecorator $chainDecorator
40 | */
41 | public function __construct(ChainActivator $chainActivator, ChainDecorator $chainDecorator)
42 | {
43 | $this->chainActivator = $chainActivator;
44 | $this->chainDecorator = $chainDecorator;
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | public function collect(Request $request, Response $response, ?Throwable $exception = null): void
51 | {
52 | $this->data = [
53 | 'summary' => [
54 | 'features' => 0,
55 | 'activeFeatures' => 0,
56 | 'inactiveFeatures' => 0,
57 | 'corruptFeatures' => 0
58 | ],
59 | 'requests' => [],
60 | 'activators' => [],
61 | 'decorators' => [],
62 | 'trace' => []
63 | ];
64 |
65 | // Activators
66 | foreach ($this->chainActivator->getActivators() as $offset => $activator) {
67 | $name = $activator->getName();
68 |
69 | $this->data['activators'][$name] = [
70 | 'priority' => ++$offset,
71 | 'name' => $name,
72 | 'requests' => 0,
73 | 'activeRequests' => 0,
74 | 'inactiveRequests' => 0,
75 | ];
76 | }
77 |
78 | // Decorators
79 | foreach ($this->chainDecorator->getDecorators() as $offset => $decorator) {
80 | $name = $decorator->getName();
81 |
82 | $this->data['decorators'][$name] = [
83 | 'priority' => ++$offset,
84 | 'name' => $name
85 | ];
86 | }
87 |
88 | // Analyze trace
89 | if (!$this->chainActivator instanceof TraceableChainActivator) {
90 | return;
91 | }
92 |
93 | $this->data['trace'] = $this->chainActivator->getTrace();
94 |
95 | foreach ($this->chainActivator->getTrace() as $trace) {
96 | if (!isset($this->data['requests'][$trace['feature']])) {
97 | $this->data['requests'][$trace['feature']] = [
98 | 'requests' => 0,
99 | 'activeRequests' => 0,
100 | 'inactiveRequests' => 0,
101 | 'activators' => []
102 | ];
103 | }
104 |
105 | $featureTrace = $this->data['requests'][$trace['feature']];
106 | $featureTrace['requests']++;
107 |
108 | if ($trace['result'] === true) {
109 | $featureTrace['activeRequests']++;
110 | } else {
111 | $featureTrace['inactiveRequests']++;
112 | }
113 |
114 | foreach ($trace['stack'] as $activator => $result) {
115 | if ($result === true && !in_array($activator, $featureTrace['activators'], true)) {
116 | $featureTrace['activators'][] = $activator;
117 | }
118 |
119 | $this->data['activators'][$activator]['requests']++;
120 |
121 | if ($result === true) {
122 | $this->data['activators'][$activator]['activeRequests']++;
123 | } else {
124 | $this->data['activators'][$activator]['inactiveRequests']++;
125 | }
126 | }
127 | $this->data['requests'][$trace['feature']] = $featureTrace;
128 | }
129 |
130 | $this->data['summary']['features'] = count($this->data['requests']);
131 | foreach ($this->data['requests'] as $trace) {
132 | if ($trace['activeRequests'] > 0 && $trace['inactiveRequests'] === 0) {
133 | $this->data['summary']['activeFeatures']++;
134 | } elseif ($trace['inactiveRequests'] > 0 && $trace['activeRequests'] === 0) {
135 | $this->data['summary']['inactiveFeatures']++;
136 | } else {
137 | $this->data['summary']['corruptFeatures']++;
138 | }
139 | }
140 | }
141 |
142 | /**
143 | * Get all results
144 | *
145 | * @return array
146 | */
147 | public function getRequests(): array
148 | {
149 | return $this->data['requests'];
150 | }
151 |
152 | /**
153 | * Get all activators
154 | *
155 | * @return array
156 | */
157 | public function getActivators(): array
158 | {
159 | return $this->data['activators'] ?? [];
160 | }
161 |
162 | /**
163 | * Get all decorators
164 | *
165 | * @return array
166 | */
167 | public function getDecorators(): array
168 | {
169 | return $this->data['decorators'] ?? [];
170 | }
171 |
172 | /**
173 | * Get trace
174 | *
175 | * @return array
176 | */
177 | public function getTrace(): array
178 | {
179 | return $this->data['trace'] ?? [];
180 | }
181 |
182 | /**
183 | * Get summary
184 | *
185 | * @return array
186 | */
187 | public function getSummary(): array
188 | {
189 | return $this->data['summary'] ?? [];
190 | }
191 |
192 | /**
193 | * {@inheritdoc}
194 | */
195 | public function getName(): string
196 | {
197 | return 'flagception';
198 | }
199 |
200 | /**
201 | * {@inheritdoc}
202 | */
203 | public function reset(): void
204 | {
205 | $this->data = [];
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [6.0.0]
2 | ### Removed
3 | - \#27 Remove deprecated `doctrine/annotations` implementation @migo315
4 | - Support for pdo instance for database-activator dropped @migo315
5 |
6 | ### Changed
7 | - Update to Flagception SDK Version ^2.0 @migo315
8 | - Update to Flagception Database Activator Version ^2.0 @migo315
9 |
10 | ### Fix
11 | - Fix deprecations @calderholding-r
12 |
13 | ## [5.0.0]
14 | ### Added
15 | - \#3 Support for PHP8 attributes @axwel13
16 | - Support for route condition expression @axwel13
17 |
18 | ### Changed
19 | - Change minimum php version to 8.0 @axwel13
20 |
21 | ## [4.1.0]
22 | ### Added
23 | - \#80 Support for multiple features in route definition @ajgarlag
24 |
25 | ### Changed
26 | - \#82 Restore support for Symfony 4.4 LTS @ajgarlag
27 |
28 | ## [4.0.2]
29 | ### Changed
30 | - Enabled support for twig v3
31 |
32 | ## [4.0.1]
33 | ### Fix
34 | - Fix subscriber for invokable controllers @vitsadecky
35 |
36 | ## [4.0.0]
37 | ### Changed
38 | - \#67 Add compatibility for Symfony 5 @migo315
39 | - \#67 Drop compatibility for Symfony 2, Symfony 3 and Symfony 4 @migo315 @hanishsingla
40 | - Drop Contentful Activator @migo315
41 | - \#64 Add Travis tests for Symony 5 @migo315
42 | - \#59 Update PHP requirements @migo315
43 |
44 | ## [3.6.0]
45 | ### Added
46 | - Support for multiple features in route definition @ajgarlag
47 |
48 | ## [3.5.1]
49 | ### Fix
50 | - Fix subscriber for invokable controllers @vitsadecky
51 |
52 | ## [3.5.0]
53 | ### Changed
54 | - \#69 Support for twig 3 @migo315
55 |
56 | ## [3.4.0]
57 | ### Changed
58 | - Update to Flagception SDK Version ^1.5 @migo315
59 | - Allow `%env()` syntax for `default` field in feature list @c33s \ @migo315
60 |
61 | ### Fix
62 | - Fix env handling for `DotEnv` component >=5.0 @c33s \ @migo315
63 |
64 | ## [3.3.0]
65 | ### Added
66 | - Add support for Symfony 5.0 @migo315
67 | - Extend Travis tests for Symfony 4.3 and 4.4 @migo315
68 |
69 | ### Fix
70 | - \#61 Replace deprecated service factory shortcut with array syntax @bretrzaun / @migo315
71 |
72 | ## [3.2.0]
73 | ### Added
74 | - \#53 Update flagception sdk to version 1.4 and implement whitelist / blacklist mode for cookies @migo315
75 |
76 | ## [3.1.2]
77 | ### Fix
78 | - \#47 Fix tree builder deprecation for symfony >= 4.2 @migo316
79 |
80 | ### Added
81 | - \#49 Add support for Symfony 4.2 @migo315
82 | - \#48 Add support for PHP7.3 @migo315
83 |
84 | ## [3.1.1]
85 | ### Changed
86 | - \#45 Replace `symfony/framework-bundle` and `doctrine/common` with following dependencies:
87 | - `doctrine/annotations`
88 | - `symfony/dependency-injection`
89 | - `symfony/yaml`
90 | - `symfony/config`
91 | - `symfony/http-kernel`
92 | - `twig/twig`
93 |
94 | ### Removed
95 | - \#45 Remove using `ClassUtils` for getting controller class
96 |
97 | ## [3.1.0]
98 | ### Fix
99 | - \#27 Fix route xml in documentation @migo315
100 |
101 | ### Changed
102 | - \#35 Swap own cookie activator with the new flagception sdk cookie activator @migo315
103 | - Refactor profiler and data collector @migo315
104 | - Swap old `ProfilerChainActivator` with new `TraceableChainActivator` @migo315
105 | - Update [Flagception SDK](https://packagist.org/packages/flagception/flagception) to version 1.3.0 @migo315
106 |
107 | ### Added
108 | - \#26 Add feature name advice in documentation @migo315
109 | - Add `php-mock` as dev dependency and add missing contentful configurator test @migo315
110 | - \#31 Add support for auto configuration for `FeatureActivatorInterface` @migo315
111 | - \#32 Add support for auto configuration for `ContectDecoratorInterface` @migo315
112 | - Add caching option for `ContentfulActivator` @migo315
113 | - Add configuration for the new [DatabaseActivator](https://packagist.org/packages/flagception/database-activator) @migo315
114 |
115 | ### Removed
116 | - Remove unneeded models and bags (just internal stuff) @migo315
117 |
118 | ## [3.0.1]
119 | ### Fix
120 | - Add service alias for `Flagception\Manager\FeatureManagerInterface` for fixing autowiring @hanishsingla
121 |
122 | ## [3.0.0]
123 | ### Refactored
124 | - Complete refactoring and renaming to `flagception` @migo315
125 | - See [Upgrade from 2.x](UPGRADE-3.0.md)
126 |
127 | ## [2.1.2] - 2017-11-13
128 | ### Fix
129 | - Bug #13 / Fix bool cast for configuration @teiling88
130 |
131 | ## [2.1.1] - 2017-11-09
132 | ### Fix
133 | - Fix variables for configuration @RedactedProfile
134 |
135 | ## [2.1.0] - 2017-10-26
136 | ### Added
137 | - Add ContextDecoratorInterface, CompilerPass and Tag for modify the context object globally @migo315
138 | - All context content are available as own variable for expression constraints @migo315
139 | - Add profiler icon @migo315
140 |
141 | ## [2.0.0] - 2017-10-11
142 | ### Added
143 | - Add events before and after feature is searched / requested @migo315
144 | - Add optional context object for features (breaking change!) @migo315
145 | - Add 'isActive' method for stashes @migo315
146 | - Add configuration option for routing metadata subscriber @migo315
147 | - Add configuration for cookie stash separator @migo315
148 | - Add constraints for ConfigStash @migo315
149 |
150 | ### Changed
151 | - Changed license to MIT @bestit
152 | - Fix phpunit whitelist @migo315
153 | - Stashes are now explicitly queried for the status of a feature and not every time for all features (breaking change!) @migo315
154 | - Move tests to root 'tests' directory @migo315
155 | - Profiler shows inactive features too @migo315
156 | - Profiler shows given context for features @migo315
157 | - Update readme @migo315
158 | - Chang configuration option of annotation subscriber @migo315
159 |
160 | ### Removed
161 | - Remove 'getActiveFeatures' method for stashes. Use 'isActive' instead @migo315
162 |
163 | ## [1.0.4] - 2017-08-07
164 | ### Added
165 | - Configuration for enable / disable annotation check @espendiller / @migo315
166 | - Add feature handling via route metadata @espendiller / @migo315
167 |
168 | ### Changed
169 | - Remove feature bag and move logic to ConfigStash @espendiller / @migo315
170 |
171 | ## [1.0.3] - 2017-08-03
172 | ### Added
173 | - Add error message for annotation subscriber if feature is inactive @migo315
174 |
175 | ### Change
176 | - Fix 'Cannot use object of type Closure as array' error @migo315
177 |
178 | ## [1.0.2] - 2017-08-02
179 | ### Added
180 | - Add getName method in twig extension for supporting twig version < 1.26 @migo315
181 |
182 | ## [1.0.1] - 2017-08-02
183 | ### Removed
184 | - Removed obsolete repository in composer.json @migo315
185 |
186 | ## [1.0.0] - 2017-08-02
187 | ### Changed
188 | - Symfony min version downgraded from ^3.2 to ^3.1 @migo315
189 | - Profiler toolbar hides feature section when 0 features are active
190 | - Fix readme typo @migo315
191 |
192 | ## [0.0.1] - 2017-06-23
193 | ### Added
194 | - Initial commit and relase @migo315
195 | - Add annotation feature check @migo315
196 | - Add twig extension for active check @migo315
197 | - Add FilterManager @migo315
198 | - Add StashBag / FeatureBag @migo315
199 | - Add Profiler @migo315
200 | - Add ConfigStash @migo315
201 | - Add CookieStash @migo315
202 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configurator/DatabaseConfigurator.php:
--------------------------------------------------------------------------------
1 |
17 | * @package Flagception\Bundle\FlagceptionBundle\DependencyInjection\Configurator
18 | */
19 | class DatabaseConfigurator implements ActivatorConfiguratorInterface
20 | {
21 | /**
22 | * {@inheritdoc}
23 | */
24 | public function getKey()
25 | {
26 | return 'database';
27 | }
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | public function addActivator(ContainerBuilder $container, array $config, array $features)
33 | {
34 | if ($config['enable'] === false) {
35 | return;
36 | }
37 |
38 | if (!class_exists('Flagception\Database\Activator\DatabaseActivator')) {
39 | throw new LogicException('For using database you have to load "flagception/database-activator"');
40 | }
41 |
42 | $definition = new Definition(DatabaseActivator::class);
43 | $credentials = null;
44 | if (isset($config['dbal'])) {
45 | $credentials = new Reference($config['dbal']);
46 | } elseif (isset($config['url'])) {
47 | $credentials = $config['url'];
48 | } else {
49 | $credentials = $config['credentials'];
50 | }
51 |
52 | $definition->addArgument($credentials);
53 | $definition->addArgument($config['options']);
54 |
55 | $definition->addTag('flagception.activator', [
56 | 'priority' => $config['priority']
57 | ]);
58 |
59 | $container->setDefinition('flagception.activator.database_activator', $definition);
60 |
61 | // Set caching
62 | if ($config['cache']['enable'] === true) {
63 | $cacheDefinition = new Definition(CacheActivator::class);
64 | $cacheDefinition->setDecoratedService('flagception.activator.database_activator');
65 | $cacheDefinition->addArgument(new Reference('flagception.activator.database_activator.cache.inner'));
66 | $cacheDefinition->addArgument(new Reference($config['cache']['pool']));
67 | $cacheDefinition->addArgument($config['cache']['lifetime']);
68 | $container->setDefinition('flagception.activator.database_activator.cache', $cacheDefinition);
69 | }
70 | }
71 |
72 | /**
73 | * {@inheritdoc}
74 | */
75 | public function addConfiguration(ArrayNodeDefinition $node)
76 | {
77 | $node
78 | ->addDefaultsIfNotSet(['enable' => false])
79 | ->validate()
80 | ->ifTrue(function ($config) {
81 | return !isset($config['url'])
82 | && !isset($config['dbal'])
83 | && !isset($config['credentials']);
84 | })
85 | ->thenInvalid('You must either set the url, dbal or credentials field.')
86 | ->end()
87 | ->children()
88 | ->booleanNode('enable')
89 | ->beforeNormalization()
90 | ->ifString()
91 | ->then(function ($value) {
92 | return filter_var($value, FILTER_VALIDATE_BOOLEAN);
93 | })
94 | ->end()
95 | ->defaultFalse()
96 | ->end()
97 | ->integerNode('priority')
98 | ->defaultValue(220)
99 | ->end()
100 | ->scalarNode('url')
101 | ->info('Connection string for the database')
102 | ->end()
103 | ->scalarNode('dbal')
104 | ->info('Service with dbal instance')
105 | ->end()
106 | ->arrayNode('credentials')
107 | ->children()
108 | ->scalarNode('dbname')
109 | ->isRequired()
110 | ->cannotBeEmpty()
111 | ->end()
112 | ->scalarNode('user')
113 | ->isRequired()
114 | ->cannotBeEmpty()
115 | ->end()
116 | ->scalarNode('password')
117 | ->isRequired()
118 | ->cannotBeEmpty()
119 | ->end()
120 | ->scalarNode('host')
121 | ->isRequired()
122 | ->cannotBeEmpty()
123 | ->end()
124 | ->scalarNode('driver')
125 | ->isRequired()
126 | ->cannotBeEmpty()
127 | ->end()
128 | ->end()
129 | ->end()
130 | ->arrayNode('options')
131 | ->addDefaultsIfNotSet()
132 | ->children()
133 | ->scalarNode('db_table')
134 | ->defaultValue('flagception_features')
135 | ->cannotBeEmpty()
136 | ->end()
137 | ->scalarNode('db_column_feature')
138 | ->defaultValue('feature')
139 | ->cannotBeEmpty()
140 | ->end()
141 | ->scalarNode('db_column_state')
142 | ->defaultValue('state')
143 | ->cannotBeEmpty()
144 | ->end()
145 | ->end()
146 | ->end()
147 | ->arrayNode('cache')
148 | ->addDefaultsIfNotSet()
149 | ->children()
150 | ->booleanNode('enable')
151 | ->beforeNormalization()
152 | ->ifString()
153 | ->then(function ($value) {
154 | return filter_var($value, FILTER_VALIDATE_BOOLEAN);
155 | })
156 | ->end()
157 | ->defaultFalse()
158 | ->end()
159 | ->scalarNode('pool')
160 | ->defaultValue('cache.app')
161 | ->cannotBeEmpty()
162 | ->end()
163 | ->integerNode('lifetime')
164 | ->defaultValue(3600)
165 | ->end()
166 | ->end()
167 | ->end()
168 | ->end();
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/tests/Profiler/FeatureDataCollectorTest.php:
--------------------------------------------------------------------------------
1 |
20 | * @package Flagception\Tests\FlagceptionBundle\Profiler
21 | */
22 | class FeatureDataCollectorTest extends TestCase
23 | {
24 | /**
25 | * Test get name
26 | *
27 | * @return void
28 | */
29 | public function testGetName()
30 | {
31 | $collector = new FeatureDataCollector(new TraceableChainActivator(), new ChainDecorator());
32 | static::assertEquals('flagception', $collector->getName());
33 | }
34 |
35 | /**
36 | * Test the reset method
37 | *
38 | * @return void
39 | */
40 | public function testReset()
41 | {
42 | $collector = new FeatureDataCollector(new TraceableChainActivator(), new ChainDecorator());
43 | $collector->reset();
44 | static::assertEquals([], $collector->getActivators());
45 | }
46 |
47 | /**
48 | * Test complete collect handling
49 | *
50 | * @return void
51 | */
52 | public function testCollect()
53 | {
54 | $collector = new FeatureDataCollector(
55 | $chainActivator = $this->createMock(TraceableChainActivator::class),
56 | $chainDecorator = new ChainDecorator()
57 | );
58 |
59 | $chainDecorator->add(new ArrayDecorator());
60 |
61 | $chainActivator
62 | ->expects(static::once())
63 | ->method('getActivators')
64 | ->willReturn([new ArrayActivator(), new CookieActivator([])]);
65 |
66 | $chainActivator
67 | ->expects(static::exactly(2))
68 | ->method('getTrace')
69 | ->willReturn([
70 | [
71 | 'feature' => 'abc',
72 | 'context' => new Context(),
73 | 'result' => true,
74 | 'stack' => [
75 | 'array' => false,
76 | 'cookie' => true
77 | ]
78 | ],
79 | [
80 | 'feature' => 'abc',
81 | 'context' => new Context(),
82 | 'result' => true,
83 | 'stack' => [
84 | 'array' => false,
85 | 'cookie' => true
86 | ]
87 | ],
88 | [
89 | 'feature' => 'def',
90 | 'context' => new Context(),
91 | 'result' => false,
92 | 'stack' => [
93 | 'array' => false,
94 | 'cookie' => false
95 | ]
96 | ],
97 | [
98 | 'feature' => 'ywz',
99 | 'context' => new Context(),
100 | 'result' => true,
101 | 'stack' => [
102 | 'array' => true
103 | ]
104 | ],
105 | [
106 | 'feature' => 'corrupt',
107 | 'context' => new Context(),
108 | 'result' => true,
109 | 'stack' => [
110 | 'array' => false,
111 | 'cookie' => true
112 | ]
113 | ],
114 | [
115 | 'feature' => 'corrupt',
116 | 'context' => new Context(),
117 | 'result' => false,
118 | 'stack' => [
119 | 'array' => false,
120 | 'cookie' => false
121 | ]
122 | ]
123 | ]);
124 |
125 | $collector->collect(new Request(), new Response());
126 |
127 | static::assertEquals([
128 | 'features' => 4,
129 | 'activeFeatures' => 2,
130 | 'inactiveFeatures' => 1,
131 | 'corruptFeatures' => 1
132 | ], $collector->getSummary());
133 |
134 | static::assertEquals([
135 | 'array' => [
136 | 'priority' => 1,
137 | 'name' => 'array'
138 | ]
139 | ], $collector->getDecorators());
140 |
141 | static::assertEquals([
142 | 'array' => [
143 | 'priority' => 1,
144 | 'name' => 'array',
145 | 'requests' => 6,
146 | 'activeRequests' => 1,
147 | 'inactiveRequests' => 5,
148 | ],
149 | 'cookie' => [
150 | 'priority' => 2,
151 | 'name' => 'cookie',
152 | 'requests' => 5,
153 | 'activeRequests' => 3,
154 | 'inactiveRequests' => 2,
155 | ]
156 | ], $collector->getActivators());
157 |
158 | static::assertEquals([
159 | [
160 | 'feature' => 'abc',
161 | 'context' => new Context(),
162 | 'result' => true,
163 | 'stack' => [
164 | 'array' => false,
165 | 'cookie' => true
166 | ]
167 | ],
168 | [
169 | 'feature' => 'abc',
170 | 'context' => new Context(),
171 | 'result' => true,
172 | 'stack' => [
173 | 'array' => false,
174 | 'cookie' => true
175 | ]
176 | ],
177 | [
178 | 'feature' => 'def',
179 | 'context' => new Context(),
180 | 'result' => false,
181 | 'stack' => [
182 | 'array' => false,
183 | 'cookie' => false
184 | ]
185 | ],
186 | [
187 | 'feature' => 'ywz',
188 | 'context' => new Context(),
189 | 'result' => true,
190 | 'stack' => [
191 | 'array' => true
192 | ]
193 | ],
194 | [
195 | 'feature' => 'corrupt',
196 | 'context' => new Context(),
197 | 'result' => true,
198 | 'stack' => [
199 | 'array' => false,
200 | 'cookie' => true
201 | ]
202 | ],
203 | [
204 | 'feature' => 'corrupt',
205 | 'context' => new Context(),
206 | 'result' => false,
207 | 'stack' => [
208 | 'array' => false,
209 | 'cookie' => false
210 | ]
211 | ]
212 | ], $collector->getTrace());
213 |
214 | static::assertEquals([
215 | 'abc' => [
216 | 'requests' => 2,
217 | 'activeRequests' => 2,
218 | 'inactiveRequests' => 0,
219 | 'activators' => [
220 | 'cookie'
221 | ]
222 | ],
223 | 'def' => [
224 | 'requests' => 1,
225 | 'activeRequests' => 0,
226 | 'inactiveRequests' => 1,
227 | 'activators' => []
228 | ],
229 | 'ywz' => [
230 | 'requests' => 1,
231 | 'activeRequests' => 1,
232 | 'inactiveRequests' => 0,
233 | 'activators' => [
234 | 'array'
235 | ]
236 | ],
237 | 'corrupt' => [
238 | 'requests' => 2,
239 | 'activeRequests' => 1,
240 | 'inactiveRequests' => 1,
241 | 'activators' => [
242 | 'cookie'
243 | ]
244 | ],
245 | ], $collector->getRequests());
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/src/Resources/views/profiler/layout.html.twig:
--------------------------------------------------------------------------------
1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %}
2 |
3 | {% block toolbar %}
4 | {% if collector.summary.features|length > 0 %}
5 | {% set icon %}
6 | {{ include('@Flagception/collector/icon.svg') }}
7 | {{ collector.summary.features }} features
8 | {% endset %}
9 |
10 | {% set text %}
11 |
12 | Features
13 | {{ collector.summary.features }}
14 |
15 |
16 | Active features
17 | {{ collector.summary.activeFeatures }}
18 |
19 |
20 | Inactive features
21 | {{ collector.summary.inactiveFeatures }}
22 |
23 | {% if collector.summary.corruptFeatures > 0 %}
24 |
25 | Corrupt features
26 | {{ collector.summary.corruptFeatures }}
27 |
28 | {% endif %}
29 | {% endset %}
30 |
31 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: true }) }}
32 | {% endif %}
33 | {% endblock %}
34 |
35 | {% block menu %}
36 |
37 |
38 | {{ include('@Flagception/collector/icon.svg') }}
39 |
40 | Features
41 | {% if collector.summary.features > 0 %}
42 |
43 | {{ collector.summary.features }}
44 |
45 | {% endif %}
46 |
47 | {% endblock %}
48 |
49 | {% block panel %}
50 | Metrics
51 |
52 |
53 | {{ collector.summary.features }}
54 | Features
55 |
56 |
57 |
58 | {{ collector.summary.activeFeatures }}
59 | Active features
60 |
61 |
62 |
63 | {{ collector.summary.inactiveFeatures }}
64 | Inactive features
65 |
66 |
67 |
68 | {{ collector.summary.corruptFeatures }}
69 | Corrupt features
70 |
71 |
72 |
73 | States
74 |
75 |
76 |
States {{ collector.summary.features }}
77 |
78 |
79 |
80 |
81 |
82 | | Feature name |
83 | State |
84 | Amount requests |
85 | Amount active |
86 | Amount inactive |
87 | Activators |
88 |
89 |
90 | {% for name, request in collector.requests %}
91 |
92 | | {{ name }} |
93 |
94 | {% if request.activeRequests > 0 and request.inactiveRequests == 0 %}
95 | ACTIVE
96 | {% elseif request.inactiveRequests > 0 and request.activeRequests == 0 %}
97 | INACTIVE
98 | {% else %}
99 | CORRUPT
100 | {% endif %}
101 | |
102 | {{ request.requests }} |
103 | {{ request.activeRequests }} |
104 | {{ request.inactiveRequests }} |
105 | {{ request.activators|join(', ') }} |
106 |
107 | {% endfor %}
108 |
109 |
110 |
111 |
112 |
Activators {{ collector.activators|length }}
113 |
114 |
115 |
116 |
117 |
118 | | Priority |
119 | Name |
120 | Amount requests |
121 | Amount active |
122 | Amount inactive |
123 |
124 |
125 | {% for activator in collector.activators %}
126 |
127 | | {{ activator.priority }} |
128 | {{ activator.name }} |
129 | {{ activator.requests }} |
130 | {{ activator.activeRequests }} |
131 | {{ activator.inactiveRequests }} |
132 |
133 | {% else %}
134 |
135 | | No activators defined |
136 |
137 | {% endfor %}
138 |
139 |
140 |
141 |
142 |
Decorators {{ collector.decorators|length }}
143 |
144 |
145 |
146 |
147 |
148 | | Priority |
149 | Name |
150 |
151 |
152 | {% for decorator in collector.decorators %}
153 |
154 | | {{ decorator.priority }} |
155 | {{ decorator.name }} |
156 |
157 | {% else %}
158 |
159 | | No decorators defined |
160 |
161 | {% endfor %}
162 |
163 |
164 |
165 |
166 |
Log
167 |
168 |
169 |
170 |
171 |
172 | | Feature name |
173 | State |
174 | Activator |
175 | Context |
176 |
177 |
178 | {% for trace in collector.trace %}
179 | {% for activator, result in trace.stack %}
180 |
181 | | {{ trace.feature }} |
182 |
183 | {% if result %}
184 | ACTIVE
185 | {% else %}
186 | INACTIVE
187 | {% endif %}
188 | |
189 | {{ activator }} |
190 | {{ dump(trace.context.all) }} |
191 |
192 | {% endfor %}
193 | {% endfor %}
194 |
195 |
196 |
197 |
198 | {% endblock %}
199 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/Configurator/CookieConfiguratorTest.php:
--------------------------------------------------------------------------------
1 |
17 | * @package Flagception\Tests\FlagceptionBundle\DependencyInjection\Configurator
18 | */
19 | class CookieConfiguratorTest extends TestCase
20 | {
21 | /**
22 | * The container
23 | *
24 | * @var ContainerBuilder
25 | */
26 | private $container;
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function setUp(): void
32 | {
33 | $container = new ContainerBuilder();
34 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../../../src/Resources/config'));
35 | $loader->load('configurators.yml');
36 |
37 | $this->container = $container;
38 | }
39 |
40 | /**
41 | * Test key
42 | *
43 | * @return void
44 | */
45 | public function testKey()
46 | {
47 | static::assertEquals('cookie', (new CookieConfigurator())->getKey());
48 | }
49 |
50 | /**
51 | * Test activator default state
52 | *
53 | * @return void
54 | */
55 | public function testActivatorDefaultState()
56 | {
57 | $config = [];
58 | $extension = new FlagceptionExtension();
59 | $extension->load($config, $this->container);
60 |
61 | static::assertFalse($this->container->hasDefinition('flagception.activator.cookie_activator'));
62 | }
63 |
64 | /**
65 | * Test activator default state
66 | *
67 | * @return void
68 | */
69 | public function testActivatorDefaultPriority()
70 | {
71 | $config = [
72 | [
73 | 'activators' => [
74 | 'cookie' => [
75 | 'enable' => true
76 | ]
77 | ]
78 | ]
79 | ];
80 | $extension = new FlagceptionExtension();
81 | $extension->load($config, $this->container);
82 |
83 | $definition = $this->container->getDefinition('flagception.activator.cookie_activator');
84 | static::assertEquals(200, $definition->getTag('flagception.activator')[0]['priority']);
85 | }
86 |
87 | /**
88 | * Test activator can be disabled
89 | *
90 | * @return void
91 | */
92 | public function testActivatorCanByDisabled()
93 | {
94 | $config = [
95 | [
96 | 'activators' => [
97 | 'cookie' => [
98 | 'enable' => false
99 | ]
100 | ]
101 | ]
102 | ];
103 | $extension = new FlagceptionExtension();
104 | $extension->load($config, $this->container);
105 |
106 | static::assertFalse($this->container->hasDefinition('flagception.activator.cookie_activator'));
107 | }
108 |
109 | /**
110 | * Test activator can be disabled by string
111 | *
112 | * @return void
113 | */
114 | public function testActivatorCanByDisabledByString()
115 | {
116 | $config = [
117 | [
118 | 'activators' => [
119 | 'cookie' => [
120 | 'enable' => 'false'
121 | ]
122 | ]
123 | ]
124 | ];
125 | $extension = new FlagceptionExtension();
126 | $extension->load($config, $this->container);
127 |
128 | static::assertFalse($this->container->hasDefinition('flagception.activator.cookie_activator'));
129 | }
130 |
131 | /**
132 | * Test activator can be enabled
133 | *
134 | * @return void
135 | */
136 | public function testActivatorCanByEnabled()
137 | {
138 | $config = [
139 | [
140 | 'activators' => [
141 | 'cookie' => [
142 | 'enable' => true
143 | ]
144 | ]
145 | ]
146 | ];
147 | $extension = new FlagceptionExtension();
148 | $extension->load($config, $this->container);
149 |
150 | static::assertTrue($this->container->hasDefinition('flagception.activator.cookie_activator'));
151 | }
152 |
153 | /**
154 | * Test activator can be enabled by string
155 | *
156 | * @return void
157 | */
158 | public function testActivatorCanByEnabledByString()
159 | {
160 | $config = [
161 | [
162 | 'activators' => [
163 | 'cookie' => [
164 | 'enable' => 'true'
165 | ]
166 | ]
167 | ]
168 | ];
169 | $extension = new FlagceptionExtension();
170 | $extension->load($config, $this->container);
171 |
172 | static::assertTrue($this->container->hasDefinition('flagception.activator.cookie_activator'));
173 | }
174 |
175 | /**
176 | * Test set activator priority
177 | *
178 | * @return void
179 | */
180 | public function testActivatorSetPriority()
181 | {
182 | $config = [
183 | [
184 | 'activators' => [
185 | 'cookie' => [
186 | 'enable' => true,
187 | 'priority' => 10
188 | ]
189 | ]
190 | ]
191 | ];
192 | $extension = new FlagceptionExtension();
193 | $extension->load($config, $this->container);
194 |
195 | $definition = $this->container->getDefinition('flagception.activator.cookie_activator');
196 | static::assertEquals(10, $definition->getTag('flagception.activator')[0]['priority']);
197 | }
198 |
199 | /**
200 | * Test add features
201 | *
202 | * @return void
203 | */
204 | public function testAddFeatures()
205 | {
206 | $config = [
207 | [
208 | 'features' => [
209 | 'feature_foo' => [
210 | 'cookie' => true
211 | ],
212 | 'feature_bar' => [
213 | 'cookie' => false
214 | ],
215 | 'feature_bazz' => [
216 | 'cookie' => 'true'
217 | ],
218 | 'feature_foobazz' => []
219 | ],
220 | 'activators' => [
221 | 'cookie' => [
222 | 'enable' => true
223 | ]
224 | ]
225 | ]
226 | ];
227 |
228 | $extension = new FlagceptionExtension();
229 | $extension->load($config, $this->container);
230 |
231 | static::assertEquals(
232 | [
233 | [
234 | 'feature_foo',
235 | 'feature_bazz',
236 | 'feature_foobazz'
237 | ],
238 | 'flagception',
239 | ',',
240 | CookieActivator::WHITELIST
241 | ],
242 | $this->container->getDefinition('flagception.activator.cookie_activator')->getArguments()
243 | );
244 | }
245 |
246 | /**
247 | * Test add features by blacklist
248 | *
249 | * @return void
250 | */
251 | public function testAddFeaturesByBlacklist()
252 | {
253 | $config = [
254 | [
255 | 'features' => [
256 | 'feature_foo' => [
257 | 'cookie' => true
258 | ],
259 | 'feature_bar' => [
260 | 'cookie' => false
261 | ],
262 | 'feature_bazz' => [
263 | 'cookie' => 'true'
264 | ],
265 | 'feature_foobazz' => [],
266 | 'feature_wyz' => [
267 | 'cookie' => 'false'
268 | ],
269 | ],
270 | 'activators' => [
271 | 'cookie' => [
272 | 'enable' => true,
273 | 'mode' => CookieActivator::BLACKLIST
274 | ]
275 | ]
276 | ]
277 | ];
278 |
279 | $extension = new FlagceptionExtension();
280 | $extension->load($config, $this->container);
281 |
282 | static::assertEquals(
283 | [
284 | [
285 | 'feature_bar',
286 | 'feature_wyz'
287 | ],
288 | 'flagception',
289 | ',',
290 | CookieActivator::BLACKLIST
291 | ],
292 | $this->container->getDefinition('flagception.activator.cookie_activator')->getArguments()
293 | );
294 | }
295 |
296 | /**
297 | * Test full config
298 | *
299 | * @return void
300 | */
301 | public function testFullConfig()
302 | {
303 | $config = [
304 | [
305 | 'activators' => [
306 | 'cookie' => [
307 | 'enable' => true,
308 | 'name' => $name = uniqid(),
309 | 'separator' => $separator = uniqid(),
310 | 'mode' => CookieActivator::BLACKLIST
311 | ]
312 | ]
313 | ]
314 | ];
315 |
316 | $extension = new FlagceptionExtension();
317 | $extension->load($config, $this->container);
318 |
319 | static::assertEquals(
320 | [
321 | [],
322 | $name,
323 | $separator,
324 | CookieActivator::BLACKLIST
325 | ],
326 | $this->container->getDefinition('flagception.activator.cookie_activator')->getArguments()
327 | );
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/tests/DependencyInjection/Configurator/DatabaseConfiguratorTest.php:
--------------------------------------------------------------------------------
1 |
20 | * @package Flagception\Tests\FlagceptionBundle\DependencyInjection\Configurator
21 | */
22 | class DatabaseConfiguratorTest extends TestCase
23 | {
24 | /**
25 | * The container
26 | *
27 | * @var ContainerBuilder
28 | */
29 | private $container;
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | protected function setUp(): void
35 | {
36 | $container = new ContainerBuilder();
37 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../../../src/Resources/config'));
38 | $loader->load('configurators.yml');
39 |
40 | $this->container = $container;
41 | }
42 |
43 | /**
44 | * Test key
45 | *
46 | * @return void
47 | */
48 | public function testKey()
49 | {
50 | static::assertEquals('database', (new DatabaseConfigurator())->getKey());
51 | }
52 |
53 | /**
54 | * Test activator default state
55 | *
56 | * @return void
57 | */
58 | public function testActivatorDefaultState()
59 | {
60 | $config = [];
61 | $extension = new FlagceptionExtension();
62 | $extension->load($config, $this->container);
63 |
64 | static::assertFalse($this->container->hasDefinition('flagception.activator.database_activator'));
65 | }
66 |
67 | /**
68 | * Test activator default state
69 | *
70 | * @return void
71 | */
72 | public function testActivatorDefaultPriority()
73 | {
74 | $config = [
75 | [
76 | 'activators' => [
77 | 'database' => [
78 | 'enable' => true,
79 | 'url' => 'mysql://foo'
80 | ]
81 | ]
82 | ]
83 | ];
84 | $extension = new FlagceptionExtension();
85 | $extension->load($config, $this->container);
86 |
87 | $definition = $this->container->getDefinition('flagception.activator.database_activator');
88 | static::assertEquals(220, $definition->getTag('flagception.activator')[0]['priority']);
89 | }
90 |
91 | /**
92 | * Test activator can be enabled
93 | *
94 | * @return void
95 | */
96 | public function testActivatorCanByEnabled()
97 | {
98 | $config = [
99 | [
100 | 'activators' => [
101 | 'database' => [
102 | 'enable' => true,
103 | 'url' => 'pdo-sqlite://:memory:'
104 | ]
105 | ]
106 | ]
107 | ];
108 | $extension = new FlagceptionExtension();
109 | $extension->load($config, $this->container);
110 |
111 | static::assertTrue($this->container->hasDefinition('flagception.activator.database_activator'));
112 |
113 | /** @var DatabaseActivator $activator */
114 | $activator = $this->container->get('flagception.activator.database_activator');
115 |
116 | static::assertInstanceOf(Connection::class, $activator->getConnection());
117 | }
118 |
119 | /**
120 | * Test activator can be enabled by string
121 | *
122 | * @return void
123 | */
124 | public function testActivatorCanByEnabledByString()
125 | {
126 | $config = [
127 | [
128 | 'activators' => [
129 | 'database' => [
130 | 'enable' => 'true',
131 | 'url' => 'pdo-sqlite://:memory:'
132 | ]
133 | ]
134 | ]
135 | ];
136 | $extension = new FlagceptionExtension();
137 | $extension->load($config, $this->container);
138 |
139 | static::assertTrue($this->container->hasDefinition('flagception.activator.database_activator'));
140 |
141 | /** @var DatabaseActivator $activator */
142 | $activator = $this->container->get('flagception.activator.database_activator');
143 |
144 | static::assertInstanceOf(Connection::class, $activator->getConnection());
145 | }
146 |
147 | /**
148 | * Test set activator priority
149 | *
150 | * @return void
151 | */
152 | public function testActivatorSetPriority()
153 | {
154 | $config = [
155 | [
156 | 'activators' => [
157 | 'database' => [
158 | 'enable' => true,
159 | 'priority' => 10,
160 | 'url' => 'pdo-sqlite://:memory:'
161 | ]
162 | ]
163 | ]
164 | ];
165 | $extension = new FlagceptionExtension();
166 | $extension->load($config, $this->container);
167 |
168 | $definition = $this->container->getDefinition('flagception.activator.database_activator');
169 | static::assertEquals(10, $definition->getTag('flagception.activator')[0]['priority']);
170 |
171 | /** @var DatabaseActivator $activator */
172 | $activator = $this->container->get('flagception.activator.database_activator');
173 |
174 | static::assertInstanceOf(Connection::class, $activator->getConnection());
175 | }
176 |
177 | /**
178 | * Test set activator by url
179 | *
180 | * @return void
181 | */
182 | public function testActivatorByUrl()
183 | {
184 | $config = [
185 | [
186 | 'activators' => [
187 | 'database' => [
188 | 'enable' => true,
189 | 'url' => 'pdo-sqlite://:memory:'
190 | ]
191 | ]
192 | ]
193 | ];
194 | $extension = new FlagceptionExtension();
195 | $extension->load($config, $this->container);
196 |
197 | static::assertTrue($this->container->hasDefinition('flagception.activator.database_activator'));
198 |
199 | /** @var DatabaseActivator $activator */
200 | $activator = $this->container->get('flagception.activator.database_activator');
201 |
202 | static::assertInstanceOf(Connection::class, $activator->getConnection());
203 | }
204 |
205 | /**
206 | * Test set activator by dbal
207 | *
208 | * @return void
209 | */
210 | public function testActivatorByDbal()
211 | {
212 | $this->container->set('my.dbal.service', DriverManager::getConnection([
213 | 'dbname' => 'mydb',
214 | 'user' => 'user',
215 | 'password' => 'secret',
216 | 'host' => 'localhost',
217 | 'driver' => 'pdo_mysql'
218 | ]));
219 |
220 | $config = [
221 | [
222 | 'activators' => [
223 | 'database' => [
224 | 'enable' => true,
225 | 'dbal' => 'my.dbal.service'
226 | ]
227 | ]
228 | ]
229 | ];
230 |
231 | $extension = new FlagceptionExtension();
232 | $extension->load($config, $this->container);
233 |
234 | static::assertTrue($this->container->hasDefinition('flagception.activator.database_activator'));
235 |
236 | /** @var DatabaseActivator $activator */
237 | $activator = $this->container->get('flagception.activator.database_activator');
238 |
239 | static::assertInstanceOf(Connection::class, $activator->getConnection());
240 | }
241 |
242 | /**
243 | * Test set activator by credentials
244 | *
245 | * @return void
246 | */
247 | public function testActivatorByCredentials()
248 | {
249 | $config = [
250 | [
251 | 'activators' => [
252 | 'database' => [
253 | 'enable' => true,
254 | 'credentials' => [
255 | 'dbname' => 'mydb',
256 | 'user' => 'user',
257 | 'password' => 'secret',
258 | 'host' => 'localhost',
259 | 'driver' => 'pdo_mysql'
260 | ]
261 | ]
262 | ]
263 | ]
264 | ];
265 | $extension = new FlagceptionExtension();
266 | $extension->load($config, $this->container);
267 |
268 | static::assertTrue($this->container->hasDefinition('flagception.activator.database_activator'));
269 |
270 | /** @var DatabaseActivator $activator */
271 | $activator = $this->container->get('flagception.activator.database_activator');
272 |
273 | static::assertInstanceOf(Connection::class, $activator->getConnection());
274 | }
275 |
276 | /**
277 | * Test set activator by invalid credentials
278 | *
279 | * @return void
280 | */
281 | public function testActivatorByInvalidCredentials()
282 | {
283 | $this->expectException(InvalidConfigurationException::class);
284 |
285 | $config = [
286 | [
287 | 'activators' => [
288 | 'database' => [
289 | 'enable' => true,
290 | 'credentials' => [
291 | 'dbname' => 'mydb',
292 | 'driver' => 'pdo_mysql'
293 | ]
294 | ]
295 | ]
296 | ]
297 | ];
298 | $extension = new FlagceptionExtension();
299 | $extension->load($config, $this->container);
300 |
301 | $this->container->hasDefinition('flagception.activator.database_activator');
302 |
303 | /** @var DatabaseActivator $activator */
304 | $activator = $this->container->get('flagception.activator.database_activator');
305 |
306 | static::assertInstanceOf(Connection::class, $activator->getConnection());
307 | }
308 |
309 | /**
310 | * Test set activator with missing connection field
311 | *
312 | * @return void
313 | */
314 | public function testActivatorByMissingConnectionField()
315 | {
316 | $this->expectException(InvalidConfigurationException::class);
317 |
318 | $config = [
319 | [
320 | 'activators' => [
321 | 'database' => [
322 | 'enable' => true
323 | ]
324 | ]
325 | ]
326 | ];
327 | $extension = new FlagceptionExtension();
328 | $extension->load($config, $this->container);
329 |
330 | $this->container->hasDefinition('flagception.activator.database_activator');
331 | }
332 |
333 | /**
334 | * Test set activator cache is disabled by default
335 | *
336 | * @return void
337 | */
338 | public function testActivatorCacheIsDisabled()
339 | {
340 | $config = [
341 | [
342 | 'activators' => [
343 | 'database' => [
344 | 'enable' => true,
345 | 'url' => 'pdo-sqlite://:memory:'
346 | ]
347 | ]
348 | ]
349 | ];
350 | $extension = new FlagceptionExtension();
351 | $extension->load($config, $this->container);
352 |
353 | static::assertFalse($this->container->hasDefinition('flagception.activator.database_activator.cache'));
354 | }
355 |
356 | /**
357 | * Test set activator with cache
358 | *
359 | * @return void
360 | */
361 | public function testActivatorWithCache()
362 | {
363 | $config = [
364 | [
365 | 'activators' => [
366 | 'database' => [
367 | 'enable' => true,
368 | 'url' => 'pdo-sqlite://:memory:',
369 | 'cache' => [
370 | 'enable' => true
371 | ]
372 | ]
373 | ]
374 | ]
375 | ];
376 | $extension = new FlagceptionExtension();
377 | $extension->load($config, $this->container);
378 |
379 | static::assertTrue($this->container->hasDefinition('flagception.activator.database_activator.cache'));
380 |
381 | $definition = $this->container->getDefinition('flagception.activator.database_activator.cache');
382 | static::assertEquals(
383 | 'flagception.activator.database_activator',
384 | $definition->getDecoratedService()[0]
385 | );
386 |
387 | /** @var DatabaseActivator $activator */
388 | $activator = $this->container->get('flagception.activator.database_activator');
389 |
390 | static::assertInstanceOf(Connection::class, $activator->getConnection());
391 | }
392 |
393 | /**
394 | * Test set activator with cache by string
395 | *
396 | * @return void
397 | */
398 | public function testActivatorWithCacheByString()
399 | {
400 | $config = [
401 | [
402 | 'activators' => [
403 | 'database' => [
404 | 'enable' => true,
405 | 'url' => 'pdo-sqlite://:memory:',
406 | 'cache' => [
407 | 'enable' => 'true'
408 | ]
409 | ]
410 | ]
411 | ]
412 | ];
413 | $extension = new FlagceptionExtension();
414 | $extension->load($config, $this->container);
415 |
416 | static::assertTrue($this->container->hasDefinition('flagception.activator.database_activator.cache'));
417 |
418 | /** @var DatabaseActivator $activator */
419 | $activator = $this->container->get('flagception.activator.database_activator');
420 |
421 | static::assertInstanceOf(Connection::class, $activator->getConnection());
422 | }
423 | }
424 |
--------------------------------------------------------------------------------