diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml
index ca2197d..67e66c6 100644
--- a/.github/workflows/dependabot-auto-merge.yml
+++ b/.github/workflows/dependabot-auto-merge.yml
@@ -13,7 +13,7 @@ jobs:
- name: Dependabot metadata
id: metadata
- uses: dependabot/fetch-metadata@v1.6.0
+ uses: dependabot/fetch-metadata@v2.0.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml
index 7520a18..255506b 100644
--- a/.github/workflows/fix-php-code-style-issues.yml
+++ b/.github/workflows/fix-php-code-style-issues.yml
@@ -14,14 +14,14 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: Fix PHP code style issues
- uses: aglipanci/laravel-pint-action@2.3.0
+ uses: aglipanci/laravel-pint-action@2.4
- name: Commit changes
- uses: stefanzweifel/git-auto-commit-action@v4
+ uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Fix styling
diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml
index 9d41c0c..ccfa9d9 100644
--- a/.github/workflows/phpstan.yml
+++ b/.github/workflows/phpstan.yml
@@ -11,7 +11,7 @@ jobs:
name: phpstan
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index b5d6720..7fefe35 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml
index 8c12ba9..ec40921 100644
--- a/.github/workflows/update-changelog.yml
+++ b/.github/workflows/update-changelog.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
ref: main
@@ -24,7 +24,7 @@ jobs:
release-notes: ${{ github.event.release.body }}
- name: Commit updated CHANGELOG
- uses: stefanzweifel/git-auto-commit-action@v4
+ uses: stefanzweifel/git-auto-commit-action@v5
with:
branch: main
commit_message: Update CHANGELOG
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77ef5c4..e696787 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
All notable changes to `enum-features` will be documented in this file.
+## v1.1.0 - 2023-07-05
+
+**Full Changelog**: https://github.com/defstudio/enum-features/compare/v1.0.0...v1.1.0
+
## v1.0.0 - 2023-07-03
### What's Changed
diff --git a/README.md b/README.md
index 5021fee..d89fb4a 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,22 @@ AppFeature::impersonate->enforce(); //throws "Feature [impersonate] is not enabl
```
+### Blade directives
+
+In blade files, a feature can be checked with `@feature` directive:
+
+```html
+
+@feature(AppFeature::multi_language)
+
+@endfeature
+
+```
+
### Customizing where and how to store enabled features
Enabled features are usually stored in config('app.features'), but this behaviour can be customized by
diff --git a/composer.json b/composer.json
index 3131f60..4d5d8b1 100644
--- a/composer.json
+++ b/composer.json
@@ -1,12 +1,13 @@
{
"name": "defstudio/enum-features",
- "description": "A simple trait to enable a feature system using Enums",
+ "description": "A simple trait to enable a feature system using Enums and Laravel Pennant",
"keywords": [
"defstudio",
"laravel",
+ "pennant",
"enum-features",
"enums",
- "feature"
+ "features"
],
"homepage": "https://github.com/defstudio/enum-features",
"license": "MIT",
@@ -18,20 +19,21 @@
}
],
"require": {
- "php": "^8.1",
- "spatie/laravel-package-tools": "^1.14.0"
+ "php": "^8.2",
+ "spatie/laravel-package-tools": "^1.16.4",
+ "illuminate/support": "^v11.4.0",
+ "laravel/pennant": "^v1.7.0"
},
"require-dev": {
- "laravel/pint": "^1.0",
- "nunomaduro/collision": "^7.9",
- "nunomaduro/larastan": "^2.0.1",
- "orchestra/testbench": "^8.0",
- "pestphp/pest": "^2.0",
- "pestphp/pest-plugin-arch": "^2.0",
- "pestphp/pest-plugin-laravel": "^2.0",
- "phpstan/extension-installer": "^1.1",
- "phpstan/phpstan-deprecation-rules": "^1.0",
- "phpstan/phpstan-phpunit": "^1.0"
+ "laravel/pint": "^v1.15.1",
+ "nunomaduro/collision": "^v8.1.1",
+ "larastan/larastan": "^v2.9.5",
+ "orchestra/testbench": "^v9.0.4",
+ "pestphp/pest": "^v2.34.7",
+ "pestphp/pest-plugin-laravel": "^v2.3.0",
+ "phpstan/extension-installer": "^1.3.1",
+ "phpstan/phpstan-deprecation-rules": "^1.1.4",
+ "phpstan/phpstan-phpunit": "^1.3.16"
},
"autoload": {
"psr-4": {
diff --git a/src/Concerns/DefinesFeatures.php b/src/Concerns/DefinesFeatures.php
index e1412bf..f209120 100644
--- a/src/Concerns/DefinesFeatures.php
+++ b/src/Concerns/DefinesFeatures.php
@@ -6,27 +6,201 @@
namespace DefStudio\EnumFeatures\Concerns;
-use DefStudio\EnumFeatures\Exceptions\FeatureException;
+use BackedEnum;
+use Illuminate\Contracts\Auth\Authenticatable;
+use Laravel\Pennant\Feature as Pennant;
+use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
trait DefinesFeatures
{
- public static function enabledFeatures(): array
+ public function define(): void
{
- return config('app.features', []);
+ Pennant::define($this->featureName(), $this->resolve(...));
}
- public function enabled(): bool
+ protected function featureName(): string
{
- return in_array($this, self::enabledFeatures());
+ return $this instanceof BackedEnum ? $this->value : $this->name;
}
- public function enforce(): void
+ public function active(?Authenticatable $scope = null): bool
{
- throw_if(! $this->enabled(), FeatureException::notEnabled($this));
+ if ($scope) {
+ return Pennant::for($scope)->active($this->featureName());
+ }
+
+ return Pennant::active($this->featureName());
+ }
+
+ public function enabled(?Authenticatable $scope = null): bool
+ {
+ return $this->active($scope);
+ }
+
+ public function inactive(?Authenticatable $scope = null): bool
+ {
+ return ! $this->active($scope);
+ }
+
+ public function disabled(?Authenticatable $scope = null): bool
+ {
+ return $this->inactive($scope);
+ }
+
+ public function middleware(): string
+ {
+ return EnsureFeaturesAreActive::using($this->featureName());
+ }
+
+ public function purge(): void
+ {
+ Pennant::purge($this->featureName());
+ }
+
+ protected function resolve(?Authenticatable $scope = null): bool
+ {
+ $featureName = $this->featureName();
+ $camelFeatureName = str($this->featureName())->camel()->ucfirst();
+
+ $try_methods = [
+ "check_$featureName",
+ "check_{$featureName}_feature",
+ "has_$featureName",
+ "has_{$featureName}Feature",
+ "check$camelFeatureName",
+ "check{$camelFeatureName}Feature",
+ "has$camelFeatureName",
+ "has{$camelFeatureName}Feature",
+ ];
+
+ foreach ($try_methods as $method) {
+ if (method_exists($this, $method)) {
+ return $this->{$method}($scope);
+ }
+ }
+
+ return false;
+ }
+
+ public function enforce(?Authenticatable $scope = null): void
+ {
+ if (! $this->active($scope)) {
+ abort(400);
+ }
+ }
+
+ public function activate(?Authenticatable $scope = null): void
+ {
+ if ($scope) {
+ Pennant::for($scope)->activate($this->featureName());
+
+ return;
+ }
+
+ Pennant::activate($this->featureName());
+ }
+
+ public function enable(?Authenticatable $scope = null): void
+ {
+ $this->activate($scope);
+ }
+
+ public function deactivate(?Authenticatable $scope = null): void
+ {
+ if ($scope) {
+ Pennant::for($scope)->deactivate($this->featureName());
+
+ return;
+ }
+
+ Pennant::deactivate($this->featureName());
+ }
+
+ public function disable(?Authenticatable $scope = null): void
+ {
+ $this->deactivate($scope);
+ }
+
+ public function forget(?Authenticatable $scope = null): void
+ {
+ if ($scope) {
+ Pennant::for($scope)->forget($this->featureName());
+
+ return;
+ }
+
+ Pennant::forget($this->featureName());
+ }
+
+ /**
+ * @param array $features
+ */
+ public static function areAllActive(array $features): bool
+ {
+ return Pennant::allAreInactive(collect($features)
+ ->map(fn (self $feature) => $feature->featureName())
+ ->toArray());
+ }
+
+ /**
+ * @param array $features
+ */
+ public static function someAreActive(array $features): bool
+ {
+ return Pennant::someAreActive(collect($features)
+ ->map(fn (self $feature) => $feature->featureName())
+ ->toArray());
+ }
+
+ /**
+ * @param array $features
+ */
+ public static function areAllEnabled(array $features): bool
+ {
+ return self::areAllActive($features);
+ }
+
+ /**
+ * @param array $features
+ */
+ public static function someAreEnabled(array $features): bool
+ {
+ return self::someAreActive($features);
+ }
+
+ /**
+ * @param array $features
+ */
+ public static function areAllInactive(array $features): bool
+ {
+ return Pennant::allAreInactive(collect($features)
+ ->map(fn (self $feature) => $feature->featureName())
+ ->toArray());
+ }
+
+ /**
+ * @param array $features
+ */
+ public static function someAreInactive(array $features): bool
+ {
+ return Pennant::someAreInactive(collect($features)
+ ->map(fn (self $feature) => $feature->featureName())
+ ->toArray());
+ }
+
+ /**
+ * @param array $features
+ */
+ public static function areAllDisabled(array $features): bool
+ {
+ return self::areAllInactive($features);
}
- public function disabled(): bool
+ /**
+ * @param array $features
+ */
+ public static function someAreDisabled(array $features): bool
{
- return ! $this->enabled();
+ return self::someAreInactive($features);
}
}
diff --git a/src/Exceptions/FeatureException.php b/src/Exceptions/FeatureException.php
index e8198cd..9b00b23 100644
--- a/src/Exceptions/FeatureException.php
+++ b/src/Exceptions/FeatureException.php
@@ -8,14 +8,7 @@
class FeatureException extends Exception
{
- public static function notEnabled(UnitEnum $feature): FeatureException
- {
- $name = $feature instanceof BackedEnum ? $feature->value : $feature->name;
-
- return new self("Feature [$name] is not enabled");
- }
-
- public static function invalid_feature_enum(UnitEnum $feature): FeatureException
+ public static function invalid_feature(UnitEnum $feature): FeatureException
{
$name = $feature instanceof BackedEnum ? $feature->value : $feature->name;
diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php
index 777dc28..5a582b8 100644
--- a/src/ServiceProvider.php
+++ b/src/ServiceProvider.php
@@ -20,7 +20,7 @@ public function packageBooted(): void
{
Blade::if('feature', function (UnitEnum $feature) {
if (! class_uses($feature, DefinesFeatures::class)) {
- throw FeatureException::invalid_feature_enum($feature);
+ throw FeatureException::invalid_feature($feature);
}
/** @var DefinesFeatures $feature */
diff --git a/tests/ArchTest.php b/tests/ArchTest.php
deleted file mode 100644
index ccc19b2..0000000
--- a/tests/ArchTest.php
+++ /dev/null
@@ -1,5 +0,0 @@
-expect(['dd', 'dump', 'ray'])
- ->each->not->toBeUsed();
diff --git a/tests/BaseTest.php b/tests/BaseTest.php
deleted file mode 100644
index 660a236..0000000
--- a/tests/BaseTest.php
+++ /dev/null
@@ -1,23 +0,0 @@
-enabled())->toBeTrue();
-
- expect(Feature::other_feature->enabled())->toBeFalse();
-});
-
-it('can check if a feature is not enabled', function () {
- expect(Feature::welcome_email->disabled())->toBeFalse();
-
- expect(Feature::other_feature->disabled())->toBeTrue();
-});
-
-it('can enforce a feature to be enabled', function () {
- Feature::welcome_email->enforce();
-
- expect(fn () => Feature::other_feature->enforce())
- ->toThrow(FeatureException::class, 'Feature [other_feature] is not enabled');
-});
diff --git a/tests/CustomConfigTest.php b/tests/CustomConfigTest.php
deleted file mode 100644
index 9c4f049..0000000
--- a/tests/CustomConfigTest.php
+++ /dev/null
@@ -1,23 +0,0 @@
-enabled())->toBeTrue();
-
- expect(CustomFeature::payments->enabled())->toBeFalse();
-});
-
-it('can check if a feature is not enabled', function () {
- expect(CustomFeature::guest_account->disabled())->toBeFalse();
-
- expect(CustomFeature::payments->disabled())->toBeTrue();
-});
-
-it('can enforce a feature to be enabled', function () {
- CustomFeature::guest_account->enforce();
-
- expect(fn () => CustomFeature::payments->enforce())
- ->toThrow(FeatureException::class, 'Feature [payments] is not enabled');
-});
diff --git a/tests/Fixtures/CustomFeature.php b/tests/Fixtures/CustomFeature.php
deleted file mode 100644
index 42b2e5e..0000000
--- a/tests/Fixtures/CustomFeature.php
+++ /dev/null
@@ -1,18 +0,0 @@
-set('app.features', [
- Feature::multi_language,
- Feature::welcome_email,
- ]);
-
- config()->set('my_package.enabled_features', [
- CustomFeature::guest_account,
- ]);
- }
}