diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..deeaab3 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,34 @@ +name: Test + +on: [push] + +jobs: + test: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + extensions: zip, sqlite3 + coverage: none + + - name: Restore caches + uses: actions/cache@v2 + with: + path: ~/.composer/cache/files + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install composer dependencies + run: composer install --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + - name: Code sniff + run: vendor/bin/php-cs-fixer fix --dry-run + + - name: Execute tests + run: composer test diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..4857215 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,11 @@ +in([ + __DIR__.'/src', + __DIR__.'/config', + __DIR__.'/database', + __DIR__.'/tests', + ]); + +return CodingLabs\styles($finder); \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 676753e..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -All notable changes to `laravel-feature-flags` will be documented in this file. - -## 1.0.0 - 202X-XX-XX - -- initial release diff --git a/LICENSE.md b/LICENSE.md index 9a8cb14..bbd779b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) Codinglabs +Copyright (c) Coding Labs Pty Ltd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5acbb05..d2ff7ce 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,212 @@ - -[](https://supportukrainenow.org) - # Dynamic feature flags for laravel. [![Latest Version on Packagist](https://img.shields.io/packagist/v/codinglabsau/laravel-feature-flags.svg?style=flat-square)](https://packagist.org/packages/codinglabsau/laravel-feature-flags) -[![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/codinglabsau/laravel-feature-flags/run-tests?label=tests)](https://github.com/codinglabsau/laravel-feature-flags/actions?query=workflow%3Arun-tests+branch%3Amain) -[![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/codinglabsau/laravel-feature-flags/Check%20&%20fix%20styling?label=code%20style)](https://github.com/codinglabsau/laravel-feature-flags/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) +[![Test](https://github.com/codinglabsau/laravel-feature-flags/actions/workflows/run-tests.yml/badge.svg)](https://github.com/codinglabsau/laravel-feature-flags/actions/workflows/run-tests.yml) [![Total Downloads](https://img.shields.io/packagist/dt/codinglabsau/laravel-feature-flags.svg?style=flat-square)](https://packagist.org/packages/codinglabsau/laravel-feature-flags) -This is where your description should go. Limit it to a paragraph or two. Consider adding a small example. - -## Support us - -[](https://spatie.be/github-ad-click/laravel-feature-flags) - -We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). - -We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). - +This package offers the ability to implement feature flags in your application which can be easily toggled on or off. You can also set a feature to a dynamic state where you can define custom rules around whether that feature is enabled or not. +___ ## Installation -You can install the package via composer: +### Install With Composer: ```bash composer require codinglabsau/laravel-feature-flags ``` -You can publish and run the migrations with: +### Database Migrations ```bash -php artisan vendor:publish --tag="laravel-feature-flags-migrations" +php artisan vendor:publish --tag="feature-flags-migrations" php artisan migrate ``` -You can publish the config file with: +### Publish Configuration: ```bash -php artisan vendor:publish --tag="laravel-feature-flags-config" +php artisan vendor:publish --tag="feature-flags-config" ``` -This is the contents of the published config file: +### Cache Store +update your `.env`: ```php -return [ -]; +FEATURES_CACHE_STORE=file ``` +Note that under the hood this package uses the `rememberForever()` method for caching and that if you are using the `Memcached` driver, items that are stored "forever" may be removed when the cache reaches its size limit. -Optionally, you can publish the views using +### Use Your Own Model -```bash -php artisan vendor:publish --tag="laravel-feature-flags-views" +To use your own model, update the config and replace the existing reference with your own model: + +```php +// app/config/feature-flags.php + +'feature_model' => \App\Models\Feature::class, +``` + +Make sure to also cast the state column to a feature state enum using the `FeatureStateCast`: + +```php +// app/Models/Feature.php + +use Codinglabs\FeatureFlags\Casts\FeatureStateCast; + +protected $casts = [ + 'state' => FeatureStateCast::class +]; ``` ## Usage +Create a new feature in the database and give it a default state: ```php -$features = new Codinglabs\FeatureFlags(); -echo $features->echoPhrase('Hello, Codinglabs!'); +Feature::create([ + 'name' => 'search-v2', + 'state' => Codinglabs\FeatureFlags\Enums\FeatureState::on() +]); ``` -## Testing +Its recommended that you seed the features to your database before a new deployment or as soon as possible after a deployment. -```bash -composer test +A feature can be in one of three states: +```php +use Codinglabs\FeatureFlags\Enums\FeatureState; + +FeatureState::on() +FeatureState::off() +FeatureState::dynamic() +``` +### Check If A Feature Is Enabled + +#### Blade View +```php +@feature('search-v2') + // new search goes here +@else + // legacy search here +@endfeature +``` + +#### In Your Code +```php +use Codinglabs\FeatureFlags\Facades\FeatureFlag; + +if (FeatureFlag::isEnabled('search-v2')) { + // new feature code +} else { + // old code +} ``` -## Changelog +#### Sharing features with UI (Inertiajs example) +```php +// config/app.php + +'features' => [ + [ + 'name' => 'search-v2', + 'state' => \Codinglabs\FeatureFlags\Enums\FeatureState::dynamic() + ] +], +``` -Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. +```php +// app/Middleware/HandleInertiaRequest.php + +Inertia::share([ + 'features' => function () { + return collect(config('app.features')) + ->filter(fn ($feature) => FeatureFlag::isEnabled($feature['name'])) + ->pluck('name'); + } +]); +``` -## Contributing +```javascript +// app.js + +Vue.mixin({ + methods: { + hasFeature: function(feature) { + return this.$page.features.includes(feature) + } + } +}) +``` +```html + -Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. +
Some cool new feature
+``` + +### Updating A Features State + +To change a features state you can call the following methods: +```php +use Codinglabs\FeatureFlags\Facades\FeatureFlag; + +FeatureFlag::turnOn('search-v2'); +FeatureFlag::turnOff('search-v2'); +FeatureFlag::makeDynamic('search-v2'); +``` +Alternatively you can set the state directly by passing a feature state enum: +```php +FeatureFlag::updateFeatureState('search-v2', FeatureState::on()) +``` +It is recommended that you only update a features state using the above methods as it will take care of updating the cache and dispatching the feature updated event: + +```php +\Codinglabs\FeatureFlags\Events\FeatureUpdatedEvent::class +``` +An example use case of the feature updated event would be if you were caching the result of a dynamic handler and need to clear that cache when a feature is updated. + +___ +## Advanced Usage + +### Dynamic Features + +A dynamic handler can be defined in the `boot()` method of your `AppServiceProvider`: +```php +use Codinglabs\FeatureFlags\Facades\FeatureFlag; + +FeatureFlag::registerDynamicHandler('search-v2', function ($feature, $request) { + return $request->user() && $request->user()->hasRole('Tester') +}); +``` +Dynamic handlers will only be called when a feature is in the `dynamic` state. This will allow you to define custom rules around whether that feature is enabled like in the example above where the user can only access the feature if they have a tester role. + +Each handler is provided with the features name and current request as arguments and must return a bool value. + +### Default Handler For Dynamic Features + +You may also define a default handler which will be the catch-all handler for features that don't have an explicit handler defined for them: + +```php +FeatureFlag::registerDefaultDynamicHandler(function ($feature, $request) { + return $request->user() && $request->user()->hasRole('Tester'); +}); +``` + +An explicit handler defined using `registerDynamicHandler()` will take precedence over the default handler. If neither a default nor explicit handler has been defined then the feature will resolve to `off` by default. + +### Handle Missing Features + +Features must exist in the database otherwise a `MissingFeatureException` will be thrown. This behaviour can be turned off by explicitly handling cases where a feature doesn't exist: + +```php +FeatureFlag::handleMissingFeaturesWith(function ($feature) { + // log or report this somewhere... +}) +``` + +If a handler for missing features has been defined then an exception will **not** be thrown and the feature will resolve to `off`. + +## Testing + +```bash +composer test +``` ## Security Vulnerabilities @@ -79,7 +214,7 @@ Please review [our security policy](../../security/policy) on how to report secu ## Credits -- [Steve Thomas](https://github.com/codinglabsau) +- [Jonathan Louw](https://github.com/JonathanLouw) - [All Contributors](../../contributors) ## License diff --git a/composer.json b/composer.json index eefd5e5..5705bb4 100644 --- a/composer.json +++ b/composer.json @@ -12,15 +12,17 @@ "license": "MIT", "authors": [ { - "name": "Steve Thomas", - "email": "steve@codinglabs.com.au", + "name": "Jonathan Louw", + "email": "JonathanLouw@users.noreply.github.com", "role": "Developer" } ], "require": { - "php": "^7.4|^8.0", - "spatie/laravel-package-tools": "^1.9.2", - "illuminate/contracts": "^7.0|^8.0|^9.0" + "php": "^8.0", + "codinglabsau/php-styles": "dev-main", + "illuminate/contracts": "^7.0|^8.0|^9.0", + "spatie/enum": "^3.12", + "spatie/laravel-package-tools": "^1.9.2" }, "require-dev": { "nunomaduro/collision": "^6.0", @@ -55,10 +57,10 @@ "Codinglabs\\FeatureFlags\\FeatureFlagsServiceProvider" ], "aliases": { - "Features": "Codinglabs\\FeatureFlags\\Facades\\Features" + "FeatureFlag": "Codinglabs\\FeatureFlags\\Facades\\FeatureFlags" } } }, "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index c7540d8..e25a37c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6b44063227042c725f2a891ca48f2c81", + "content-hash": "9b3268844b656b649cbfb2ac4f22ff5d", "packages": [ { "name": "brick/math", @@ -66,6 +66,259 @@ ], "time": "2021-08-15T20:50:18+00:00" }, + { + "name": "codinglabsau/php-styles", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/codinglabsau/php-styles.git", + "reference": "9c540a3e3563451fff5c7d45b13708ce692925cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codinglabsau/php-styles/zipball/9c540a3e3563451fff5c7d45b13708ce692925cf", + "reference": "9c540a3e3563451fff5c7d45b13708ce692925cf", + "shasum": "" + }, + "require": { + "friendsofphp/php-cs-fixer": "^3.7" + }, + "default-branch": true, + "type": "library", + "autoload": { + "files": [ + "./src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Coding Labs styles for PHP-CS-Fixer", + "support": { + "issues": "https://github.com/codinglabsau/php-styles/issues", + "source": "https://github.com/codinglabsau/php-styles/tree/main" + }, + "time": "2022-03-15T00:08:17+00:00" + }, + { + "name": "composer/pcre", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/e300eb6c535192decd27a85bc72a9290f0d6b3bd", + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T20:21:48+00:00" + }, + { + "name": "composer/semver", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "5d8e574bb0e69188786b8ef77d43341222a41a71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/5d8e574bb0e69188786b8ef77d43341222a41a71", + "reference": "5d8e574bb0e69188786b8ef77d43341222a41a71", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.3.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-03-16T11:22:07+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.1", @@ -141,6 +394,78 @@ }, "time": "2021-08-13T13:06:58+00:00" }, + { + "name": "doctrine/annotations", + "version": "1.13.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "5b668aef16090008790395c02c893b1ba13f7e08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08", + "reference": "5b668aef16090008790395c02c893b1ba13f7e08", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^6.0 || ^8.1", + "phpstan/phpstan": "^0.12.20", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", + "symfony/cache": "^4.4 || ^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.13.2" + }, + "time": "2021-08-05T19:00:23+00:00" + }, { "name": "doctrine/inflector", "version": "2.0.4", @@ -437,6 +762,95 @@ ], "time": "2021-10-11T09:18:27+00:00" }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.8.0", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", + "reference": "cbad1115aac4b5c3c5540e7210d3c9fba2f81fa3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/cbad1115aac4b5c3c5540e7210d3c9fba2f81fa3", + "reference": "cbad1115aac4b5c3c5540e7210d3c9fba2f81fa3", + "shasum": "" + }, + "require": { + "composer/semver": "^3.2", + "composer/xdebug-handler": "^3.0.3", + "doctrine/annotations": "^1.13", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0", + "php-cs-fixer/diff": "^2.0", + "symfony/console": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/options-resolver": "^5.4 || ^6.0", + "symfony/polyfill-mbstring": "^1.23", + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php81": "^1.25", + "symfony/process": "^5.4 || ^6.0", + "symfony/stopwatch": "^5.4 || ^6.0" + }, + "require-dev": { + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^1.5", + "mikey179/vfsstream": "^1.6.10", + "php-coveralls/php-coveralls": "^2.5.2", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", + "phpspec/prophecy": "^1.15", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "phpunitgoodpractices/polyfill": "^1.5", + "phpunitgoodpractices/traits": "^1.9.1", + "symfony/phpunit-bridge": "^6.0", + "symfony/yaml": "^5.4 || ^6.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz RumiƄski", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.8.0" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2022-03-18T17:20:59+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.2.0", @@ -1482,6 +1896,58 @@ }, "time": "2022-01-24T11:29:14+00:00" }, + { + "name": "php-cs-fixer/diff", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/diff.git", + "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3", + "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", + "symfony/process": "^3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "sebastian/diff v3 backport support for PHP 5.6+", + "homepage": "https://github.com/PHP-CS-Fixer", + "keywords": [ + "diff" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/diff/issues", + "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2" + }, + "time": "2020-10-14T08:32:19+00:00" + }, { "name": "phpoption/phpoption", "version": "1.8.1", @@ -1553,6 +2019,55 @@ ], "time": "2021-12-04T23:24:31+00:00" }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -1838,25 +2353,24 @@ }, { "name": "ramsey/uuid", - "version": "4.2.3", + "version": "4.3.1", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df" + "reference": "8505afd4fea63b81a85d3b7b53ac3cb8dc347c28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df", - "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8505afd4fea63b81a85d3b7b53ac3cb8dc347c28", + "reference": "8505afd4fea63b81a85d3b7b53ac3cb8dc347c28", "shasum": "" }, "require": { "brick/math": "^0.8 || ^0.9", + "ext-ctype": "*", "ext-json": "*", - "php": "^7.2 || ^8.0", - "ramsey/collection": "^1.0", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php80": "^1.14" + "php": "^8.0", + "ramsey/collection": "^1.0" }, "replace": { "rhumsaa/uuid": "self.version" @@ -1893,9 +2407,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "4.x-dev" - }, "captainhook": { "force-install": true } @@ -1907,32 +2418,108 @@ "psr-4": { "Ramsey\\Uuid\\": "src/" } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.3.1" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2022-03-27T21:42:02+00:00" + }, + { + "name": "spatie/enum", + "version": "3.12.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/enum.git", + "reference": "c0180f1de7c5d06e4b84efbc751ea19167140173" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/enum/zipball/c0180f1de7c5d06e4b84efbc751ea19167140173", + "reference": "c0180f1de7c5d06e4b84efbc751ea19167140173", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "fakerphp/faker": "^1.9.1", + "larapack/dd": "^1.1", + "phpunit/phpunit": "^9.0", + "vimeo/psalm": "^4.3" + }, + "suggest": { + "fakerphp/faker": "To use the enum faker provider", + "phpunit/phpunit": "To use the enum assertions" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Enum\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brent Roose", + "email": "brent@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Tom Witkowski", + "email": "dev@gummibeer.de", + "homepage": "https://gummibeer.de", + "role": "Developer" + } ], - "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "description": "PHP Enums", + "homepage": "https://github.com/spatie/enum", "keywords": [ - "guid", - "identifier", - "uuid" + "enum", + "enumerable", + "spatie" ], "support": { - "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.2.3" + "docs": "https://docs.spatie.be/enum", + "issues": "https://github.com/spatie/enum/issues", + "source": "https://github.com/spatie/enum" }, "funding": [ { - "url": "https://github.com/ramsey", - "type": "github" + "url": "https://spatie.be/open-source/support-us", + "type": "custom" }, { - "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", - "type": "tidelift" + "url": "https://github.com/spatie", + "type": "github" } ], - "time": "2021-09-25T23:10:38+00:00" + "time": "2022-02-05T09:44:52+00:00" }, { "name": "spatie/laravel-package-tools", @@ -2453,6 +3040,69 @@ ], "time": "2021-07-15T12:33:35+00:00" }, + { + "name": "symfony/filesystem", + "version": "v6.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "52b888523545b0b4049ab9ce48766802484d7046" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/52b888523545b0b4049ab9ce48766802484d7046", + "reference": "52b888523545b0b4049ab9ce48766802484d7046", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-02T12:58:14+00:00" + }, { "name": "symfony/finder", "version": "v6.0.3", @@ -2850,6 +3500,73 @@ ], "time": "2022-01-02T09:55:41+00:00" }, + { + "name": "symfony/options-resolver", + "version": "v6.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "51f7006670febe4cbcbae177cbffe93ff833250d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/51f7006670febe4cbcbae177cbffe93ff833250d", + "reference": "51f7006670febe4cbcbae177cbffe93ff833250d", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:55:41+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.25.0", @@ -3736,6 +4453,68 @@ ], "time": "2021-11-04T17:53:12+00:00" }, + { + "name": "symfony/stopwatch", + "version": "v6.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "f2c1780607ec6502f2121d9729fd8150a655d337" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/f2c1780607ec6502f2121d9729fd8150a655d337", + "reference": "f2c1780607ec6502f2121d9729fd8150a655d337", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/service-contracts": "^1|^2|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v6.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-21T17:15:17+00:00" + }, { "name": "symfony/string", "version": "v6.0.3", @@ -7748,68 +8527,6 @@ ], "time": "2022-01-04T09:04:05+00:00" }, - { - "name": "symfony/stopwatch", - "version": "v6.0.5", - "source": { - "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "f2c1780607ec6502f2121d9729fd8150a655d337" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/f2c1780607ec6502f2121d9729fd8150a655d337", - "reference": "f2c1780607ec6502f2121d9729fd8150a655d337", - "shasum": "" - }, - "require": { - "php": ">=8.0.2", - "symfony/service-contracts": "^1|^2|^3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides a way to profile code", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.0.5" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-02-21T17:15:17+00:00" - }, { "name": "symfony/yaml", "version": "v6.0.3", @@ -8138,11 +8855,13 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": { + "codinglabsau/php-styles": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.4|^8.0" + "php": "^8.0" }, "platform-dev": [], "plugin-api-version": "2.1.0" diff --git a/config/feature-flags.php b/config/feature-flags.php index c803fc2..e0dab8f 100644 --- a/config/feature-flags.php +++ b/config/feature-flags.php @@ -7,7 +7,7 @@ | Cache |-------------------------------------------------------------------------- | - | Configure the cache driver that will be used to cache the state of a + | Configure the cache store that will be used to cache the state of a | feature. You can also configure a prefix for all keys in the cache. */ @@ -20,8 +20,7 @@ |-------------------------------------------------------------------------- | | If you need to customise any models used then you can swap them out by - | replacing the default models defined here. Make sure you extend the - | feature model if you do choose to create a custom model. + | replacing the default models defined here. */ 'feature_model' => \Codinglabs\FeatureFlags\Models\Feature::class, diff --git a/database/factories/FeatureFactory.php b/database/factories/FeatureFactory.php index 32aa8a3..df714c5 100644 --- a/database/factories/FeatureFactory.php +++ b/database/factories/FeatureFactory.php @@ -3,6 +3,7 @@ namespace Codinglabs\FeatureFlags\Database\Factories; use Codinglabs\FeatureFlags\Models\Feature; +use Codinglabs\FeatureFlags\Enums\FeatureState; use Illuminate\Database\Eloquent\Factories\Factory; class FeatureFactory extends Factory @@ -13,7 +14,11 @@ public function definition() { return [ 'name' => $this->faker->unique()->slug(2), - 'state' => $this->faker->randomElement(['on', 'off', 'restricted']) + 'state' => $this->faker->randomElement([ + FeatureState::on()->value, + FeatureState::off()->value, + FeatureState::dynamic()->value, + ]) ]; } } diff --git a/src/Casts/FeatureStateCast.php b/src/Casts/FeatureStateCast.php new file mode 100644 index 0000000..3a47947 --- /dev/null +++ b/src/Casts/FeatureStateCast.php @@ -0,0 +1,26 @@ + $value->value + ]; + } +} diff --git a/src/Enums/FeatureState.php b/src/Enums/FeatureState.php new file mode 100644 index 0000000..3b50c2e --- /dev/null +++ b/src/Enums/FeatureState.php @@ -0,0 +1,14 @@ +feature = config('features.feature_model')::where('name', $feature)->first(); - } -} \ No newline at end of file diff --git a/src/Events/FeatureRestrictedEvent.php b/src/Events/FeatureRestrictedEvent.php deleted file mode 100644 index 17118e0..0000000 --- a/src/Events/FeatureRestrictedEvent.php +++ /dev/null @@ -1,13 +0,0 @@ -feature = config('features.feature_model')::where('name', $feature)->first(); - } -} \ No newline at end of file diff --git a/src/Events/FeatureUpdatedEvent.php b/src/Events/FeatureUpdatedEvent.php index 79b573a..2790222 100644 --- a/src/Events/FeatureUpdatedEvent.php +++ b/src/Events/FeatureUpdatedEvent.php @@ -2,12 +2,14 @@ namespace Codinglabs\FeatureFlags\Events; +use Illuminate\Database\Eloquent\Model; + class FeatureUpdatedEvent extends Event { - public $feature; + public Model $feature; - public function __construct(string $feature) + public function __construct(Model $feature) { - $this->feature = config('feature-flags.feature_model')::where('name', $feature)->first(); + $this->feature = $feature; } -} \ No newline at end of file +} diff --git a/src/Exceptions/MissingFeatureException.php b/src/Exceptions/MissingFeatureException.php new file mode 100644 index 0000000..487f41f --- /dev/null +++ b/src/Exceptions/MissingFeatureException.php @@ -0,0 +1,9 @@ +join('.'); + $parts = [config('feature-flags.cache_prefix'), $feature]; + + return implode('.', array_filter($parts, 'strlen')); } - public static function isEnabled(string $feature): bool + private static function getFeatureModel(string $feature): ?Model + { + if ($featureModel = config('feature-flags.feature_model')::firstWhere('name', $feature)) { + return $featureModel; + } + + if (is_callable(self::$handleMissingFeatureClosure)) { + call_user_func(self::$handleMissingFeatureClosure, $feature); + } else { + throw new MissingFeatureException("Missing feature: {$feature}"); + } + + return null; + } + + private static function getState(string $feature): FeatureState { - $featureKey = self::getFeatureKey($feature); + $featureKey = self::getFeatureCacheKey($feature); - $state = cache()->store(config('feature-flags.cache_store'))->rememberForever($featureKey, function () use ($feature) { - if ($featureModel = config('feature-flags.feature_model')::where('name', $feature)->first()) { - return $featureModel->state; + $state = self::cache()->rememberForever($featureKey, function () use ($feature) { + if ($featureModel = self::getFeatureModel($feature)) { + return $featureModel->state->value; } return null; }); if ($state === null) { - cache()->store(config('feature-flags.cache_store'))->forget($featureKey); + self::cache()->forget($featureKey); - return false; + return FeatureState::off(); } + return FeatureState::from($state); + } + + public static function handleMissingFeatureWith(Closure $closure): void + { + self::$handleMissingFeatureClosure = $closure; + } + + public static function isEnabled(string $feature): bool + { + $state = self::getState($feature); + switch ($state) { - case 'on': + case FeatureState::on(): return true; - case 'off': + case FeatureState::off(): return false; - case 'restricted': + case FeatureState::dynamic(): { - if (array_key_exists($feature, self::$restricted)) { - return self::$restricted[$feature]($feature, request()) === true; + if (array_key_exists($feature, self::$dynamicHandlers)) { + return self::$dynamicHandlers[$feature]($feature, request()) === true; + } elseif (is_callable(self::$defaultDynamicHandler)) { + return call_user_func(self::$defaultDynamicHandler, $feature, request()) === true; } return false; @@ -56,14 +97,46 @@ public static function isEnabled(string $feature): bool return false; } - public static function updateFeatureState(string $feature, string $state) + public static function reset(): void { - $featureModel = config('feature-flags.feature_model')::where('name', $feature)->first(); + self::$dynamicHandlers = []; + self::$defaultDynamicHandler = null; + self::$handleMissingFeatureClosure = null; + } + + public static function makeDynamic(string $feature): void + { + self::updateFeatureState($feature, FeatureState::dynamic()); + } + + public static function registerDynamicHandler(string $feature, callable $closure): void + { + self::$dynamicHandlers[$feature] = $closure; + } + + public static function registerDefaultDynamicHandler(Closure $closure): void + { + self::$defaultDynamicHandler = $closure; + } - $featureModel->update(['state' => $state]); + public static function turnOn(string $feature): void + { + self::updateFeatureState($feature, FeatureState::on()); + } - cache()->store(config('feature-flags.cache_store'))->forget(static::getFeatureKey($feature)); + public static function turnOff(string $feature): void + { + self::updateFeatureState($feature, FeatureState::off()); + } - event(new FeatureUpdatedEvent($feature)); + public static function updateFeatureState(string $feature, FeatureState $state): void + { + if ($featureModel = self::getFeatureModel($feature)) { + $featureModel->update(['state' => $state]); + + self::cache()->forget(static::getFeatureCacheKey($feature)); + + event(new FeatureUpdatedEvent($featureModel)); + } } } diff --git a/src/FeatureFlagsServiceProvider.php b/src/FeatureFlagsServiceProvider.php index 4520a83..2150648 100644 --- a/src/FeatureFlagsServiceProvider.php +++ b/src/FeatureFlagsServiceProvider.php @@ -2,9 +2,10 @@ namespace Codinglabs\FeatureFlags; +use Illuminate\Support\Facades\Blade; use Spatie\LaravelPackageTools\Package; +use Codinglabs\FeatureFlags\Facades\FeatureFlag; use Spatie\LaravelPackageTools\PackageServiceProvider; -use Codinglabs\FeatureFlags\Commands\FeaturesCommand; class FeatureFlagsServiceProvider extends PackageServiceProvider { @@ -20,4 +21,18 @@ public function configurePackage(Package $package): void ->hasConfigFile() ->hasMigration('create_features_table'); } + + public function packageRegistered() + { + $this->app->singleton('features', function () { + return new FeatureFlags(); + }); + } + + public function packageBooted() + { + Blade::if('feature', function ($value) { + return FeatureFlag::isEnabled($value); + }); + } } diff --git a/src/Models/Feature.php b/src/Models/Feature.php index ece3606..67b10e8 100644 --- a/src/Models/Feature.php +++ b/src/Models/Feature.php @@ -2,9 +2,16 @@ namespace Codinglabs\FeatureFlags\Models; +use Codinglabs\FeatureFlags\Casts\FeatureStateCast; use Illuminate\Database\Eloquent\Factories\HasFactory; class Feature extends \Illuminate\Database\Eloquent\Model { use HasFactory; -} \ No newline at end of file + + protected $guarded = []; + + protected $casts = [ + 'state' => FeatureStateCast::class + ]; +} diff --git a/src/Traits/HasFeatureStates.php b/src/Traits/HasFeatureStates.php new file mode 100644 index 0000000..e207797 --- /dev/null +++ b/src/Traits/HasFeatureStates.php @@ -0,0 +1,7 @@ +flush(); - +beforeEach(function () { config([ - 'feature-flags.cache_prefix' => 'testing', 'feature-flags.cache_store' => 'array', + 'feature-flags.cache_prefix' => 'testing', ]); + + cache()->store('array')->clear(); +}); + +afterEach(function () { + FeatureFlag::reset(); +}); + +it('throws an exception if calling isEnabled for a feature that does not exist', function () { + $this->expectException(MissingFeatureException::class); + + FeatureFlag::isEnabled('some-feature'); + expect(cache()->store('array')->get('testing.some-feature'))->toBeNull(); }); it('generates the correct cache key', function () { - expect(FeatureFlags::getFeatureKey('some-feature'))->toBe('testing.some-feature'); + config(['feature-flags.cache_prefix' => 'some-prefix']); + + expect(FeatureFlag::getFeatureCacheKey('some-feature'))->toBe('some-prefix.some-feature'); }); -it('feature is not enabled if it does not exist', function () { - expect(FeatureFlags::isEnabled('some-feature'))->toBeFalse(); - expect(cache()->driver(config('feature-flags.cache_store'))->get('testing.some-feature'))->toBeNull(); +it('handles a missing feature exception when a global handler has been defined', function () { + FeatureFlag::handleMissingFeatureWith(function ($feature) { + // handling... + }); + + expect(FeatureFlag::isEnabled('some-feature'))->toBeFalse(); + expect(cache()->store('array')->get('testing.some-feature'))->toBeNull(); }); -it('feature is not enabled if state is off', function () { +it('resolves isEnabled to false when the features state is "off"', function () { Feature::factory()->create([ 'name' => 'some-feature', - 'state' => 'off' + 'state' => FeatureState::off() ]); - expect(FeatureFlags::isEnabled('some-feature'))->toBeFalse(); - expect(cache()->driver(config('feature-flags.cache_store'))->get('testing.some-feature'))->toBe('off'); + expect(FeatureFlag::isEnabled('some-feature'))->toBeFalse(); + expect(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::off()->value); }); -it('feature is enabled if restricted and closure returns true', function () { +it('resolves isEnabled to true when the features state is "on"', function () { Feature::factory()->create([ 'name' => 'some-feature', - 'state' => 'restricted' + 'state' => FeatureState::on() ]); - FeatureFlags::restrictFeatureWith('some-feature', function($feature) { - expect($feature)->toBe('some-feature'); + expect(FeatureFlag::isEnabled('some-feature'))->toBeTrue(); + expect(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::on()->value); +}); + +it('resolves isEnabled to true when feature state is "restricted" and the closure returns true', function () { + Feature::factory()->create([ + 'name' => 'some-feature', + 'state' => FeatureState::dynamic(), + ]); + FeatureFlag::registerDynamicHandler('some-feature', function ($feature) { return true; }); - expect(FeatureFlags::isEnabled('some-feature'))->toBeTrue(); - expect(cache()->driver(config('feature-flags.cache_store'))->get('testing.some-feature'))->toBe('restricted'); + expect(FeatureFlag::isEnabled('some-feature'))->toBeTrue(); + expect(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::dynamic()->value); }); -it('feature is not enabled if restricted and closure returns false', function () { +it('resolves isEnabled to false when feature state is "restricted" and the closure returns false', function () { Feature::factory()->create([ 'name' => 'some-feature', - 'state' => 'restricted' + 'state' => FeatureState::dynamic(), ]); - FeatureFlags::restrictFeatureWith('some-feature', function($feature) { - expect($feature)->toBe('some-feature'); - + FeatureFlag::registerDynamicHandler('some-feature', function ($feature) { return false; }); - expect(FeatureFlags::isEnabled('some-feature'))->toBeFalse(); - expect(cache()->driver(config('feature-flags.cache_store'))->get('testing.some-feature'))->toBe('restricted'); -}); \ No newline at end of file + expect(FeatureFlag::isEnabled('some-feature'))->toBeFalse(); + expect(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::dynamic()->value); +}); + +it('uses the default restricted closure if no feature specific closure has been defined', function () { + Feature::factory()->create([ + 'name' => 'some-feature', + 'state' => FeatureState::dynamic(), + ]); + + FeatureFlag::registerDefaultDynamicHandler(function () { + return true; + }); + + expect(FeatureFlag::isEnabled('some-feature'))->toBeTrue(); + expect(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::dynamic()->value); +}); + +it('resolves isEnabled to false when feature state is "restricted" and no restricted closure has been defined', function () { + Feature::factory()->create([ + 'name' => 'some-feature', + 'state' => FeatureState::dynamic(), + ]); + + expect(FeatureFlag::isEnabled('some-feature'))->toBeFalse(); +}); + +it('can update a features state', function () { + Event::fake(); + + Feature::factory()->create([ + 'name' => 'some-feature', + 'state' => FeatureState::off() + ]); + + cache()->store('array')->set('testing.some-feature', 'off'); + + FeatureFlag::updateFeatureState('some-feature', FeatureState::on()); + + Event::assertDispatched(\Codinglabs\FeatureFlags\Events\FeatureUpdatedEvent::class); + expect(FeatureFlag::isEnabled('some-feature'))->toBeTrue(); + expect(cache()->store('array')->get('testing.some-feature'))->toBe(FeatureState::on()->value); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 2da5372..e7a66f6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,8 +2,8 @@ namespace Codinglabs\FeatureFlags\Tests; -use Illuminate\Database\Eloquent\Factories\Factory; use Orchestra\Testbench\TestCase as Orchestra; +use Illuminate\Database\Eloquent\Factories\Factory; use Codinglabs\FeatureFlags\FeatureFlagsServiceProvider; class TestCase extends Orchestra @@ -13,7 +13,7 @@ protected function setUp(): void parent::setUp(); Factory::guessFactoryNamesUsing( - fn (string $modelName) => 'Codinglabs\\FeatureFlags\\Database\\Factories\\'.class_basename($modelName).'Factory' + fn (string $modelName) => 'Codinglabs\\FeatureFlags\\Database\\Factories\\' . class_basename($modelName) . 'Factory' ); } @@ -29,7 +29,7 @@ public function getEnvironmentSetUp($app) config()->set('database.default', 'testing'); - $migration = include __DIR__.'/../database/migrations/create_features_table.php.stub'; + $migration = include __DIR__ . '/../database/migrations/create_features_table.php.stub'; $migration->up(); } }