├── .styleci.yml ├── src ├── Domain │ ├── Exception │ │ └── FeatureException.php │ ├── Repository │ │ └── FeatureRepositoryInterface.php │ ├── Model │ │ └── Feature.php │ └── FeatureManager.php ├── Model │ └── Feature.php ├── Featurable │ ├── FeaturableInterface.php │ └── Featurable.php ├── Migration │ ├── 2016_12_17_163450_create_featurables_table.php │ └── 2016_12_17_105737_create_features_table.php ├── Facade │ └── Feature.php ├── Console │ └── Command │ │ └── ScanViewsForFeaturesCommand.php ├── Config │ └── features.php ├── Provider │ └── FeatureServiceProvider.php ├── Service │ └── FeaturesViewScanner.php └── Repository │ └── EloquentFeatureRepository.php ├── .editorconfig ├── phpunit.xml ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md ├── composer.json ├── CONDUCT.md ├── .github └── workflows │ └── .github-actions.yml └── README.md /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | -------------------------------------------------------------------------------- /src/Domain/Exception/FeatureException.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests/Unit 15 | 16 | 17 | tests/Integration 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Featurable/Featurable.php: -------------------------------------------------------------------------------- 1 | first(); 13 | 14 | if ((bool) $model->is_enabled === true) { 15 | return true; 16 | } 17 | 18 | $feature = $this->features()->where('name', '=', $featureName)->first(); 19 | return ($feature) ? true : false; 20 | } 21 | 22 | public function features() 23 | { 24 | return $this->morphToMany(Feature::class, 'featurable'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `laravel-feature` will be documented in this file. 4 | 5 | Updates follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | - Add method signature to PHPDoc of the Feature facade 11 | 12 | ## 0.1.0 - 2016-12-18 13 | 14 | ### Added 15 | - All the domain classes and interfaces for the features management; 16 | - Eloquent concrete implementation of the FeatureRepositoryInterface; 17 | - A trait to allow every model to be "featurable"; 18 | - A command line tool to scan views for new features and save them; 19 | 20 | ### Deprecated 21 | - Nothing 22 | 23 | ### Fixed 24 | - Nothing 25 | 26 | ### Removed 27 | - Nothing 28 | 29 | ### Security 30 | - Nothing 31 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Detailed description 4 | 5 | Provide a detailed description of the change or addition you are proposing. 6 | 7 | Make it clear if the issue is a bug, an enhancement or just a question. 8 | 9 | ## Context 10 | 11 | Why is this change important to you? How would you use it? 12 | 13 | How can it benefit other users? 14 | 15 | ## Possible implementation 16 | 17 | Not obligatory, but suggest an idea for implementing addition or change. 18 | 19 | ## Your environment 20 | 21 | Include as many relevant details about the environment you experienced the bug in and how to reproduce it. 22 | 23 | * Version used (e.g. PHP 5.6, HHVM 3): 24 | * Operating system and version (e.g. Ubuntu 16.04, Windows 7): 25 | * Link to your project: 26 | * ... 27 | * ... 28 | -------------------------------------------------------------------------------- /src/Migration/2016_12_17_163450_create_featurables_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->integer('feature_id'); 19 | 20 | $table->integer('featurable_id'); 21 | $table->string('featurable_type'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::drop('featurables'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Migration/2016_12_17_105737_create_features_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | 19 | $table->string('name'); 20 | $table->boolean('is_enabled'); 21 | $table->timestamps(); 22 | 23 | $table->index('name'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::drop('features'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Domain/Model/Feature.php: -------------------------------------------------------------------------------- 1 | name = $name; 19 | $this->isEnabled = $isEnabled; 20 | } 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function getName() 26 | { 27 | return $this->name; 28 | } 29 | 30 | /** 31 | * @return bool 32 | */ 33 | public function isEnabled() 34 | { 35 | return $this->isEnabled; 36 | } 37 | 38 | public function setNewName($newName) 39 | { 40 | $this->name = $newName; 41 | } 42 | 43 | public function enable() 44 | { 45 | $this->isEnabled = true; 46 | } 47 | 48 | public function disable() 49 | { 50 | $this->isEnabled = false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Facade/Feature.php: -------------------------------------------------------------------------------- 1 | 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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/francescomalatesta/laravel-feature). 6 | 7 | ## Pull Requests 8 | 9 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. 10 | 11 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 12 | 13 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 14 | 15 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 16 | 17 | - **Create feature branches** - Don't ask us to pull from your master branch. 18 | 19 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 20 | 21 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 22 | 23 | ## Running Tests 24 | 25 | ``` bash 26 | $ composer test 27 | ``` 28 | 29 | **Happy coding**! 30 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | Describe your changes in detail. 6 | 7 | ## Motivation and context 8 | 9 | Why is this change required? What problem does it solve? 10 | 11 | If it fixes an open issue, please link to the issue here (if you write `fixes #num` 12 | or `closes #num`, the issue will be automatically closed when the pull is accepted.) 13 | 14 | ## How has this been tested? 15 | 16 | Please describe in detail how you tested your changes. 17 | 18 | Include details of your testing environment, and the tests you ran to 19 | see how your change affects other areas of the code, etc. 20 | 21 | ## Screenshots (if appropriate) 22 | 23 | ## Types of changes 24 | 25 | What types of changes does your code introduce? Put an `x` in all the boxes that apply: 26 | - [ ] Bug fix (non-breaking change which fixes an issue) 27 | - [ ] New feature (non-breaking change which adds functionality) 28 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 29 | 30 | ## Checklist: 31 | 32 | Go over all the following points, and put an `x` in all the boxes that apply. 33 | 34 | Please, please, please, don't send your pull request until all of the boxes are ticked. Once your pull request is created, it will trigger a build on our [continuous integration](http://www.phptherightway.com/#continuous-integration) server to make sure your [tests and code style pass](https://help.github.com/articles/about-required-status-checks/). 35 | 36 | - [ ] I have read the **[CONTRIBUTING](CONTRIBUTING.md)** document. 37 | - [ ] My pull request addresses exactly one patch/feature. 38 | - [ ] I have created a branch for this patch/feature. 39 | - [ ] Each individual commit in the pull request is meaningful. 40 | - [ ] I have added tests to cover my changes. 41 | - [ ] If my change requires a change to the documentation, I have updated it accordingly. 42 | 43 | If you're unsure about any of these, don't hesitate to ask. We're here to help! 44 | -------------------------------------------------------------------------------- /src/Console/Command/ScanViewsForFeaturesCommand.php: -------------------------------------------------------------------------------- 1 | service = app()->make(FeaturesViewScanner::class); 38 | } 39 | 40 | /** 41 | * Execute the console command. 42 | * 43 | * @return mixed 44 | */ 45 | public function handle() 46 | { 47 | $features = $this->service->scan(); 48 | $areEnabledByDefault = config('features.scanned_default_enabled'); 49 | 50 | $this->getOutput()->writeln(''); 51 | 52 | if (count($features) === 0) { 53 | $this->error('No features were found in the project views!'); 54 | $this->getOutput()->writeln(''); 55 | return; 56 | } 57 | 58 | $this->info(count($features) . ' features found in views:'); 59 | $this->getOutput()->writeln(''); 60 | 61 | foreach ($features as $feature) { 62 | $this->getOutput()->writeln('- ' . $feature); 63 | } 64 | 65 | $this->getOutput()->writeln(''); 66 | $this->info('All the new features were added to the database with the ' 67 | . ($areEnabledByDefault ? 'ENABLED' : 'disabled') . 68 | ' status by default. Nothing changed for the already present ones.'); 69 | 70 | $this->getOutput()->writeln(''); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "francescomalatesta/laravel-feature", 3 | "type": "library", 4 | "description": "A simple package to manage feature flagging in a Laravel project.", 5 | "keywords": ["laravel", "feature", "flag"], 6 | "homepage": "https://github.com/francescomalatesta/laravel-feature", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Francesco Malatesta", 11 | "email": "hellofrancesco@gmail.com", 12 | "homepage": "https://github.com/francescomalatesta", 13 | "role": "Developer" 14 | } 15 | ], 16 | "require": { 17 | "php" : "^7.4|^8.1", 18 | "illuminate/database": "^8.0|^9.0", 19 | "illuminate/support": "^8.0|^9.0" 20 | }, 21 | "require-dev": { 22 | "mockery/mockery": "^1.0", 23 | "orchestra/database": "^6.0|^7.0", 24 | "orchestra/testbench": "^6.0|^7.0", 25 | "phpunit/phpunit": "^8.0|^9.0", 26 | "squizlabs/php_codesniffer": "^3.3" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "LaravelFeature\\": "src" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "LaravelFeature\\Tests\\": "tests" 36 | } 37 | }, 38 | "scripts": { 39 | "test": "phpunit", 40 | "check-style": "phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src/Config src/Console src/Domain src/Facade src/Facade src/Featurable src/Model src/Provider src/Service", 41 | "fix-style": "phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src/Config src/Console src/Domain src/Facade src/Facade src/Featurable src/Model src/Provider src/Service" 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "minimum-stability": "dev", 47 | "extra": { 48 | "laravel": { 49 | "providers": [ 50 | "LaravelFeature\\Provider\\FeatureServiceProvider" 51 | ], 52 | "aliases": { 53 | "Feature": "LaravelFeature\\Facade\\Feature" 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Config/features.php: -------------------------------------------------------------------------------- 1 | [ 24 | base_path('resources/views') 25 | ], 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Scanned Features Default Status 30 | |-------------------------------------------------------------------------- 31 | | 32 | | When you use the feature:scan command, new features could be added to the 33 | | system. Be default, this new features are disabled. You can change this 34 | | by setting this value to true instead of false. 35 | | 36 | | By doing so, new added features will be automatically enabled globally. 37 | | 38 | */ 39 | 40 | 'scanned_default_enabled' => true, 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Features Repository 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Here you can configure the concrete class you will use to work with 48 | | features. By default, this class is the EloquentFeatureRepository shipped 49 | | with this package. As the name says, it works with Eloquent. 50 | | 51 | | However, you can use a custom feature repository if you want, just by 52 | | creating a new class that implements the FeatureRepositoryInterface. 53 | | 54 | */ 55 | 56 | 'repository' => LaravelFeature\Repository\EloquentFeatureRepository::class 57 | 58 | ]; 59 | -------------------------------------------------------------------------------- /src/Provider/FeatureServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/../Migration'); 20 | 21 | $this->publishes([ 22 | __DIR__.'/../Config/features.php' => config_path('features.php'), 23 | ]); 24 | 25 | $this->registerBladeDirectives(); 26 | } 27 | 28 | /** 29 | * Register any application services. 30 | * 31 | * @return void 32 | */ 33 | public function register() 34 | { 35 | $this->mergeConfigFrom(__DIR__.'/../Config/features.php', 'features'); 36 | 37 | $config = $this->app->make('config'); 38 | 39 | $this->app->bind(FeatureRepositoryInterface::class, function () use ($config) { 40 | return app()->make($config->get('features.repository')); 41 | }); 42 | 43 | $this->registerConsoleCommand(); 44 | } 45 | 46 | private function registerBladeDirectives() 47 | { 48 | $this->registerBladeFeatureDirective(); 49 | $this->registerBladeFeatureForDirective(); 50 | } 51 | 52 | private function registerBladeFeatureDirective() 53 | { 54 | Blade::directive('feature', function ($featureName) { 55 | return "isEnabled($featureName)): ?>"; 56 | }); 57 | 58 | Blade::directive('endfeature', function () { 59 | return ''; 60 | }); 61 | } 62 | 63 | private function registerBladeFeatureForDirective() 64 | { 65 | Blade::directive('featurefor', function ($args) { 66 | return "isEnabledFor($args)): ?>"; 67 | }); 68 | 69 | Blade::directive('endfeaturefor', function () { 70 | return ''; 71 | }); 72 | } 73 | 74 | private function registerConsoleCommand() 75 | { 76 | if ($this->app->runningInConsole()) { 77 | $this->commands([ 78 | ScanViewsForFeaturesCommand::class 79 | ]); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Service/FeaturesViewScanner.php: -------------------------------------------------------------------------------- 1 | featureManager = $featureManager; 26 | $this->config = $config; 27 | } 28 | 29 | public function scan() 30 | { 31 | $pathsToBeScanned = $this->config->get('features.scanned_paths', [ 'resources/views' ]); 32 | 33 | $foundDirectives = []; 34 | 35 | foreach ($pathsToBeScanned as $path) { 36 | $views = $this->getAllBladeViewsInPath($path); 37 | 38 | foreach ($views as $view) { 39 | $foundDirectives = array_merge($foundDirectives, $this->getFeaturesForView($view)); 40 | } 41 | } 42 | 43 | $foundDirectives = array_unique($foundDirectives); 44 | 45 | foreach ($foundDirectives as $directive) { 46 | $this->featureManager->add($directive, $this->config->get('features.scanned_default_enabled')); 47 | } 48 | 49 | return $foundDirectives; 50 | } 51 | 52 | private function getAllBladeViewsInPath($path) 53 | { 54 | $files = scandir($path); 55 | $files = array_diff($files, ['..', '.']); 56 | 57 | $bladeViews = []; 58 | 59 | foreach ($files as $file) { 60 | $itemPath = $path . DIRECTORY_SEPARATOR . $file; 61 | 62 | if (is_dir($itemPath)) { 63 | $bladeViews = array_merge($bladeViews, $this->getAllBladeViewsInPath($itemPath)); 64 | } 65 | 66 | if (is_file($itemPath) && Str::endsWith($file, '.blade.php')) { 67 | $bladeViews[] = $itemPath; 68 | } 69 | } 70 | 71 | return $bladeViews; 72 | } 73 | 74 | private function getFeaturesForView($view) 75 | { 76 | $fileContents = file_get_contents($view); 77 | 78 | preg_match_all('/@feature\(["\'](.+)["\']\)|@featurefor\(["\'](.+)["\']\,.*\)/', $fileContents, $results); 79 | 80 | return collect($results[1]) 81 | ->merge($results[2]) 82 | ->filter() 83 | ->toArray(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Domain/FeatureManager.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 21 | } 22 | 23 | public function add($featureName, $isEnabled) 24 | { 25 | $feature = Feature::fromNameAndStatus($featureName, $isEnabled); 26 | $this->repository->save($feature); 27 | } 28 | 29 | public function remove($featureName) 30 | { 31 | $feature = $this->repository->findByName($featureName); 32 | $this->repository->remove($feature); 33 | } 34 | 35 | public function rename($featureOldName, $featureNewName) 36 | { 37 | /** @var Feature $feature */ 38 | $feature = $this->repository->findByName($featureOldName); 39 | $feature->setNewName($featureNewName); 40 | 41 | $this->repository->save($feature); 42 | } 43 | 44 | public function enable($featureName) 45 | { 46 | /** @var Feature $feature */ 47 | $feature = $this->repository->findByName($featureName); 48 | 49 | $feature->enable(); 50 | 51 | $this->repository->save($feature); 52 | } 53 | 54 | public function disable($featureName) 55 | { 56 | /** @var Feature $feature */ 57 | $feature = $this->repository->findByName($featureName); 58 | 59 | $feature->disable(); 60 | 61 | $this->repository->save($feature); 62 | } 63 | 64 | public function isEnabled($featureName) 65 | { 66 | /** @var Feature $feature */ 67 | $feature = $this->repository->findByName($featureName); 68 | return $feature->isEnabled(); 69 | } 70 | 71 | public function enableFor($featureName, FeaturableInterface $featurable) 72 | { 73 | $this->repository->enableFor($featureName, $featurable); 74 | } 75 | 76 | public function disableFor($featureName, FeaturableInterface $featurable) 77 | { 78 | $this->repository->disableFor($featureName, $featurable); 79 | } 80 | 81 | public function isEnabledFor($featureName, FeaturableInterface $featurable) 82 | { 83 | return $this->repository->isEnabledFor($featureName, $featurable); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Repository/EloquentFeatureRepository.php: -------------------------------------------------------------------------------- 1 | getName())->first(); 17 | 18 | if (!$model) { 19 | $model = new Model(); 20 | } 21 | 22 | $model->name = $feature->getName(); 23 | $model->is_enabled = $feature->isEnabled(); 24 | 25 | try { 26 | $model->save(); 27 | } catch (\Exception $e) { 28 | throw new FeatureException('Unable to save the feature: ' . $e->getMessage()); 29 | } 30 | } 31 | 32 | public function remove(Feature $feature) 33 | { 34 | /** @var Model $model */ 35 | $model = Model::where('name', '=', $feature->getName())->first(); 36 | if (!$model) { 37 | throw new FeatureException('Unable to find the feature.'); 38 | } 39 | 40 | $model->delete(); 41 | } 42 | 43 | public function findByName($featureName) 44 | { 45 | /** @var Model $model */ 46 | $model = Model::where('name', '=', $featureName)->first(); 47 | if (!$model) { 48 | throw new FeatureException('Unable to find the feature.'); 49 | } 50 | 51 | return Feature::fromNameAndStatus( 52 | $model->name, 53 | $model->is_enabled 54 | ); 55 | } 56 | 57 | public function enableFor($featureName, FeaturableInterface $featurable) 58 | { 59 | /** @var Model $model */ 60 | $model = Model::where('name', '=', $featureName)->first(); 61 | if (!$model) { 62 | throw new FeatureException('Unable to find the feature.'); 63 | } 64 | 65 | if ((bool) $model->is_enabled === true || $featurable->hasFeature($featureName) === true) { 66 | return; 67 | } 68 | 69 | $featurable->features()->attach($model->id); 70 | } 71 | 72 | public function disableFor($featureName, FeaturableInterface $featurable) 73 | { 74 | /** @var Model $model */ 75 | $model = Model::where('name', '=', $featureName)->first(); 76 | if (!$model) { 77 | throw new FeatureException('Unable to find the feature.'); 78 | } 79 | 80 | if ((bool) $model->is_enabled === true || $featurable->hasFeature($featureName) === false) { 81 | return; 82 | } 83 | 84 | $featurable->features()->detach($model->id); 85 | } 86 | 87 | public function isEnabledFor($featureName, FeaturableInterface $featurable) 88 | { 89 | /** @var Model $model */ 90 | $model = Model::where('name', '=', $featureName)->first(); 91 | if (!$model) { 92 | throw new FeatureException('Unable to find the feature.'); 93 | } 94 | 95 | return ($model->is_enabled) ? true : $featurable->hasFeature($featureName); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at `francescomalatesta@live.it`. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /.github/workflows/.github-actions.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | tags: 9 | - '*' 10 | pull_request: 11 | branches: [ master ] 12 | 13 | workflow_dispatch: 14 | 15 | jobs: 16 | phpcs: 17 | strategy: 18 | matrix: 19 | version: ['7.4', '8.1'] 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout the repository 24 | uses: actions/checkout@v2 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Setup PHP with composer v2 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: ${{ matrix.version }} 32 | tools: composer:v2 33 | 34 | - name: Install composer packages 35 | run: | 36 | php -v 37 | composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts 38 | 39 | - name: Execute PHP_CodeSniffer 40 | run: | 41 | php -v 42 | composer check-style 43 | 44 | phpunit: 45 | strategy: 46 | matrix: 47 | version: ['7.4', '8.1'] 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | - name: Checkout the repository 52 | uses: actions/checkout@v2 53 | with: 54 | fetch-depth: 0 55 | 56 | - name: Setup PHP 57 | uses: shivammathur/setup-php@v2 58 | with: 59 | php-version: ${{ matrix.version }} 60 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, mysql, mysqli, pdo_mysql, bcmath, intl, exif, iconv 61 | coverage: xdebug 62 | 63 | - name: Install composer packages 64 | run: | 65 | php -v 66 | composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts 67 | 68 | - name: Execute tests 69 | run: | 70 | php -v 71 | ./vendor/phpunit/phpunit/phpunit --version 72 | ./vendor/phpunit/phpunit/phpunit --coverage-clover=coverage.xml 73 | # export CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} 74 | # bash <(curl -s https://codecov.io/bash) || echo 'Codecov failed to upload' 75 | 76 | # - name: Upload code coverage 77 | # run: | 78 | # export CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} 79 | # bash <(curl -s https://codecov.io/bash) || echo 'Codecov failed to upload' 80 | 81 | package-security-checker: 82 | runs-on: ubuntu-latest 83 | 84 | steps: 85 | - name: Checkout the repository 86 | uses: actions/checkout@v2 87 | 88 | - name: Setup PHP 89 | uses: shivammathur/setup-php@v2 90 | 91 | - name: Install composer packages 92 | run: | 93 | php -v 94 | composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts 95 | 96 | - name: Install security-checker 97 | run: | 98 | test -d local-php-security-checker || curl -L https://github.com/fabpot/local-php-security-checker/releases/download/v1.2.0/local-php-security-checker_1.2.0_linux_amd64 --output local-php-security-checker 99 | chmod +x local-php-security-checker 100 | ./local-php-security-checker 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel-Feature 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/francescomalatesta/laravel-feature/v/stable)](https://packagist.org/packages/francescomalatesta/laravel-feature) 4 | [![Build Status](https://travis-ci.org/francescomalatesta/laravel-feature.svg?branch=master)](https://travis-ci.org/francescomalatesta/laravel-feature) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/francescomalatesta/laravel-feature/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/francescomalatesta/laravel-feature/?branch=master) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/francescomalatesta/laravel-feature/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/francescomalatesta/laravel-feature/?branch=master) 7 | [![StyleCI](https://styleci.io/repos/76716509/shield?branch=master)](https://styleci.io/repos/76716509) 8 | 9 | Laravel-Feature is a package fully dedicated to feature toggling in your application, in the easiest way. For Laravel, of course. 10 | 11 | It was inspired by the [AdEspresso Feature Flag Bundle](https://github.com/adespresso/FeatureBundle). 12 | 13 | ## Feature-What? 14 | 15 | Feature toggling is basically a way to **have full control on the activation of a feature** in your applications. 16 | 17 | Let's make a couple of examples to give you an idea: 18 | 19 | * you just finished to work on the latest feature and you want to push it, but the marketing team wants you to deploy it in a second moment; 20 | * the new killer-feature is ready, but you want to enable it only for a specific set of users; 21 | 22 | With Laravel-Feature, you can: 23 | 24 | * easily **define new features** in your application; 25 | * **enable/disable features** globally; 26 | * **enable/disable features for specific users**, or **for whatever you want**; 27 | 28 | There are many things to know about feature toggling: take a look to [this great article](http://martinfowler.com/articles/feature-toggles.html) for more info. It's a really nice and useful lecture. 29 | 30 | ## Install 31 | 32 | You can install Laravel-Feature with Composer. 33 | 34 | ``` bash 35 | $ composer require francescomalatesta/laravel-feature 36 | ``` 37 | 38 | After that, you need to **add the `FeatureServiceProvider` to the `app.php` config file**. 39 | 40 | ```php 41 | ... 42 | LaravelFeature\Provider\FeatureServiceProvider::class, 43 | ... 44 | ``` 45 | 46 | Now you have to **run migrations**, to add the tables Laravel-Feature needs. 47 | 48 | ```bash 49 | $ php artisan migrate 50 | ``` 51 | 52 | ... and you're good to go! 53 | 54 | ### Facade 55 | 56 | If you want, you can also **add the `Feature` facade** to the `aliases` array in the `app.php` config file. 57 | 58 | ```php 59 | ... 60 | 'Feature' => \LaravelFeature\Facade\Feature::class, 61 | ... 62 | ``` 63 | 64 | If you don't like Facades, **inject the `FeatureManager`** class wherever you want! 65 | 66 | ### Config File 67 | 68 | By default, you can immediately use Laravel-Feature. However, if you want to tweak some settings, feel free to **publish the config file** with 69 | 70 | ```bash 71 | $ php artisan vendor:publish --provider="LaravelFeature\Provider\FeatureServiceProvider" 72 | ``` 73 | 74 | ## Basic Usage 75 | 76 | There are two ways you can use features: working with them **globally** or **specifically for a specific entity**. 77 | 78 | ### Globally Enabled/Disabled Features 79 | 80 | #### Declare a New Feature 81 | 82 | Let's say you have a new feature that you want to keep hidden until a certain moment. We will call it "page_code_cleaner". Let's **add it to our application**: 83 | 84 | ```php 85 | Feature::add('page_code_cleaner', false); 86 | ``` 87 | 88 | Easy, huh? As you can imagine, **the first argument is the feature name**. **The second is a boolean we specify to define the current status** of the feature. 89 | 90 | * `true` stands for **the feature is enabled for everyone**; 91 | * `false` stands for **the feature is hidden, no one can use it/see it**; 92 | 93 | And that's all. 94 | 95 | #### Check if a Feature is Enabled 96 | 97 | Now, let's imagine a better context for our example. We're building a CMS, and our "page_code_cleaner" is used to... clean our HTML code. Let's assume we have a controller like this one. 98 | 99 | ```php 100 | class CMSController extends Controller { 101 | public function getPage($pageSlug) { 102 | 103 | // here we are getting our page code from some service 104 | $content = PageService::getContentBySlug($pageSlug); 105 | 106 | // here we are showing our page code 107 | return view('layout.pages', compact('content')); 108 | } 109 | } 110 | ``` 111 | 112 | Now, we want to deploy the new service, but **we don't want to make it available for users**, because the marketing team asked us to release it the next week. LaravelFeature helps us with this: 113 | 114 | ```php 115 | class CMSController extends Controller { 116 | public function getPage($pageSlug) { 117 | 118 | // here we are getting our page code from some service 119 | $content = PageService::getContentBySlug($pageSlug); 120 | 121 | // feature flagging here! 122 | if(Feature::isEnabled('page_code_cleaner')) { 123 | $content = PageCleanerService::clean($content); 124 | } 125 | 126 | // here we are showing our page code 127 | return view('layout.pages', compact('content')); 128 | } 129 | } 130 | ``` 131 | 132 | Ta-dah! Now, **the specific service code will be executed only if the "page_code_cleaner" feature is enabled**. 133 | 134 | #### Change a Feature Activation Status 135 | 136 | Obviously, using the `Feature` class we can easily **toggle the feature activation status**. 137 | 138 | ```php 139 | // release the feature! 140 | Feature::enable('page_code_cleaner'); 141 | 142 | // hide the feature! 143 | Feature::disable('page_code_cleaner'); 144 | ``` 145 | 146 | #### Remove a Feature 147 | 148 | Even if it's not so used, you can also **delete a feature** easily with 149 | 150 | ```php 151 | Feature::remove('page_code_cleaner'); 152 | ``` 153 | 154 | Warning: *be sure about what you do. If you remove a feature from the system, you will stumble upon exceptions if checks for the deleted features are still present in the codebase.* 155 | 156 | #### Work with Views 157 | 158 | I really love blade directives, they help me writing more elegant code. I prepared **a custom blade directive, `@feature`**: 159 | 160 | ```php 161 |
This is an example template div. Always visible.
162 | 163 | @feature('my_awesome_feature') 164 |

This paragraph will be visible only if "my_awesome_feature" is enabled!

165 | @endfeature 166 | 167 |
This is another example template div. Always visible too.
168 | ``` 169 | 170 | A really nice shortcut! 171 | 172 | ### Enable/Disable Features for Specific Users/Entities 173 | 174 | Even if the previous things we saw are useful, LaravelFeature **is not just about pushing the on/off button on a feature**. Sometimes, business necessities require more flexibility. Think about a [**Canary Release**](http://martinfowler.com/bliki/CanaryRelease.html): we want to rollout a feature only to specific users. Or, maybe, just for one tester user. 175 | 176 | #### Enable Features Management for Specific Users 177 | 178 | LaravelFeature makes this possible, and also easier just as **adding a trait to our `User` class**. 179 | 180 | In fact, all you need to do is to: 181 | 182 | * **add the `LaravelFeature\Featurable\Featurable` trait** to the `User` class; 183 | * let the same class **implement the `FeaturableInterface` interface**; 184 | 185 | ```php 186 | ... 187 | 188 | class User extends Authenticatable implements FeaturableInterface 189 | { 190 | use Notifiable, Featurable; 191 | 192 | ... 193 | ``` 194 | 195 | Nothing more! LaravelFeature now already knows what to do. 196 | 197 | #### Status Priority 198 | 199 | *Please keep in mind that all you're going to read from now is not valid if a feature is already enabled globally. To activate a feature for specific users, you first need to disable it.* 200 | 201 | Laravel-Feature **first checks if the feature is enabled globally, then it goes down at entity-level**. 202 | 203 | #### Enable/Disable a Feature for a Specific User 204 | 205 | ```php 206 | $user = Auth::user(); 207 | 208 | // now, the feature "my.feature" is enabled ONLY for $user! 209 | Feature::enableFor('my.feature', $user); 210 | 211 | // now, the feature "my.feature" is disabled for $user! 212 | Feature::disableFor('my.feature', $user); 213 | 214 | ``` 215 | 216 | #### Check if a Feature is Enabled for a Specific User 217 | 218 | ```php 219 | $user = Auth::user(); 220 | 221 | if(Feature::isEnabledFor('my.feature', $user)) { 222 | 223 | // do amazing things! 224 | 225 | } 226 | ``` 227 | 228 | #### Other Notes 229 | 230 | LaravelFeature also provides a Blade directive to check if a feature is enabled for a specific user. You can use the `@featurefor` blade tags: 231 | ```php 232 | @featurefor('my.feature', $user) 233 | 234 | // do $user related things here! 235 | 236 | @endfeaturefor 237 | ``` 238 | 239 | ## Advanced Things 240 | 241 | Ok, now that we got the basics, let's raise the bar! 242 | 243 | ### Enable Features Management for Other Entities 244 | 245 | As I told before, you can easily add features management for Users just by using the `Featurable` trait and implementing the `FeaturableInterface` in the User model. However, when structuring the relationships, I decided to implement a **many-to-many polymorphic relationship**. This means that you can **add feature management to any model**! 246 | 247 | Let's make an example: imagine that **you have a `Role` model** you use to implement a basic roles systems for your users. This because you have admins and normal users. 248 | 249 | So, **you rolled out the amazing killer feature but you want to enable it only for admins**. How to do this? Easy. Recap: 250 | 251 | * add the `Featurable` trait to the `Role` model; 252 | * be sure the `Role` model implements the `FeaturableInterface`; 253 | 254 | Let's think the role-user relationship as one-to-many one. 255 | 256 | You will probably have a `role()` method on your `User` class, right? Good. You already know the rest: 257 | 258 | ```php 259 | // $role is the admin role! 260 | $role = Auth::user()->role; 261 | 262 | ... 263 | 264 | Feature::enableFor('my.feature', $role); 265 | 266 | ... 267 | 268 | if(Feature::isEnabledFor('my.feature', $role)) { 269 | 270 | // this code will be executed only if the user is an admin! 271 | 272 | } 273 | ``` 274 | 275 | ### Scan Directories for Features 276 | 277 | One of the nice bonuses of the package that inspired me when making this package, is the ability to **"scan" views, find `@feature` declarations and then add these scanned features if not already present** on the system. 278 | 279 | I created a simple **artisan command** to do this. 280 | 281 | ```bash 282 | $ php artisan feature:scan 283 | ``` 284 | 285 | The command will use a dedicated service to **fetch the `resources/views` folder and scan every single Blade view to find `@feature` directives**. It will then output the search results. 286 | 287 | Try it, you will like it! 288 | 289 | **Note:** if you have published the config file, you will be able to **change the list of scanned directories**. 290 | 291 | ### Using a Custom Features Repository 292 | 293 | Imagine that you want to **change the place or the way you store features**. For some crazy reason, you want to store it on a static file, or on Dropbox. 294 | 295 | Now, Eloquent doesn't have a Dropbox driver, so you can't use this package. **Bye.** 296 | 297 | Just joking! When making this package, I wanted to be sure to create a fully reusable logic if the developer doesn't want to use Eloquent anymore. 298 | 299 | To do this, I created a nice interface for the Job, and created some bindings in the Laravel Service Container. Nothing really complex, anyway. 300 | 301 | The interface I am talking about is `FeatureRepositoryInterface`. 302 | 303 | ```php 304 | LaravelFeature\Repository\EloquentFeatureRepository::class 342 | ``` 343 | 344 | will become... 345 | 346 | ```php 347 | 'repository' => My\Wonderful\DropboxFeatureRepository::class 348 | ``` 349 | 350 | Done! By the way, don't forget to let the entities you need to **implement the `FeaturableInterface`**. 351 | 352 | ```php 353 |