├── .docheader ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .php-cs-fixer.php ├── Annotations └── Toggle.php ├── CHANGELOG.md ├── Context └── UserContextFactory.php ├── DataCollector └── ToggleCollector.php ├── DependencyInjection ├── Configuration.php └── QandidateToggleExtension.php ├── EventListener └── ToggleListener.php ├── LICENSE ├── Makefile ├── QandidateToggleBundle.php ├── README.md ├── Resources ├── config │ ├── redis.xml │ └── services.xml ├── doc │ └── example │ │ ├── .gitignore │ │ ├── composer.json │ │ └── index.php └── views │ └── data_collector │ └── toggle.html.twig ├── Tests ├── Context │ └── UserContextFactoryTest.php ├── DependencyInjection │ ├── ConfigurationTest.php │ └── QandidateToggleExtensionTest.php ├── EventListener │ ├── Fixture │ │ ├── FooControllerToggleAtClassAndMethod.php │ │ └── FooControllerToggleAtInvoke.php │ └── ToggleListenerTest.php ├── Fixtures │ └── tests │ │ └── feature_active.test ├── Functional │ ├── AppKernel.php │ ├── ContextFactoryTest.php │ ├── Resources │ │ └── config │ │ │ ├── test.yml │ │ │ └── test_services.xml │ └── WebTestCase.php ├── TokenStorage.php ├── Twig │ └── ToggleTwigExtensionTest.php └── TwigIntegrationTest.php ├── Twig └── ToggleTwigExtension.php ├── composer.json ├── phpstan-baseline.neon ├── phpstan.neon └── phpunit.xml.dist /.docheader: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the qandidate/toggle-bundle package. 3 | * 4 | * (c) Qandidate.com 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # see https://probot.github.io/apps/stale/ 2 | 3 | # inherit settings from https://github.com/qandidate-labs/.github/blob/main/.github/stale.yml 4 | _extends: .github 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "master" 7 | push: 8 | branches: 9 | - "master" 10 | schedule: 11 | - cron: "37 13 * * 1" 12 | 13 | jobs: 14 | tests: 15 | name: "Run tests" 16 | runs-on: "ubuntu-20.04" 17 | strategy: 18 | matrix: 19 | php-version: 20 | - "7.4" 21 | - "8.0" 22 | - "8.1" 23 | - "8.2" 24 | symfony-version: # https://symfony.com/releases 25 | - "4.4" 26 | - "5.2" 27 | - "5.3" 28 | steps: 29 | - name: "Checkout" 30 | uses: "actions/checkout@v3" 31 | - name: "Install PHP" 32 | uses: "shivammathur/setup-php@v2" 33 | with: 34 | php-version: "${{ matrix.php-version }}" 35 | coverage: "none" 36 | env: 37 | fail-fast: true 38 | - name: "Validate composer.json and composer.lock" 39 | run: "composer validate --strict --no-interaction --ansi" 40 | - name: "Install dependencies with Composer" 41 | uses: "ramsey/composer-install@v2" 42 | - name: "Install specific Symfony version" 43 | run: "composer --no-update require symfony/symfony:^${{ matrix.symfony-version }}" 44 | - name: "Run tests" 45 | run: "make test" 46 | 47 | coding-standards: 48 | name: "Coding standards" 49 | runs-on: "ubuntu-20.04" 50 | steps: 51 | - name: "Checkout" 52 | uses: "actions/checkout@v3" 53 | - name: "Install PHP" 54 | uses: "shivammathur/setup-php@v2" 55 | with: 56 | php-version: "8.0" 57 | coverage: "none" 58 | - name: "Install dependencies with Composer" 59 | uses: "ramsey/composer-install@v2" 60 | - name: "Check coding standards" 61 | run: "make php-cs-fixer-ci" 62 | - name: Create Pull Request 63 | if: github.ref == 'refs/heads/master' 64 | uses: peter-evans/create-pull-request@v5 65 | with: 66 | commit-message: Apply coding standards 67 | branch: php-cs-fixer 68 | delete-branch: true 69 | title: 'Apply coding standards' 70 | draft: false 71 | base: master 72 | 73 | static-analysis: 74 | name: "Static analysis" 75 | runs-on: "ubuntu-20.04" 76 | steps: 77 | - name: "Checkout" 78 | uses: "actions/checkout@v3" 79 | - name: "Install PHP" 80 | uses: "shivammathur/setup-php@v2" 81 | with: 82 | php-version: "8.0" 83 | coverage: "none" 84 | - name: "Install dependencies with Composer" 85 | uses: "ramsey/composer-install@v2" 86 | - name: "Run PHPStan" 87 | run: "make phpstan" 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor 3 | /Tests/Functional/cache 4 | /Tests/Functional/logs 5 | .phpunit.result.cache 6 | var/ 7 | .php-cs-fixer.cache 8 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setFinder( 6 | \PhpCsFixer\Finder::create() 7 | ->in(__DIR__) 8 | ->exclude([ 9 | 'vendor', 10 | 'var', 11 | ]) 12 | ); 13 | 14 | return $config; 15 | -------------------------------------------------------------------------------- /Annotations/Toggle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Annotations; 15 | 16 | /** 17 | * @Annotation 18 | */ 19 | class Toggle 20 | { 21 | /** 22 | * @var string 23 | */ 24 | public $name; 25 | } 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # 1.0.x 4 | 5 | * [60eac73](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/60eac73e97ab6bbcefd5f48761f66a4bb3e40414) require symfony/security-bundle (#54) (othillo) 6 | * [091c564](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/091c5646874f434679ce9c05686482999fbd665d) support Symfony 4 (#53) (othillo) 7 | * [01461d7](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/01461d7efb89234b0cf8034a15c7ca2d99048498) example using MicroKernelTrait (othillo) 8 | 9 | # 0.8.x 10 | 11 | * [06b3b9e](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/06b3b9e122b2b977a37e4917512be05601ed7603) Add collection_factory config directive to README (krizon) 12 | * [f5f14c4](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/f5f14c4ca26c44dd6fcd082f120be3bfbce9323f) process configuration on the extension (othillo) 13 | 14 | # 0.7.x 15 | 16 | * [83f8f81](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/83f8f81de640e1122e789a88a81c8cd39f93d75b) Documents the toggle annotation (adev) 17 | * [fb7b1f0](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/fb7b1f07e99f72ebd9a28c263efd1d992390a8bc) Add symfony/security-core dependency (adev) 18 | * [deec1d0](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/deec1d053fdbf3963e567f3c3db02b9304587518) Support invokable controllers in annotation toggle listener (krizon) 19 | 20 | # 0.6.x 21 | 22 | * [17567ea](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/17567ea75dfe43d608c536b195af8cc1e189294c) Update qandidate/toggle dependency (Roel Philipsen) 23 | * [e9e4a33](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/e9e4a33deb6411d6da20ab932cfd586e55a5ce70) Use newer phpunit version if possible (Alexander) 24 | * [1660b99](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/1660b99b543e0094a2745ade6d9f72d0e5b1ba50) Switch to container based infrastructure on travis (Alexander) 25 | * [2ec79bd](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/2ec79bda7df79c3dae0c34fa4998a55dc862a504) Use phpunit installed by composer (Alexander) 26 | * [ef58675](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/ef58675f75aa767fecd5be35a0e75ff98cc992d4) Update travis-ci build matrix (Alexander) 27 | * [d111883](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/d111883405622e1954a41e4dd23c727a97747d9f) drop support for PHP 5.5 (othillo) 28 | * [6fc0b5a](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/6fc0b5a6f6cb76c9d56ac2f244d7f776bfe24912) use matthiasnoback/symfony-dependency-injection-test for testing the extension (othillo) 29 | * [4c11568](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/4c11568c8fb261e143dae784c24bbad1b0c4391a) Allow yml configuration (othillo) 30 | * [7718e14](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/7718e1443b9b758954d8913bf5ebc94498c906ff) Rename service & inject service instead of calling constructor (othillo) 31 | * [64683d5](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/64683d5ede48eb81d53abea16997f5ed64965cd1) Remove unused usages (othillo) 32 | * [ee0a592](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/ee0a59271426b833fb4cb5dafb310c22398449b0) Remove unnecessary code from phpunit.xml.dist (othillo) 33 | * [895e930](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/895e93080d36da0463fdbd351e96f443ffd87c70) fixed unsupported status (othillo) 34 | * [5f00e7d](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/5f00e7dce3394061e3b75506542c8a2767dfebdd) test the compiled toggle collection (othillo) 35 | * [cf375c5](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/cf375c51a12daa07de13f61352fe2950ca0c5e81) removed necessity of factory (othillo) 36 | * [b779e8a](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/b779e8a540485a5bfe203cc53b74b9754b21653c) updated README (othillo) 37 | * [2c06fd3](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/2c06fd3129961d69414af34b9feeb275beff4584) renamed symfony to config (othillo) 38 | 39 | ## 0.5.x 40 | 41 | * [083a3ae](http://github.com/qandidate-labs/qandidate-toggle-bundle/commit/083a3aeb1b07c59074d8de66b3cf89282e7b991c) Add support for symfony 3 (Martin Parsiegla) 42 | 43 | ## 0.4.x 44 | 45 | - [BC BREAK] Updated the symfony dependency to 2.7 46 | - Added support for collection_factory 47 | 48 | ## 0.3.x 49 | 50 | - [BC BREAK] The twig function has been renamed from `is_active` to `feature_is_active` 51 | - Added a twig test `is active feature` 52 | 53 | ## 0.2.x 54 | 55 | - [@ricbra] added the `@Toggle` annotation, to enable/disable a controller or a specific controller action 56 | 57 | [@ricbra]: https://github.com/ricbra 58 | -------------------------------------------------------------------------------- /Context/UserContextFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Context; 15 | 16 | use Qandidate\Toggle\Context; 17 | use Qandidate\Toggle\ContextFactory; 18 | use Symfony\Component\Security\Core\Authentication\Token\NullToken; 19 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 20 | 21 | class UserContextFactory extends ContextFactory 22 | { 23 | /** 24 | * @var TokenStorageInterface 25 | */ 26 | private $tokenStorage; 27 | 28 | public function __construct(TokenStorageInterface $tokenStorage) 29 | { 30 | $this->tokenStorage = $tokenStorage; 31 | } 32 | 33 | public function createContext(): Context 34 | { 35 | $context = new Context(); 36 | 37 | $token = $this->tokenStorage->getToken(); 38 | 39 | if (null !== $token && !$token instanceof NullToken) { 40 | $context->set('username', method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername()); 41 | } 42 | 43 | return $context; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DataCollector/ToggleCollector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\DataCollector; 15 | 16 | use Qandidate\Toggle\Context; 17 | use Qandidate\Toggle\ContextFactory; 18 | use Qandidate\Toggle\Serializer\OperatorConditionSerializer; 19 | use Qandidate\Toggle\Serializer\OperatorSerializer; 20 | use Qandidate\Toggle\Serializer\ToggleSerializer; 21 | use Qandidate\Toggle\Toggle; 22 | use Qandidate\Toggle\ToggleManager; 23 | use Symfony\Component\HttpFoundation\Request; 24 | use Symfony\Component\HttpFoundation\Response; 25 | use Symfony\Component\HttpKernel\DataCollector\DataCollector; 26 | 27 | class ToggleCollector extends DataCollector 28 | { 29 | /** 30 | * @var ToggleManager 31 | */ 32 | private $toggleManager; 33 | /** 34 | * @var ContextFactory 35 | */ 36 | private $contextFactory; 37 | 38 | public function __construct(ToggleManager $toggleManager, ContextFactory $contextFactory) 39 | { 40 | $this->toggleManager = $toggleManager; 41 | $this->contextFactory = $contextFactory; 42 | } 43 | 44 | /** 45 | * Collects data for the given Request and Response. 46 | * 47 | * @return void 48 | */ 49 | public function collect(Request $request, Response $response, \Throwable $exception = null) 50 | { 51 | $serializer = new ToggleSerializer(new OperatorConditionSerializer(new OperatorSerializer())); 52 | 53 | $toggleData = array_map(function (Toggle $toggle) use ($serializer) { 54 | return $serializer->serialize($toggle); 55 | }, $this->toggleManager->all()); 56 | 57 | $this->data['toggleDetails'] = $toggleData; 58 | $this->data['context'] = $this->contextFactory->createContext(); 59 | } 60 | 61 | public function getContext(): ?Context 62 | { 63 | return $this->data['context']; 64 | } 65 | 66 | public function getToggleDetails(): array 67 | { 68 | return $this->data['toggleDetails']; 69 | } 70 | 71 | /** 72 | * Returns the name of the collector. 73 | */ 74 | public function getName(): string 75 | { 76 | return 'qandidate.toggle_collector'; 77 | } 78 | 79 | public function reset(): void 80 | { 81 | $this->data = []; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\DependencyInjection; 15 | 16 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 17 | use Symfony\Component\Config\Definition\ConfigurationInterface; 18 | 19 | class Configuration implements ConfigurationInterface 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getConfigTreeBuilder() 25 | { 26 | $treeBuilder = new TreeBuilder('qandidate_toggle'); 27 | 28 | $treeBuilder->getRootNode() 29 | ->children() 30 | ->enumNode('persistence') 31 | ->values(['in_memory', 'redis', 'factory', 'config']) 32 | ->defaultValue('in_memory') 33 | ->end() 34 | ->arrayNode('collection_factory') 35 | ->children() 36 | ->scalarNode('service_id') 37 | ->isRequired() 38 | ->cannotBeEmpty() 39 | ->end() 40 | ->scalarNode('method') 41 | ->isRequired() 42 | ->cannotBeEmpty() 43 | ->end() 44 | ->end() 45 | ->end() 46 | ->scalarNode('context_factory') 47 | ->defaultNull() 48 | ->end() 49 | ->scalarNode('redis_namespace') 50 | ->defaultValue('toggle_%kernel.environment%') 51 | ->end() 52 | ->scalarNode('redis_client') 53 | ->defaultNull() 54 | ->end() 55 | ->arrayNode('toggles') 56 | ->prototype('array') 57 | ->children() 58 | ->scalarNode('name')->end() 59 | ->scalarNode('strategy')->end() 60 | ->scalarNode('status')->end() 61 | ->arrayNode('conditions') 62 | ->prototype('array') 63 | ->children() 64 | ->scalarNode('name')->end() 65 | ->scalarNode('key')->end() 66 | ->arrayNode('operator') 67 | ->prototype('variable') 68 | ->end() 69 | ->end() 70 | ->end() 71 | ->end() 72 | ->end() 73 | ->end() 74 | ->end() 75 | ->end() 76 | ->end() 77 | ->validate() 78 | ->ifTrue(function ($v) { 79 | if (isset($v['persistence']) && 'factory' === $v['persistence']) { 80 | return !isset($v['collection_factory']['service_id'], $v['collection_factory']['method']); 81 | } 82 | 83 | return false; 84 | }) 85 | ->thenInvalid( 86 | 'When choosing "factory" persistence make sure you set "collection_factory.service_id" and "collection_factory.method"') 87 | ->end(); 88 | 89 | return $treeBuilder; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /DependencyInjection/QandidateToggleExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\DependencyInjection; 15 | 16 | use Qandidate\Toggle\ToggleCollection\InMemoryCollection; 17 | use Symfony\Component\Config\FileLocator; 18 | use Symfony\Component\DependencyInjection\Alias; 19 | use Symfony\Component\DependencyInjection\ContainerBuilder; 20 | use Symfony\Component\DependencyInjection\Definition; 21 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 22 | use Symfony\Component\DependencyInjection\Reference; 23 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 24 | 25 | class QandidateToggleExtension extends Extension 26 | { 27 | /** 28 | * @return void 29 | */ 30 | public function load(array $configs, ContainerBuilder $container) 31 | { 32 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config/')); 33 | $loader->load('services.xml'); 34 | 35 | $config = $this->processConfiguration(new Configuration(), $configs); 36 | $collection = 'in_memory'; 37 | switch (true) { 38 | case 'redis' === $config['persistence']: 39 | $loader->load('redis.xml'); 40 | 41 | $collection = 'predis'; 42 | 43 | $container->setParameter('qandidate.toggle.redis.namespace', $config['redis_namespace']); 44 | $container->setAlias('qandidate.toggle.redis.client', $config['redis_client']); 45 | 46 | break; 47 | case 'factory' === $config['persistence']: 48 | $collection = 'factory'; 49 | $definition = new Definition(InMemoryCollection::class); 50 | $definition->setFactory([ 51 | new Reference($config['collection_factory']['service_id']), 52 | $config['collection_factory']['method'], 53 | ]); 54 | 55 | $container->setDefinition('qandidate.toggle.collection.factory', $definition); 56 | 57 | break; 58 | case 'config' === $config['persistence']: 59 | $collection = 'factory'; 60 | $definition = $container->getDefinition('qandidate.toggle.collection.in_memory'); 61 | $definition->setFactory([ 62 | new Reference('qandidate.toggle.collection.serializer.in_memory'), 63 | 'deserialize', 64 | ]); 65 | $definition->addArgument($config['toggles']); 66 | 67 | $container->setDefinition('qandidate.toggle.collection.factory', $definition); 68 | break; 69 | } 70 | 71 | $container->setAlias('qandidate.toggle.collection', new Alias('qandidate.toggle.collection.'.$collection, true)); 72 | 73 | $contextFactoryService = 'qandidate.toggle.user_context_factory'; 74 | if (null !== $config['context_factory']) { 75 | $contextFactoryService = $config['context_factory']; 76 | } 77 | 78 | $container->setAlias('qandidate.toggle.context_factory', $contextFactoryService); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /EventListener/ToggleListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\EventListener; 15 | 16 | use Doctrine\Common\Annotations\Reader; 17 | use Doctrine\Common\Util\ClassUtils; 18 | use Qandidate\Bundle\ToggleBundle\Annotations\Toggle; 19 | use Qandidate\Toggle\Context; 20 | use Qandidate\Toggle\ToggleManager; 21 | use Symfony\Component\HttpKernel\Event\ControllerEvent; 22 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 23 | 24 | class ToggleListener 25 | { 26 | private $reader; 27 | private $toggleManager; 28 | private $context; 29 | 30 | public function __construct(Reader $reader, ToggleManager $toggleManager, Context $context) 31 | { 32 | $this->reader = $reader; 33 | $this->toggleManager = $toggleManager; 34 | $this->context = $context; 35 | } 36 | 37 | public function onKernelController(ControllerEvent $event): void 38 | { 39 | $controller = $event->getController(); 40 | 41 | if (is_array($controller)) { 42 | $class = ClassUtils::getClass((object) $controller[0]); 43 | $object = new \ReflectionClass($class); 44 | $method = $object->getMethod($controller[1]); 45 | } else { 46 | $object = new \ReflectionClass($controller); 47 | $method = $object->getMethod('__invoke'); 48 | } 49 | 50 | foreach ($this->reader->getClassAnnotations($object) as $annotation) { 51 | if ($annotation instanceof Toggle) { 52 | if (!$this->toggleManager->active($annotation->name, $this->context)) { 53 | throw new NotFoundHttpException(); 54 | } 55 | } 56 | } 57 | 58 | foreach ($this->reader->getMethodAnnotations($method) as $annotation) { 59 | if ($annotation instanceof Toggle) { 60 | if (!$this->toggleManager->active($annotation->name, $this->context)) { 61 | throw new NotFoundHttpException(); 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Qandidate.com - http://qandidate.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL:=help 2 | 3 | .PHONY: dependencies 4 | dependencies: 5 | composer install --no-interaction --no-suggest --no-scripts --ansi 6 | 7 | .PHONY: test 8 | test: 9 | vendor/bin/phpunit --testdox --exclude-group=none --colors=always 10 | 11 | .PHONY: qa 12 | qa: php-cs-fixer phpstan 13 | 14 | .PHONY: php-cs-fixer 15 | php-cs-fixer: 16 | vendor/bin/php-cs-fixer fix --no-interaction --allow-risky=yes --diff --verbose 17 | 18 | .PHONY: php-cs-fixer-ci 19 | php-cs-fixer-ci: 20 | vendor/bin/php-cs-fixer fix --no-interaction --allow-risky=yes --diff --verbose 21 | 22 | PHONY: phpstan 23 | phpstan: 24 | vendor/bin/phpstan analyse --level=max 25 | 26 | .PHONY: changelog 27 | changelog: 28 | git log $$(git describe --abbrev=0 --tags)...HEAD --no-merges --pretty=format:"* [%h](http://github.com/${TRAVIS_REPO_SLUG}/commit/%H) %s (%cN)" 29 | 30 | .PHONY: license 31 | license: 32 | vendor/bin/docheader check --no-interaction --ansi -vvv --exclude-dir=vendor --exclude-dir=var . 33 | 34 | # Based on https://suva.sh/posts/well-documented-makefiles/ 35 | help: ## Display this help 36 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) 37 | -------------------------------------------------------------------------------- /QandidateToggleBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle; 15 | 16 | use Symfony\Component\HttpKernel\Bundle\Bundle; 17 | 18 | class QandidateToggleBundle extends Bundle 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qandidate Toggle Symfony Bundle 2 | 3 | This Bundle provides the integration with [our toggle library]. It provides the 4 | services and configuration you need to implement feature toggles in your 5 | application. 6 | 7 | ![build status](https://github.com/qandidate-labs/qandidate-toggle-bundle/actions/workflows/ci.yml/badge.svg) 8 | 9 | [our toggle library]: https://github.com/qandidate-labs/qandidate-toggle 10 | 11 | ## About 12 | 13 | Read the our blog post series about this repository at: 14 | - http://labs.qandidate.com/blog/2014/08/18/a-new-feature-toggling-library-for-php/ 15 | - http://labs.qandidate.com/blog/2014/08/19/open-sourcing-our-feature-toggle-api-and-ui/ 16 | 17 | ## Installation 18 | 19 | ### Using Symfony Flex 20 | 21 | The easiest way to install and configure the QandidateToggleBundle with Symfony is by using 22 | [Symfony Flex](https://github.com/symfony/flex). 23 | 24 | Make sure you have Symfony Flex installed: 25 | 26 | ``` 27 | $ composer require symfony/flex ^1.0 28 | $ composer config extra.symfony.allow-contrib true 29 | ``` 30 | 31 | Install the bundle: 32 | 33 | ``` 34 | $ composer require qandidate/toggle-bundle ^1.0 35 | ``` 36 | 37 | Symfony Flex will automatically register and configure the bundle. 38 | 39 | ### Manually 40 | 41 | Add the bundle to your composer.json 42 | 43 | ```bash 44 | $ composer require qandidate/toggle-bundle ^1.0 45 | ``` 46 | 47 | Add the bundle to your Kernel: 48 | 49 | ```php 50 | $bundles = array( 51 | // .. 52 | new Symfony\Bundle\SecurityBundle\SecurityBundle(), 53 | new Symfony\Bundle\TwigBundle\TwigBundle(), 54 | new Qandidate\Bundle\ToggleBundle\QandidateToggleBundle(), 55 | ); 56 | ``` 57 | 58 | ## Configuration 59 | 60 | ```yaml 61 | qandidate_toggle: 62 | persistence: in_memory|redis|factory|config 63 | context_factory: null|your.context_factory.service.id 64 | redis_namespace: toggle_%kernel.environment% # default, only required when persistence = redis 65 | redis_client: null|your.redis_client.service.id # only required when persistence = redis 66 | collection_factory: # only required when persistence = factory 67 | service_id: your.collection_factory.service.id 68 | method: create 69 | ``` 70 | 71 | ## Sample Configuration for Symfony 72 | 73 | ```yaml 74 | qandidate_toggle: 75 | persistence: config 76 | toggles: 77 | always-active-feature: 78 | name: always-active-feature 79 | status: always-active 80 | inactive-feature: 81 | name: inactive-feature 82 | status: inactive 83 | conditions: 84 | conditionally-active: 85 | name: conditionally-active 86 | status: conditionally-active 87 | conditions: 88 | - name: operator-condition 89 | key: user_id 90 | operator: 91 | name: greater-than 92 | value: 42 93 | ``` 94 | 95 | ## Example usage 96 | 97 | Usage can vary on your application. This example uses the supplied 98 | `UserContextFactory`, but you probably need to create your own factory. 99 | 100 | ```xml 101 | 102 | 103 | 104 | 105 | 106 | 107 | ``` 108 | 109 | ```php 110 | // Acme\Controller 111 | 112 | public function __construct( 113 | /* ArticleRepository, Templating, ..*/ 114 | ToggleManager $manager, 115 | ContextFactory $contextFactory 116 | ) { 117 | // .. 118 | $this->manager = $manager; 119 | $this->context = $contextFactory->createContext(); 120 | } 121 | 122 | // .. 123 | 124 | public function articleAction(Request $request) 125 | { 126 | $this->article = $this->repository->findBySlug($request->request->get('slug')); 127 | 128 | return $this->templating->render('article.html.twig', array( 129 | 'article' => $article, 130 | 'enableComments' => $this->manager->active('comments', $this->context), 131 | )); 132 | } 133 | ``` 134 | 135 | You can find a working example using the Symfony [MicroKernelTrait](https://symfony.com/doc/current/configuration/micro_kernel_trait.html) 136 | in the [Resources/doc/example](https://github.com/qandidate-labs/qandidate-toggle-bundle/tree/master/Resources/doc/example) directory. 137 | 138 | ## Annotation Usage 139 | 140 | You can also use the `@Toggle` annotation on a controller. When the toggle isn't active a 404 exception is thrown. 141 | 142 | ```php 143 | use Qandidate\Bundle\ToggleBundle\Annotations\Toggle; 144 | 145 | /** 146 | * @Toggle("cool-feature") 147 | */ 148 | class FooController 149 | { 150 | 151 | /** 152 | * @Toggle("another-cool-feature") 153 | */ 154 | public function barAction() 155 | { 156 | } 157 | 158 | public function bazAction() 159 | { 160 | } 161 | } 162 | ``` 163 | 164 | ## Twig usage 165 | 166 | If you use Twig you can also use the function: 167 | 168 | ```jinja 169 | {% if feature_is_active('comments') %} 170 | {# Awesome comments #} 171 | {% endif %} 172 | ``` 173 | Or the Twig test: 174 | 175 | ```jinja 176 | {% if 'comments' is active feature %} 177 | {# Awesome comments #} 178 | {% endif %} 179 | ``` 180 | 181 | Both are registered in the [ToggleTwigExtension](Twig/ToggleTwigExtension.php). 182 | 183 | ## Data collector 184 | 185 | With the data collector you have a overview about all toggles. In the toolbar you see all conditions and the current status. 186 | 187 | In the panel you have two lists: 188 | 189 | * You can see all keys and there current values. 190 | * Then you can see all configured toggles, there conditions and if they are active. 191 | 192 | ## Testing 193 | 194 | To run PHPUnit tests: 195 | 196 | ```bash 197 | $ ./vendor/bin/phpunit 198 | ``` 199 | 200 | ## License 201 | 202 | MIT, see LICENSE. 203 | -------------------------------------------------------------------------------- /Resources/config/redis.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | %qandidate.toggle.redis.namespace% 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Qandidate\Toggle\ToggleManager 9 | Qandidate\Toggle\ToggleCollection\InMemoryCollection 10 | Qandidate\Toggle\Serializer\InMemoryCollectionSerializer 11 | Qandidate\Toggle\ToggleCollection\PredisCollection 12 | Qandidate\Bundle\ToggleBundle\Context\UserContextFactory 13 | Qandidate\Bundle\ToggleBundle\Twig\ToggleTwigExtension 14 | Qandidate\Bundle\ToggleBundle\EventListener\ToggleListener 15 | Qandidate\Toggle\Context 16 | Qandidate\Bundle\ToggleBundle\DataCollector\ToggleCollector 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Resources/doc/example/.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor/ 3 | /cache/ 4 | -------------------------------------------------------------------------------- /Resources/doc/example/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qandidate/toggle-bundle-example", 3 | "description": "MicroKernel Symfony application demonstrating the Qandidate Toggle Bundle", 4 | "type": "project", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "othillo", 9 | "email": "othillo@othillo.nl" 10 | }, 11 | { 12 | "name": "Qandidate.com", 13 | "homepage": "http://labs.qandidate.com" 14 | } 15 | ], 16 | "require": { 17 | "qandidate/toggle-bundle": "^1.0@dev", 18 | "symfony/symfony": "^4.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Resources/doc/example/index.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; 15 | use Symfony\Component\Config\Loader\LoaderInterface; 16 | use Symfony\Component\DependencyInjection\ContainerBuilder; 17 | use Symfony\Component\HttpFoundation\JsonResponse; 18 | use Symfony\Component\HttpFoundation\Request; 19 | use Symfony\Component\HttpKernel\Kernel; 20 | use Symfony\Component\Routing\RouteCollectionBuilder; 21 | 22 | require __DIR__.'/vendor/autoload.php'; 23 | 24 | class AppKernel extends Kernel 25 | { 26 | use MicroKernelTrait; 27 | 28 | public function registerBundles() 29 | { 30 | return [ 31 | new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), 32 | new Symfony\Bundle\SecurityBundle\SecurityBundle(), 33 | new \Qandidate\Bundle\ToggleBundle\QandidateToggleBundle(), 34 | ]; 35 | } 36 | 37 | protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) 38 | { 39 | $c->loadFromExtension('framework', [ 40 | 'secret' => 'Everything is awesome', 41 | ]); 42 | 43 | // minimal configuration to enable security bundle 44 | $c->loadFromExtension('security', [ 45 | 'providers' => [ 46 | 'my_custom_provider' => [ 47 | 'memory' => [], 48 | ], 49 | ], 50 | 'firewalls' => [ 51 | 'my_firewall' => [ 52 | 'anonymous' => [], 53 | ], 54 | ], 55 | ]); 56 | 57 | $c->loadFromExtension('qandidate_toggle', [ 58 | 'persistence' => 'config', 59 | 'toggles' => [ 60 | 'always-active-feature' => [ 61 | 'name' => 'always-active-feature', 62 | 'status' => 'always-active', 63 | ], 64 | 'inactive-feature' => [ 65 | 'name' => 'inactive-feature', 66 | 'status' => 'inactive', 67 | ], 68 | 'conditionally-active' => [ 69 | 'name' => 'conditionally-active', 70 | 'status' => 'conditionally-active', 71 | 'conditions' => [ 72 | [ 73 | 'name' => 'operator-condition', 74 | 'key' => 'user_id', 75 | 'operator' => [ 76 | 'name' => 'greater-than', 77 | 'value' => 42, 78 | ], 79 | ], 80 | ], 81 | ], 82 | ], 83 | ]); 84 | } 85 | 86 | protected function configureRoutes(RouteCollectionBuilder $routes) 87 | { 88 | $routes->add('/', 'kernel:indexAction'); 89 | } 90 | 91 | public function indexAction() 92 | { 93 | $toggleManager = $this->getContainer()->get('qandidate.toggle.manager'); 94 | $context = new \Qandidate\Toggle\Context(); 95 | $context->set('user_id', 43); 96 | 97 | $output = array_map(function (Qandidate\Toggle\Toggle $toggle) use ($toggleManager, $context) { 98 | return [ 99 | 'name' => $toggle->getName(), 100 | 'active' => $toggleManager->active($toggle->getName(), $context), 101 | ]; 102 | }, $toggleManager->all()); 103 | 104 | return new JsonResponse([ 105 | 'context' => $context->toArray(), 106 | 'toggles' => $output, 107 | ]); 108 | } 109 | } 110 | 111 | $kernel = new AppKernel('dev', true); 112 | $request = Request::createFromGlobals(); 113 | $response = $kernel->handle($request); 114 | $response->send(); 115 | $kernel->terminate($request, $response); 116 | -------------------------------------------------------------------------------- /Resources/views/data_collector/toggle.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | 5 | {% set icon %} 6 | Toggle 7 | {% endset %} 8 | 9 | {% set text %} 10 | {% for toggle in collector.toggleDetails %} 11 |
12 | {{ toggle.name }} 13 | {% if toggle.name is active feature %} 14 | active 15 | {% else %} 16 | inactive 17 | {% endif %} 18 |
19 | {% endfor %} 20 | {% endset %} 21 | 22 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig') }} 23 | 24 | {% endblock %} 25 | 26 | {% block menu %} 27 | 28 | Toggle 29 | 30 | {% endblock %} 31 | 32 | {% block panel %} 33 |

Context

34 | 35 | {% if collector.context.toArray|length %} 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {% for contextName, contextValue in collector.context.toArray %} 45 | 46 | 47 | 48 | 49 | {% endfor %} 50 | 51 |
ContextValue
{{ contextName }}{{ contextValue }}
52 | {% else %} 53 |
54 |

No context data found.

55 |
56 | {% endif %} 57 | 58 |

Toggles

59 | 60 | {% if collector.toggleDetails|length %} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | {% for toggleDetails in collector.toggleDetails %} 72 | 73 | 74 | 75 | 76 | {% if toggleDetails.name is active feature %} 77 | 78 | {% else %} 79 | 80 | {% endif %} 81 | 82 | {% endfor %} 83 | 84 |
Toggle nameConditionsStatusCurrent Status
{{ toggleDetails.name }}{{ block('toggle_detail_conditions') }}{{ toggleDetails.status }}activeinactive
85 | {% else %} 86 |
87 |

No toggle definition found.

88 |
89 | {% endif %} 90 | {% endblock %} 91 | 92 | {% block toggle_detail_conditions %} 93 | {% for condition in toggleDetails.conditions %} 94 | {% set values = '' %} 95 | {% if condition.operator.value is defined %} 96 | {% set values = condition.operator.value %} 97 | {% endif %} 98 | 99 | {% if condition.operator.values is defined %} 100 | {% set values = condition.operator.values|join(', ') %} 101 | {% endif %} 102 | 103 | {{ condition.name }}: {{ condition.key }} {{ condition.operator.name }} {{ values }} 104 | {% if not loop.last %}
{% endif %} 105 | {% else %} 106 | No conditions 107 | {% endfor %} 108 | {% endblock %} 109 | -------------------------------------------------------------------------------- /Tests/Context/UserContextFactoryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests\Context; 15 | 16 | use PHPUnit\Framework\TestCase; 17 | use Qandidate\Bundle\ToggleBundle\Context\UserContextFactory; 18 | use Qandidate\Bundle\ToggleBundle\Tests\TokenStorage; 19 | use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; 20 | 21 | class UserContextFactoryTest extends TestCase 22 | { 23 | public function setUp(): void 24 | { 25 | $this->tokenStorage = new TokenStorage(); 26 | $this->contextFactory = new UserContextFactory($this->tokenStorage); 27 | } 28 | 29 | /** 30 | * @test 31 | */ 32 | public function it_should_set_the_username_when_available() 33 | { 34 | $this->tokenStorage->setToken(new AnonymousToken('key', 'foobar')); 35 | 36 | $this->assertEquals('foobar', $this->contextFactory->createContext()->get('username')); 37 | } 38 | 39 | /** 40 | * @test 41 | */ 42 | public function it_should_not_set_the_username_when_token_is_unavailable() 43 | { 44 | $this->assertFalse($this->contextFactory->createContext()->has('username')); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests\DependencyInjection; 15 | 16 | use Matthias\SymfonyConfigTest\PhpUnit\ConfigurationTestCaseTrait; 17 | use PHPUnit\Framework\TestCase; 18 | use Qandidate\Bundle\ToggleBundle\DependencyInjection\Configuration; 19 | use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; 20 | 21 | class ConfigurationTest extends TestCase 22 | { 23 | use ConfigurationTestCaseTrait; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function getConfiguration() 29 | { 30 | return new Configuration(); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function it_accepts_empty_configuration_and_configures_defaults() 37 | { 38 | $this->assertProcessedConfigurationEquals( 39 | [ 40 | [], 41 | ], 42 | [ 43 | 'persistence' => 'in_memory', 44 | 'context_factory' => null, 45 | 'redis_namespace' => 'toggle_%kernel.environment%', 46 | 'redis_client' => null, 47 | 'toggles' => [], 48 | ] 49 | ); 50 | } 51 | 52 | /** 53 | * @test 54 | */ 55 | public function it_defaults_to_in_memory_persistence() 56 | { 57 | $this->assertProcessedConfigurationEquals( 58 | [ 59 | [], 60 | ], 61 | [ 62 | 'persistence' => 'in_memory', 63 | ], 64 | 'persistence' 65 | ); 66 | } 67 | 68 | /** 69 | * @test 70 | */ 71 | public function it_configures_toggles_without_conditions() 72 | { 73 | $this->assertProcessedConfigurationEquals( 74 | [ 75 | [ 76 | 'toggles' => [ 77 | 'always-active-feature' => [ 78 | 'name' => 'always-active-feature', 79 | 'status' => 'always-active', 80 | ], 81 | 'inactive-feature' => [ 82 | 'name' => 'inactive-feature', 83 | 'status' => 'inactive', 84 | ], 85 | ], 86 | ], 87 | ], 88 | [ 89 | 'toggles' => [ 90 | 'always_active_feature' => [ 91 | 'name' => 'always-active-feature', 92 | 'status' => 'always-active', 93 | 'conditions' => [], 94 | ], 95 | 'inactive_feature' => [ 96 | 'name' => 'inactive-feature', 97 | 'status' => 'inactive', 98 | 'conditions' => [], 99 | ], 100 | ], 101 | ], 102 | 'toggles' 103 | ); 104 | } 105 | 106 | /** 107 | * @test 108 | */ 109 | public function it_configures_toggles_with_conditions() 110 | { 111 | $this->assertProcessedConfigurationEquals( 112 | [ 113 | [ 114 | 'toggles' => [ 115 | 'conditionally-active' => [ 116 | 'name' => 'conditionally-active', 117 | 'status' => 'conditionally-active', 118 | 'conditions' => [ 119 | [ 120 | 'name' => 'operator-condition', 121 | 'key' => 'user_id', 122 | 'operator' => [ 123 | 'name' => 'greater-than', 124 | 'value' => 42, 125 | ], 126 | ], 127 | ], 128 | ], 129 | ], 130 | ], 131 | ], 132 | [ 133 | 'toggles' => [ 134 | 'conditionally_active' => [ 135 | 'name' => 'conditionally-active', 136 | 'status' => 'conditionally-active', 137 | 'conditions' => [ 138 | [ 139 | 'name' => 'operator-condition', 140 | 'key' => 'user_id', 141 | 'operator' => [ 142 | 'name' => 'greater-than', 143 | 'value' => 42, 144 | ], 145 | ], 146 | ], 147 | ], 148 | ], 149 | ], 150 | 'toggles' 151 | ); 152 | } 153 | 154 | /** 155 | * @test 156 | */ 157 | public function it_configures_toggles_with_inset_operator() 158 | { 159 | $this->assertProcessedConfigurationEquals( 160 | [ 161 | [ 162 | 'toggles' => [ 163 | 'conditionally-active' => [ 164 | 'name' => 'conditionally-active', 165 | 'status' => 'conditionally-active', 166 | 'conditions' => [ 167 | [ 168 | 'name' => 'operator-condition', 169 | 'key' => 'user_id', 170 | 'operator' => [ 171 | 'name' => 'greater-than', 172 | 'values' => [41, 42], 173 | ], 174 | ], 175 | ], 176 | ], 177 | ], 178 | ], 179 | ], 180 | [ 181 | 'toggles' => [ 182 | 'conditionally_active' => [ 183 | 'name' => 'conditionally-active', 184 | 'status' => 'conditionally-active', 185 | 'conditions' => [ 186 | [ 187 | 'name' => 'operator-condition', 188 | 'key' => 'user_id', 189 | 'operator' => [ 190 | 'name' => 'greater-than', 191 | 'values' => [41, 42], 192 | ], 193 | ], 194 | ], 195 | ], 196 | ], 197 | ], 198 | 'toggles' 199 | ); 200 | } 201 | 202 | /** 203 | * @test 204 | */ 205 | public function it_configures_toggles_with_unanimous_strategy() 206 | { 207 | $this->assertProcessedConfigurationEquals( 208 | [ 209 | [ 210 | 'toggles' => [ 211 | 'conditionally-active' => [ 212 | 'name' => 'conditionally-active', 213 | 'status' => 'conditionally-active', 214 | 'strategy' => 'unanimous', 215 | 'conditions' => [ 216 | [ 217 | 'name' => 'operator-condition', 218 | 'key' => 'env', 219 | 'operator' => [ 220 | 'name' => 'in-set', 221 | 'values' => ['dev'], 222 | ], 223 | ], 224 | [ 225 | 'name' => 'operator-condition', 226 | 'key' => 'roles', 227 | 'operator' => [ 228 | 'name' => 'has-intersection', 229 | 'values' => ['ROLE_SOME_ROLE'], 230 | ], 231 | ], 232 | ], 233 | ], 234 | ], 235 | ], 236 | ], 237 | [ 238 | 'toggles' => [ 239 | 'conditionally_active' => [ 240 | 'name' => 'conditionally-active', 241 | 'status' => 'conditionally-active', 242 | 'strategy' => 'unanimous', 243 | 'conditions' => [ 244 | [ 245 | 'name' => 'operator-condition', 246 | 'key' => 'env', 247 | 'operator' => [ 248 | 'name' => 'in-set', 249 | 'values' => ['dev'], 250 | ], 251 | ], 252 | [ 253 | 'name' => 'operator-condition', 254 | 'key' => 'roles', 255 | 'operator' => [ 256 | 'name' => 'has-intersection', 257 | 'values' => ['ROLE_SOME_ROLE'], 258 | ], 259 | ], 260 | ], 261 | ], 262 | ], 263 | ], 264 | 'toggles' 265 | ); 266 | } 267 | 268 | /** 269 | * @test 270 | */ 271 | public function it_requires_collection_factory_to_be_set_when_persistence_is_factory() 272 | { 273 | $this->expectException(InvalidConfigurationException::class); 274 | $this->expectExceptionMessage('Invalid configuration for path "qandidate_toggle": When choosing "factory" persistence make sure you set "collection_factory.service_id" and "collection_factory.method"'); 275 | 276 | $this->assertProcessedConfigurationEquals( 277 | [ 278 | [ 279 | 'persistence' => 'factory', 280 | ], 281 | ], 282 | [ 283 | 'persistence' => 'factory', 284 | ], 285 | 'persistence' 286 | ); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/QandidateToggleExtensionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests\DependencyInjection; 15 | 16 | use Doctrine\Common\Annotations\AnnotationReader; 17 | use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; 18 | use Qandidate\Bundle\ToggleBundle\DependencyInjection\QandidateToggleExtension; 19 | use Qandidate\Bundle\ToggleBundle\Tests\TokenStorage; 20 | use Qandidate\Toggle\Toggle; 21 | use Qandidate\Toggle\ToggleCollection\InMemoryCollection; 22 | use Qandidate\Toggle\ToggleCollection\PredisCollection; 23 | use Qandidate\Toggle\ToggleManager; 24 | 25 | class QandidateToggleExtensionTest extends AbstractExtensionTestCase 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function getContainerExtensions(): array 31 | { 32 | return [ 33 | new QandidateToggleExtension(), 34 | ]; 35 | } 36 | 37 | /** 38 | * @test 39 | * 40 | * @doesNotPerformAssertions 41 | */ 42 | public function it_builds_the_container_with_empty_config() 43 | { 44 | $this->load(); 45 | } 46 | 47 | /** 48 | * @test 49 | */ 50 | public function it_aliases_the_in_memory_collection_by_default() 51 | { 52 | $this->load(); 53 | $this->assertContainerBuilderHasAlias('qandidate.toggle.collection', 'qandidate.toggle.collection.in_memory'); 54 | } 55 | 56 | /** 57 | * @test 58 | */ 59 | public function it_aliases_the_redis_collection_when_configured() 60 | { 61 | $this->load([ 62 | 'persistence' => 'redis', 63 | 'redis_namespace' => 'toggle', 64 | 'redis_client' => 'redis_client', 65 | ]); 66 | 67 | $this->assertContainerBuilderHasAlias('qandidate.toggle.collection', 'qandidate.toggle.collection.predis'); 68 | $this->assertContainerBuilderHasAlias('qandidate.toggle.redis.client', 'redis_client'); 69 | $this->assertContainerBuilderHasParameter('qandidate.toggle.redis.namespace', 'toggle'); 70 | } 71 | 72 | /** 73 | * @test 74 | */ 75 | public function it_loads_the_redis_service_file_when_configuring_the_redis_collection() 76 | { 77 | $this->load([ 78 | 'persistence' => 'redis', 79 | 'redis_namespace' => 'toggle', 80 | 'redis_client' => 'redis_client', 81 | ]); 82 | 83 | $this->assertContainerBuilderHasService('qandidate.toggle.collection.predis', PredisCollection::class); 84 | } 85 | 86 | /** 87 | * @test 88 | */ 89 | public function it_sets_the_default_redis_namespace() 90 | { 91 | $this->load([ 92 | 'persistence' => 'redis', 93 | 'redis_client' => 'redis_client', 94 | ]); 95 | 96 | $this->assertContainerBuilderHasParameter('qandidate.toggle.redis.namespace', 'toggle_%kernel.environment%'); 97 | } 98 | 99 | /** 100 | * @test 101 | */ 102 | public function it_creates_the_toggle_collection_factory_definition() 103 | { 104 | $this->load([ 105 | 'persistence' => 'factory', 106 | 'collection_factory' => [ 107 | 'service_id' => 'factory.service.id', 108 | 'method' => 'create', 109 | ], 110 | ]); 111 | 112 | $definition = $this->container->getDefinition('qandidate.toggle.collection.factory'); 113 | $factory = $definition->getFactory(); 114 | $this->assertSame(InMemoryCollection::class, $definition->getClass()); 115 | $this->assertArrayHasKey(0, $factory); 116 | $this->assertArrayHasKey(1, $factory); 117 | $this->assertInstanceOf('Symfony\Component\DependencyInjection\Reference', $factory[0]); 118 | $this->assertSame('factory.service.id', (string) $factory[0]); 119 | $this->assertSame('create', $factory[1]); 120 | } 121 | 122 | /** 123 | * @test 124 | */ 125 | public function it_registers_the_manager() 126 | { 127 | $this->load(); 128 | 129 | $this->assertContainerBuilderHasServiceDefinitionWithArgument('qandidate.toggle.manager', 0, 'qandidate.toggle.collection'); 130 | $this->assertContainerBuilderHasService('qandidate.toggle.manager', ToggleManager::class); 131 | } 132 | 133 | /** 134 | * @test 135 | */ 136 | public function it_registers_the_twig_extension() 137 | { 138 | $this->load(); 139 | 140 | $this->assertContainerBuilderHasServiceDefinitionWithTag('qandidate.toggle.twig_extension', 'twig.extension'); 141 | } 142 | 143 | /** 144 | * @test 145 | */ 146 | public function it_creates_the_context_factory_alias() 147 | { 148 | $this->load(); 149 | 150 | $this->assertContainerBuilderHasAlias('qandidate.toggle.context_factory', 'qandidate.toggle.user_context_factory'); 151 | } 152 | 153 | /** 154 | * @test 155 | */ 156 | public function it_aliases_the_context_factory_to_configured_service() 157 | { 158 | $this->load([ 159 | 'context_factory' => 'acme.yolo', 160 | ]); 161 | 162 | $this->assertContainerBuilderHasAlias('qandidate.toggle.context_factory', 'acme.yolo'); 163 | } 164 | 165 | /** 166 | * @test 167 | */ 168 | public function it_creates_a_toggle_collection_from_config() 169 | { 170 | $this->load([ 171 | 'persistence' => 'config', 172 | 'toggles' => [ 173 | 'some_feature' => [ 174 | 'name' => 'some_feature', 175 | 'status' => 'conditionally-active', 176 | ], 177 | ], 178 | ]); 179 | 180 | $this->registerService('security.token_storage', TokenStorage::class); 181 | $this->registerService('annotation_reader', AnnotationReader::class); 182 | 183 | $this->compile(); 184 | $toggleCollection = $this->container->get('qandidate.toggle.collection'); 185 | $this->assertInstanceOf(InMemoryCollection::class, $toggleCollection); 186 | 187 | $toggles = $toggleCollection->all(); 188 | $this->assertCount(1, $toggles); 189 | 190 | $this->assertEquals(new Toggle('some_feature', []), $toggles['some_feature']); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Tests/EventListener/Fixture/FooControllerToggleAtClassAndMethod.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests\EventListener\Fixture; 15 | 16 | use Qandidate\Bundle\ToggleBundle\Annotations\Toggle; 17 | 18 | /** 19 | * @Toggle("cool-feature") 20 | */ 21 | class FooControllerToggleAtClassAndMethod 22 | { 23 | public const METHOD_EXECUTED = 'method.executed'; 24 | 25 | /** 26 | * @Toggle("another-cool-feature") 27 | */ 28 | public function barAction() 29 | { 30 | return self::METHOD_EXECUTED; 31 | } 32 | 33 | public function bazAction() 34 | { 35 | return self::METHOD_EXECUTED; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/EventListener/Fixture/FooControllerToggleAtInvoke.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests\EventListener\Fixture; 15 | 16 | use Qandidate\Bundle\ToggleBundle\Annotations\Toggle; 17 | 18 | class FooControllerToggleAtInvoke 19 | { 20 | /** 21 | * @Toggle("cool-feature-on-invoke") 22 | */ 23 | public function __invoke() 24 | { 25 | return 'method.executed'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/EventListener/ToggleListenerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests\EventListener; 15 | 16 | use Doctrine\Common\Annotations\AnnotationReader; 17 | use PHPUnit\Framework\TestCase; 18 | use Qandidate\Bundle\ToggleBundle\Annotations\Toggle; 19 | use Qandidate\Bundle\ToggleBundle\EventListener\ToggleListener; 20 | use Qandidate\Bundle\ToggleBundle\Tests\EventListener\Fixture\FooControllerToggleAtClassAndMethod; 21 | use Qandidate\Bundle\ToggleBundle\Tests\EventListener\Fixture\FooControllerToggleAtInvoke; 22 | use Qandidate\Toggle\Context; 23 | use Symfony\Component\HttpFoundation\Request; 24 | use Symfony\Component\HttpKernel\Event\ControllerEvent; 25 | use Symfony\Component\HttpKernel\HttpKernelInterface; 26 | 27 | class ToggleListenerTest extends TestCase 28 | { 29 | public function setUp(): void 30 | { 31 | $this->request = $this->createRequest(); 32 | } 33 | 34 | public function tearDown(): void 35 | { 36 | $this->listener = null; 37 | $this->request = null; 38 | } 39 | 40 | public function test_inactive_toggle_annotation_at_method() 41 | { 42 | $this->expectException('Symfony\Component\HttpKernel\Exception\NotFoundHttpException'); 43 | $this->listener = $this->createListener(false); 44 | $controller = new FooControllerToggleAtClassAndMethod(); 45 | 46 | $this->event = $this->getControllerEvent([$controller, 'barAction'], $this->request); 47 | $this->listener->onKernelController($this->event); 48 | } 49 | 50 | public function test_inactive_toggle_annotation_at_class() 51 | { 52 | $this->expectException('Symfony\Component\HttpKernel\Exception\NotFoundHttpException'); 53 | $this->listener = $this->createListener(false); 54 | $controller = new FooControllerToggleAtClassAndMethod(); 55 | 56 | $this->event = $this->getControllerEvent([$controller, 'bazAction'], $this->request); 57 | $this->listener->onKernelController($this->event); 58 | } 59 | 60 | public function test_active_toggle_annotation_at_method() 61 | { 62 | $this->listener = $this->createListener(true); 63 | $controller = new FooControllerToggleAtClassAndMethod(); 64 | 65 | $this->event = $this->getControllerEvent([$controller, 'barAction'], $this->request); 66 | $this->listener->onKernelController($this->event); 67 | // If we end up here toggle is active, no exception thrown 68 | $this->assertTrue(true); 69 | } 70 | 71 | public function test_inactive_toggle_annotation_at_invoke() 72 | { 73 | $this->expectException('Symfony\Component\HttpKernel\Exception\NotFoundHttpException'); 74 | $this->listener = $this->createListener(false); 75 | $controller = new FooControllerToggleAtInvoke(); 76 | 77 | $this->event = $this->getControllerEvent($controller, $this->request); 78 | $this->listener->onKernelController($this->event); 79 | } 80 | 81 | public function test_active_toggle_annotation_at_invoke() 82 | { 83 | $this->listener = $this->createListener(true); 84 | $controller = new FooControllerToggleAtInvoke(); 85 | 86 | $this->event = $this->getControllerEvent($controller, $this->request); 87 | $this->listener->onKernelController($this->event); 88 | // If we end up here toggle is active, no exception thrown 89 | $this->assertTrue(true); 90 | } 91 | 92 | protected function createToggleManager($isToggleActive) 93 | { 94 | $toggleManager = $this->createMock('Qandidate\Toggle\ToggleManager'); 95 | 96 | $toggleManager->method('active') 97 | ->willReturn($isToggleActive); 98 | 99 | return $toggleManager; 100 | } 101 | 102 | protected function createListener($isToggleActive) 103 | { 104 | $toggleManager = $this->createToggleManager($isToggleActive); 105 | 106 | return new ToggleListener(new AnnotationReader(), $toggleManager, new Context()); 107 | } 108 | 109 | protected function createRequest() 110 | { 111 | return new Request([], [], []); 112 | } 113 | 114 | protected function getControllerEvent($controller, Request $request) 115 | { 116 | $mockKernel = $this->getMockForAbstractClass('Symfony\Component\HttpKernel\Kernel', ['test', '']); 117 | 118 | return new ControllerEvent($mockKernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Tests/Fixtures/tests/feature_active.test: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | "foobar" is active feature 3 | --TEMPLATE-- 4 | {{ 'foo' is active feature ? 'yes' : 'no'}} 5 | {{ 'bar' is active feature ? 'yes' : 'no'}} 6 | {{ 'foobar' is active feature ? 'yes' : 'no'}} 7 | {% set baz = 'foo' %} 8 | {{ baz is active feature ? 'yes' : 'no'}} 9 | {{ feature_is_active('foo') ? 'yes' : 'no' }} 10 | --DATA-- 11 | return array() 12 | --EXPECT-- 13 | yes 14 | no 15 | no 16 | yes 17 | yes 18 | -------------------------------------------------------------------------------- /Tests/Functional/AppKernel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests\Functional; 15 | 16 | use Qandidate\Bundle\ToggleBundle\QandidateToggleBundle; 17 | use Symfony\Bundle\FrameworkBundle\FrameworkBundle; 18 | use Symfony\Component\Config\Loader\LoaderInterface; 19 | use Symfony\Component\HttpKernel\Kernel; 20 | 21 | class AppKernel extends Kernel 22 | { 23 | public function registerBundles() 24 | { 25 | return [ 26 | new FrameworkBundle(), 27 | new QandidateToggleBundle(), 28 | ]; 29 | } 30 | 31 | public function registerContainerConfiguration(LoaderInterface $loader) 32 | { 33 | $loader->load(__DIR__.'/Resources/config/test.yml'); 34 | $loader->load(__DIR__.'/Resources/config/test_services.xml'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/Functional/ContextFactoryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests\Functional; 15 | 16 | use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; 17 | 18 | class ContextFactoryTest extends WebTestCase 19 | { 20 | public function setUp(): void 21 | { 22 | parent::setUp(); 23 | 24 | $this->client = static::createClient(); 25 | $this->client->getContainer()->get('security.token_storage')->setToken($this->createSecurityToken()); 26 | } 27 | 28 | /** 29 | * @test 30 | * 31 | * @doesNotPerformAssertions 32 | */ 33 | public function it_has_the_factory_service() 34 | { 35 | $this->client->getContainer()->get('qandidate.toggle.user_context_factory'); 36 | } 37 | 38 | /** 39 | * @test 40 | */ 41 | public function it_should_use_the_username_from_the_security_context() 42 | { 43 | $context = $this->client->getContainer()->get('qandidate.toggle.user_context_factory')->createContext(); 44 | 45 | $this->assertEquals('fooUser', $context->get('username')); 46 | } 47 | 48 | private function createSecurityToken() 49 | { 50 | return new AnonymousToken('userKey', 'fooUser', ['ROLE_USER']); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/Functional/Resources/config/test.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | kernel.secret: 'yolo' 3 | 4 | framework: 5 | test: ~ 6 | -------------------------------------------------------------------------------- /Tests/Functional/Resources/config/test_services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Tests/Functional/WebTestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests\Functional; 15 | 16 | use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase; 17 | 18 | class WebTestCase extends BaseWebTestCase 19 | { 20 | protected static function getKernelClass() 21 | { 22 | return AppKernel::class; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/TokenStorage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests; 15 | 16 | use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; 17 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 18 | 19 | class TokenStorage implements TokenStorageInterface 20 | { 21 | private $token; 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function getToken() 27 | { 28 | return $this->token; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function setToken(TokenInterface $token = null) 35 | { 36 | $this->token = $token; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function isGranted($attributes, $object = null) 43 | { 44 | return null !== $this->token; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/Twig/ToggleTwigExtensionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests\Twig; 15 | 16 | use PHPUnit\Framework\TestCase; 17 | use Qandidate\Bundle\ToggleBundle\Twig\ToggleTwigExtension; 18 | use Qandidate\Toggle\Context; 19 | use Qandidate\Toggle\Toggle; 20 | use Qandidate\Toggle\ToggleCollection\InMemoryCollection; 21 | use Qandidate\Toggle\ToggleManager; 22 | use Twig\TwigFunction; 23 | 24 | class ToggleTwigExtensionTest extends TestCase 25 | { 26 | private $contextFactory; 27 | private $extension; 28 | private $toggleManager; 29 | 30 | public function setUp(): void 31 | { 32 | $this->toggleManager = new ToggleManager(new InMemoryCollection()); 33 | $this->contextFactory = $this->getMockBuilder('Qandidate\Toggle\ContextFactory') 34 | ->disableOriginalConstructor() 35 | ->setMethods(['createContext']) 36 | ->getMock(); 37 | 38 | $this->extension = new ToggleTwigExtension($this->toggleManager, $this->contextFactory); 39 | } 40 | 41 | /** 42 | * @test 43 | */ 44 | public function it_should_provide_an_is_active_function() 45 | { 46 | $functions = $this->extension->getFunctions(); 47 | 48 | $this->assertCount(1, $functions); 49 | $this->assertInstanceof(TwigFunction::class, $functions[0]); 50 | $this->assertEquals('feature_is_active', $functions[0]->getName()); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function it_should_return_if_a_toggle_is_active() 57 | { 58 | $this->contextFactory 59 | ->expects($this->any()) 60 | ->method('createContext') 61 | ->will($this->returnValue($this->createEmptyContext())); 62 | 63 | $this->assertFalse($this->extension->is_active('foo')); 64 | 65 | $this->toggleManager->add($this->createToggle('foo', Toggle::ALWAYS_ACTIVE)); 66 | 67 | $this->assertTrue($this->extension->is_active('foo')); 68 | } 69 | 70 | private function createEmptyContext() 71 | { 72 | return new Context(); 73 | } 74 | 75 | private function createToggle($name, $status, array $conditions = []) 76 | { 77 | $toggle = new Toggle($name, $conditions); 78 | $toggle->activate($status); 79 | 80 | return $toggle; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/TwigIntegrationTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Tests; 15 | 16 | use Qandidate\Bundle\ToggleBundle\Twig\ToggleTwigExtension; 17 | use Qandidate\Toggle\Context; 18 | use Qandidate\Toggle\ContextFactory; 19 | use Qandidate\Toggle\Toggle; 20 | use Qandidate\Toggle\ToggleCollection\InMemoryCollection; 21 | use Qandidate\Toggle\ToggleManager; 22 | use Twig\Test\IntegrationTestCase; 23 | 24 | class TwigIntegrationTest extends IntegrationTestCase 25 | { 26 | private $contextFactory; 27 | private $toggleManager; 28 | 29 | public function setUp(): void 30 | { 31 | $toggleCollection = new InMemoryCollection(); 32 | $toggleCollection->set('foo', $this->createToggle('foo', true)); 33 | $toggleCollection->set('bar', $this->createToggle('foo', false)); 34 | 35 | $this->contextFactory = new StubContextFactory(); 36 | $this->toggleManager = new ToggleManager($toggleCollection); 37 | } 38 | 39 | public function getExtensions() 40 | { 41 | return [ 42 | new ToggleTwigExtension($this->toggleManager, $this->contextFactory), 43 | ]; 44 | } 45 | 46 | public function getFixturesDir() 47 | { 48 | return __DIR__.'/Fixtures/'; 49 | } 50 | 51 | private function createToggle($name, $active) 52 | { 53 | $toggle = new Toggle($name, []); 54 | 55 | if ($active) { 56 | $toggle->activate(Toggle::ALWAYS_ACTIVE); 57 | } else { 58 | $toggle->deactivate(); 59 | } 60 | 61 | return $toggle; 62 | } 63 | } 64 | 65 | class StubContextFactory extends ContextFactory 66 | { 67 | public function createContext(): Context 68 | { 69 | return new Context(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Twig/ToggleTwigExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Qandidate\Bundle\ToggleBundle\Twig; 15 | 16 | use Qandidate\Toggle\ContextFactory; 17 | use Qandidate\Toggle\ToggleManager; 18 | use Twig\Extension\AbstractExtension; 19 | use Twig\TwigFunction; 20 | use Twig\TwigTest; 21 | 22 | class ToggleTwigExtension extends AbstractExtension 23 | { 24 | private $contextFactory; 25 | private $toggleManager; 26 | 27 | public function __construct(ToggleManager $toggleManager, ContextFactory $contextFactory) 28 | { 29 | $this->toggleManager = $toggleManager; 30 | $this->contextFactory = $contextFactory; 31 | } 32 | 33 | public function is_active(string $name): bool 34 | { 35 | return $this->toggleManager->active($name, $this->contextFactory->createContext()); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function getFunctions() 42 | { 43 | return [ 44 | new TwigFunction('feature_is_active', [$this, 'is_active']), 45 | ]; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function getTests() 52 | { 53 | return [ 54 | new TwigTest('active feature', [$this, 'is_active']), 55 | ]; 56 | } 57 | 58 | public function getName(): string 59 | { 60 | return 'qandidate_toggle_twig_extension'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qandidate/toggle-bundle", 3 | "description": "This Bundle provides the integration with qandidate/toggle. It provides the services and configuration you need to implement feature toggles in your Symfony application.", 4 | "type": "symfony-bundle", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Willem-Jan", 9 | "email": "wjzijderveld@gmail.com" 10 | }, 11 | { 12 | "name": "Qandidate.com", 13 | "homepage": "http://labs.qandidate.com" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "Qandidate\\Bundle\\ToggleBundle\\": "" 19 | }, 20 | "exclude-from-classmap": [ 21 | "/Tests/" 22 | ] 23 | }, 24 | "require": { 25 | "php": ">=7.2", 26 | "qandidate/toggle": "^2.0", 27 | "symfony/framework-bundle": "^4.4||^5.0", 28 | "symfony/http-foundation": "^4.4.7||^5.0.7", 29 | "symfony/http-kernel": "^4.4.13||^5.1.5", 30 | "symfony/security-bundle": "^4.4||^5.0", 31 | "doctrine/common":"^2.13||^3.0", 32 | "doctrine/annotations": "^1.13" 33 | }, 34 | "require-dev": { 35 | "symfony/browser-kit": "^4.4||^5.0", 36 | "twig/twig": "^3.0", 37 | "symfony/twig-bundle": "^4.4||^5.0", 38 | "phpunit/phpunit": "^9.5", 39 | "matthiasnoback/symfony-dependency-injection-test": "^4.0", 40 | "matthiasnoback/symfony-config-test": "^4.0", 41 | "broadway/coding-standard": "^1.2", 42 | "phpstan/phpstan": "^1.0" 43 | }, 44 | "suggest": { 45 | "twig/twig": "For using the twig helper" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:children\\(\\)\\.$#" 5 | count: 1 6 | path: DependencyInjection/Configuration.php 7 | 8 | - 9 | message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\\\|T of object, callable given\\.$#" 10 | count: 1 11 | path: EventListener/ToggleListener.php 12 | 13 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - . 4 | excludes_analyse: 5 | - Tests/ 6 | - vendor/ 7 | - var/ 8 | - Resources/doc/example/ 9 | checkMissingIterableValueType: false 10 | inferPrivatePropertyTypeFromConstructor: true 11 | 12 | includes: 13 | - phpstan-baseline.neon 14 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./Tests/ 16 | 17 | 18 | 19 | --------------------------------------------------------------------------------