├── Annotations ├── Driver │ └── FeatureCheckerDriver.php └── MustHaveFeature.php ├── Controller └── ExceptionController.php ├── DependencyInjection ├── Configuration.php └── FeatureCheckerExtension.php ├── EventListener └── ExceptionListener.php ├── Exception └── FeatureNotActivatedException.php ├── FeatureCheckerBundle.php ├── LICENSE.md ├── Resources ├── config │ └── services.yml ├── doc │ ├── annotations.md │ ├── configuration.md │ ├── index.md │ ├── installation.md │ ├── overriding_controller.md │ ├── overriding_template.md │ ├── service.md │ └── twig_variables.md ├── meta │ └── LICENSE └── views │ └── exception │ └── featureNotActivated.html.twig ├── Service └── FeatureChecker.php ├── Twig └── GlobalsExtension.php └── composer.json /Annotations/Driver/FeatureCheckerDriver.php: -------------------------------------------------------------------------------- 1 | annotationReader = $annotationReader; 38 | $this->checker = new FeatureChecker($featuresConfiguration); 39 | } 40 | 41 | /** 42 | * This event will fire during any controller call 43 | * 44 | * @param FilterControllerEvent $event 45 | * @return void 46 | * @throws FeatureNotActivatedException 47 | */ 48 | public function onFilterController(FilterControllerEvent $event) 49 | { 50 | $featureCheckerAnnotations = $this->getFeatureCheckerAnnotations($event); 51 | 52 | foreach ($featureCheckerAnnotations as $featureCheckerAnnotation) { 53 | $featureName = $featureCheckerAnnotation->getFeatureName(); 54 | 55 | if (!$this->checker->isFeatureEnabled($featureName)) { 56 | throw new FeatureNotActivatedException($featureName); 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Get action feature checker annotations from event 63 | * 64 | * @param FilterControllerEvent $event 65 | * @return MustHaveFeature[] 66 | */ 67 | protected function getFeatureCheckerAnnotations(FilterControllerEvent $event) 68 | { 69 | list($object, $method) = $event->getController(); 70 | 71 | // the controller could be a proxy, 72 | // e.g. when using the JMSSecuriyExtraBundle or JMSDiExtraBundle 73 | $className = ClassUtils::getClass($object); 74 | 75 | $reflectionClass = new \ReflectionClass($className); 76 | $reflectionMethod = $reflectionClass->getMethod($method); 77 | 78 | $allAnnotations = $this->annotationReader->getMethodAnnotations($reflectionMethod); 79 | 80 | $allAnnotations = is_array($allAnnotations) ? $allAnnotations : array(); 81 | 82 | // Filter FeatureChecker annotation, especially MustHaveFeature 83 | return array_filter($allAnnotations, function($annotation) { 84 | return $annotation instanceof MustHaveFeature; 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Annotations/MustHaveFeature.php: -------------------------------------------------------------------------------- 1 | $value) { 34 | if (!property_exists($this, $key)) { 35 | throw new \InvalidArgumentException(sprintf('Property "%s" does not exist.', $key)); 36 | } 37 | 38 | $this->$key = $value; 39 | } 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getFeatureName() 46 | { 47 | return $this->featureName; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Controller/ExceptionController.php: -------------------------------------------------------------------------------- 1 | render('FeatureCheckerBundle:exception:featureNotActivated.html.twig', array( 24 | 'message' => $exception->getMessage(), 25 | 'featureName' => $exception->getFeatureName(), 26 | )); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('feature_checker'); 22 | 23 | $rootNode 24 | ->children() 25 | ->booleanNode('disable_undefined') 26 | ->defaultFalse() 27 | ->info('If true, undefined feature will be considered as disabled.') 28 | ->end() 29 | ->arrayNode('features') 30 | ->prototype('variable')->end() 31 | ->end() 32 | ->end() 33 | ; 34 | 35 | return $treeBuilder; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DependencyInjection/FeatureCheckerExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 26 | 27 | $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 28 | $loader->load('services.yml'); 29 | 30 | $container->setParameter($this->getAlias().'.disable_undefined', $config['disable_undefined']); 31 | 32 | // Sets features list in container 33 | $container->setParameter($this->getAlias().'.features', $config['features']); 34 | } 35 | 36 | /** 37 | * The extension alias 38 | * 39 | * @return string 40 | */ 41 | public function getAlias() 42 | { 43 | return 'feature_checker'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /EventListener/ExceptionListener.php: -------------------------------------------------------------------------------- 1 | disableUndefined = $disableUndefined; 26 | } 27 | 28 | 29 | /** 30 | * Makes exception controller handle response 31 | * 32 | * @param GetResponseForExceptionEvent $event 33 | * @return void 34 | */ 35 | public function onKernelException(GetResponseForExceptionEvent $event) 36 | { 37 | $exception = $event->getException(); 38 | 39 | if ( 40 | (($exception instanceof FeatureNotDefinedException) && !$this->disableUndefined) 41 | || (!($exception instanceof FeatureNotActivatedException) && !($exception instanceof FeatureNotDefinedException)) 42 | ) { 43 | return; 44 | } 45 | 46 | $attributes = array( 47 | '_controller' => 'FeatureCheckerBundle:Exception:notActivated', 48 | 'exception' => new FeatureNotActivatedException($exception->getFeatureName()), 49 | ); 50 | 51 | $subRequest = $event->getRequest()->duplicate(array(), null, $attributes); 52 | $response = $event->getKernel()->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false); 53 | 54 | $event->setResponse($response); // this will stop event propagation 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Exception/FeatureNotActivatedException.php: -------------------------------------------------------------------------------- 1 | featureName = $featureName; 25 | 26 | $message = sprintf("The feature '%s' is not activated.", $featureName); 27 | 28 | parent::__construct($message, 500, null); 29 | } 30 | 31 | /** 32 | * Get feature name 33 | * 34 | * @return string 35 | */ 36 | public function getFeatureName() 37 | { 38 | return $this->featureName; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /FeatureCheckerBundle.php: -------------------------------------------------------------------------------- 1 | extension) { 19 | $this->extension = new FeatureCheckerExtension(); 20 | } 21 | 22 | return $this->extension; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Laurent Wiesel 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | feature_checker.driver: 3 | class: LWI\FeatureCheckerBundle\Annotations\Driver\FeatureCheckerDriver 4 | tags: 5 | - { name: kernel.event_listener, event: kernel.controller, method: onFilterController } 6 | arguments: 7 | - @annotation_reader 8 | - %feature_checker.features% 9 | 10 | feature_checker.exception_listener: 11 | class: LWI\FeatureCheckerBundle\EventListener\ExceptionListener 12 | arguments: 13 | - %feature_checker.disable_undefined% 14 | tags: 15 | - { name: kernel.event_listener, event: kernel.exception, method: onKernelException } 16 | 17 | feature_checker.twig.globals_extension: 18 | class: LWI\FeatureCheckerBundle\Twig\GlobalsExtension 19 | arguments: 20 | - %feature_checker.features% 21 | tags: 22 | - { name: twig.extension } 23 | 24 | feature_checker.checker: 25 | class: LWI\FeatureCheckerBundle\Service\FeatureChecker 26 | arguments: 27 | - %feature_checker.disable_undefined% 28 | - %feature_checker.features% 29 | -------------------------------------------------------------------------------- /Resources/doc/annotations.md: -------------------------------------------------------------------------------- 1 | # Using annotations on actions 2 | 3 | Features can be checked in controller annotations. Only the allowed features will execute the action. 4 | 5 | ``` php 6 | // src/AppBundle/Controller/DefaultController.php 7 | 8 | namespace AppBundle\Controller; 9 | 10 | use LWI\FeatureCheckerBundle\Annotations\MustHaveFeature; 11 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 12 | 13 | class DefaultController extends Controller 14 | { 15 | /** 16 | * @MustHaveFeature("feature1") 17 | */ 18 | public function indexAction() 19 | { 20 | return $this->render('default/index.html.twig'); 21 | } 22 | } 23 | ``` 24 | 25 | Sub-features can be checked with this notation: 26 | 27 | ``` php 28 | /** 29 | * @MustHaveFeature("feature1") 30 | * @MustHaveFeature("feature3.feature31") 31 | */ 32 | public function secondAction() 33 | { 34 | return $this->render('default/second.html.twig'); 35 | } 36 | ``` 37 | 38 | You can also test whole feature sets. A feature set is considered enabled when all sub-features -at any sub-level- is enabled. 39 | 40 | ``` php 41 | /** 42 | * @MustHaveFeature("feature3") 43 | */ 44 | public function thirdAction() 45 | { 46 | return $this->render('default/third.html.twig'); 47 | } 48 | ``` 49 | 50 | Next step: [Using service in actions](service.md) -------------------------------------------------------------------------------- /Resources/doc/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Below is an example of the configuration necessary to use the FeatureCheckerBundle in your application: 4 | 5 | ``` yaml 6 | # app/config/config.yml 7 | 8 | feature_checker: 9 | features: 10 | # List here your features 11 | feature1: true 12 | feature2: false 13 | # You can also nest features 14 | feature3: 15 | feature31: true 16 | ``` 17 | 18 | Next step: [Using annotations on actions](annotations.md) -------------------------------------------------------------------------------- /Resources/doc/index.md: -------------------------------------------------------------------------------- 1 | # FeatureCheckerBundle documentation 2 | 3 | Find here all the documentation about the bundle: 4 | 5 | - [Installation](installation.md) 6 | - [Configuration](configuration.md) 7 | - [Using annotations on actions](annotations.md) 8 | - [Using service in actions](service.md) 9 | - [Using Twig global variables](twig_variables.md) 10 | - [Overriding error template](overriding_template.md) 11 | - [Overriding error controller](overriding_controller.md) -------------------------------------------------------------------------------- /Resources/doc/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | The installation takes 2 steps: 4 | 5 | 1. Download FeatureCheckerBundle using composer 6 | 2. Enable the bundle 7 | 8 | ## Step1: Download FeatureCheckerBundle using composer 9 | Add FeatureCheckerBundle by running the command: 10 | 11 | ``` bash 12 | $ php composer.phar require lwiesel/feature-checker-bundle "~1.1" 13 | ``` 14 | 15 | Composer will install the bundle to your project's `vendor/lwiesel` directory. 16 | 17 | ## Step 2: Enable the bundle 18 | 19 | Enable the bundle in the kernel: 20 | 21 | ``` php 22 | // app/AppKernel.php 23 | 24 | public function registerBundles() 25 | { 26 | $bundles = array( 27 | // ... 28 | new LWI\FeatureCheckerBundle\FeatureCheckerBundle(), 29 | ); 30 | } 31 | ``` 32 | 33 | Next step: [Configuration](configuration.md) -------------------------------------------------------------------------------- /Resources/doc/overriding_controller.md: -------------------------------------------------------------------------------- 1 | # Overriding error controller 2 | 3 | The error controller can be overriden to add features before the rendering of the error template. 4 | 5 | First, create a new bundle and override the `getParent` method in the bundle class. 6 | 7 | ``` php 8 | // src/Acme/FeatureCheckerBundle/FeatureCheckerBundle.php 9 | 10 | namespace Acme\FeatureCheckerBundle; 11 | 12 | use Symfony\Component\HttpKernel\Bundle\Bundle; 13 | 14 | class AcmeUserBundle extends Bundle 15 | { 16 | public function getParent() 17 | { 18 | return 'FeatureCheckerBundle'; 19 | } 20 | } 21 | ``` 22 | 23 | **Note:** 24 | 25 | The Symfony2 framework only allows a bundle to have one child. 26 | You cannot create another bundle that is also a child of FeatureCheckerBundle. 27 | 28 | Now that you have created the new child bundle you can simply create a controller class with the same name and in the same location as the one you want to override. 29 | 30 | ``` php 31 | // src/Acme/FeatureCheckerBundle/Controller/ExceptionController.php 32 | 33 | namespace Acme\FeatureCheckerBundle\Controller; 34 | 35 | use LWI\FeatureCheckerBundle\Exception\FeatureNotActivatedException; 36 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 37 | 38 | /** 39 | * ExceptionController 40 | * 41 | * Handles rendering of error pages 42 | */ 43 | class ExceptionController extends Controller 44 | { 45 | /** 46 | * Feature not activated 47 | * 48 | * @param FeatureNotActivatedException $exception 49 | * @return \Symfony\Component\HttpFoundation\Response 50 | */ 51 | public function notActivatedAction(FeatureNotActivatedException $exception) 52 | { 53 | /* 54 | Do additional code here, change rendering, etc. 55 | */ 56 | 57 | return $this->render('FeatureCheckerBundle:exception:featureNotActivated.html.twig', array( 58 | 'message' => $exception->getMessage(), 59 | 'featureName' => $exception->getFeatureName(), 60 | )); 61 | } 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /Resources/doc/overriding_template.md: -------------------------------------------------------------------------------- 1 | # Overriding error template 2 | 3 | The default error template is very simple : 4 | 5 | ``` twig 6 |

Error with feature {{ featureName }}

7 | 8 |

{{ message }}

9 | ``` 10 | 11 | `featureName` being the tested feature which appeared unabled. 12 | `message` being the default exception message. 13 | 14 | There are 2 ways to override this template: 15 | 1. Define a new template of the same name in the app/Resources directory (easy) 16 | 2. Create a new bundle that is defined as a child of FeatureCheckerBundle (advanced) 17 | 18 | ## Method 1: Define new template in app/Resources 19 | Just add a new file in `app/Resources/FeatureChecker/views/exception`, named `featureNotActivated.html.twig`. 20 | 21 | The `featureName` and `message` will still be accessible in the new template. 22 | 23 | ## Method 2: Create a child bundle 24 | 25 | **Note:** 26 | 27 | This method is more complicated than the one outlined above. 28 | Unless you are planning to override the controllers as well as the 29 | templates, it is recommended that you use the other method. 30 | 31 | First, create a new bundle and override the `getParent` method in the bundle class. 32 | 33 | ``` php 34 | // src/Acme/FeatureCheckerBundle/FeatureCheckerBundle.php 35 | 36 | namespace Acme\FeatureCheckerBundle; 37 | 38 | use Symfony\Component\HttpKernel\Bundle\Bundle; 39 | 40 | class AcmeUserBundle extends Bundle 41 | { 42 | public function getParent() 43 | { 44 | return 'FeatureCheckerBundle'; 45 | } 46 | } 47 | ``` 48 | 49 | By returning the name of the bundle in the `getParent` method of your bundle class, you are telling the Symfony2 framework that your bundle is a child of the FeatureCheckerBundle. 50 | 51 | Now that you have declared your bundle as a child of the FeatureCheckerBundle, you can override the parent bundle's templates. To override the error template, simply create a new file in the `src/Acme/FeatureCheckerBundle/Resources/views` directory named `featureNotActivated.html.twig`. Notice how this file resides in the same exact path relative to the bundle directory as it does in the FeatureCheckerBundle. 52 | 53 | After overriding a template in your child bundle, you must clear the cache for the override to take effect, even in a development environment. 54 | 55 | Next step: [Overriding error controller](overriding_controller.md) -------------------------------------------------------------------------------- /Resources/doc/service.md: -------------------------------------------------------------------------------- 1 | # Using service in actions 2 | 3 | Features can also be checked directly in controller actions, through the FeatureChecker service. 4 | 5 | ``` php 6 | // src/AppBundle/Controller/DefaultController.php 7 | 8 | namespace AppBundle\Controller; 9 | 10 | use LWI\FeatureCheckerBundle\Annotations\MustHaveFeature; 11 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 12 | 13 | class DefaultController extends Controller 14 | { 15 | public function indexAction() 16 | { 17 | // Get the FeatureChecker service 18 | $checker = $this->container->get('feature_checker.checker'); 19 | 20 | // If the feature is not enabled, this action will stop and an error page will be shown. 21 | $checker->check('feature1'); 22 | 23 | return $this->render('default/index.html.twig'); 24 | } 25 | } 26 | ``` 27 | 28 | Next step: [Using Twig global variables](twig_variables.md) -------------------------------------------------------------------------------- /Resources/doc/twig_variables.md: -------------------------------------------------------------------------------- 1 | # Using Twig global variables 2 | 3 | Global Twig variables are declared based on the features configuration. Those are accessible via the following syntax : 4 | 5 | ``` yaml 6 | # app/config/config.yml 7 | 8 | feature_checker: 9 | features: 10 | feature1: true 11 | feature2: false 12 | feature3: 13 | feature31: true 14 | ``` 15 | 16 | ``` twig 17 | {% if feature_checker.feature1 %} 18 | Feature 1 is enabled 19 | {% endif %} 20 | 21 | {% if not feature_checker.feature2 %} 22 | Feature 2 is disabled 23 | {% endif %} 24 | 25 | {% if feature_checker.feature3.feature31 %} 26 | Feature 31 is enabled 27 | {% endif %} 28 | 29 | {% if feature_checker.feature3 %} 30 | Feature 3 and all its sub-features are enabled 31 | {% endif %} 32 | ``` 33 | 34 | Next step: [Overriding error template](overriding_template.md) -------------------------------------------------------------------------------- /Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Laurent Wiesel 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Resources/views/exception/featureNotActivated.html.twig: -------------------------------------------------------------------------------- 1 |

Error with feature {{ featureName }}

2 | 3 |

{{ message }}

4 | -------------------------------------------------------------------------------- /Service/FeatureChecker.php: -------------------------------------------------------------------------------- 1 | disableUndefined = $disableUndefined; 28 | $this->checker = new Checker($featuresConfiguration); 29 | } 30 | 31 | 32 | /** 33 | * Check given feature -or set of features- 34 | * 35 | * @param $featureName 36 | * @throws FeatureNotActivatedException 37 | */ 38 | public function check($featureName) 39 | { 40 | if (!$this->checker->isFeatureEnabled($featureName)) { 41 | throw new FeatureNotActivatedException($featureName); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Twig/GlobalsExtension.php: -------------------------------------------------------------------------------- 1 | features = $features; 23 | } 24 | 25 | /** 26 | * @return array 27 | */ 28 | public function getGlobals() 29 | { 30 | return array( 31 | 'feature_checker' => $this->features, 32 | ); 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getNamespace() 39 | { 40 | return 'feature_checker'; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getName() 47 | { 48 | return $this->getNamespace().'.globals_extension'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lwiesel/feature-checker-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Enable and disable functional features in Symfony2 applications.", 5 | "keywords": ["feature-checker-bundle", "FeatureCheckerBundle"], 6 | "homepage": "https://github.com/lwiesel/FeatureCheckerBundle", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Laurent Wiesel", 11 | "email": "wiesel.laurent@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3.2", 16 | "lwiesel/feature-checker": "~1.1", 17 | "symfony/framework-bundle": "~2.3", 18 | "symfony/twig-bundle": "~2.3", 19 | "doctrine/common": "~2.4" 20 | }, 21 | "require-dev": { 22 | "phpspec/phpspec": "~2.1", 23 | "phpunit/phpunit" : "4.*", 24 | "henrikbjorn/phpspec-code-coverage": "~1.0", 25 | "scrutinizer/ocular": "~1.1" 26 | }, 27 | "autoload": { 28 | "psr-4": { "LWI\\FeatureCheckerBundle\\": "" } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "LWI\\FeatureCheckerBundle\\Test\\": "Tests" 33 | } 34 | }, 35 | "config": { 36 | "bin-dir": "bin" 37 | } 38 | } 39 | --------------------------------------------------------------------------------