├── .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 | [](https://packagist.org/packages/francescomalatesta/laravel-feature)
4 | [](https://travis-ci.org/francescomalatesta/laravel-feature)
5 | [](https://scrutinizer-ci.com/g/francescomalatesta/laravel-feature/?branch=master)
6 | [](https://scrutinizer-ci.com/g/francescomalatesta/laravel-feature/?branch=master)
7 | [](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 |