rpillz / laravel-feature-access
Feature access for Laravel apps
Fund package maintenance!
RPillz
Requires
- php: ^8.0 || ^8.1
- illuminate/contracts: ^8.37 || ^9.0
- spatie/laravel-package-tools: ^1.13.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- orchestra/testbench: ^7.9
- pestphp/pest: ^1.20
- spatie/ray: ^1.28
This package is auto-updated.
Last update: 2024-11-10 00:28:49 UTC
README
Add Plans (eg: Basic, Standard, Pro) and Feature restrictions (eg: Can Make 3 Pages, Can Upload Video) to your Laravel app.
Plans and corresponding features are hard-coded in a config file, but these defaults may be overridden with a database entry for a specific user. (ie: Your special friend who wants all the features, but, like, for free.)
Feature access can be assigned to any model (eg: User, Team) via a trait, which adds properties to use in your app logic, such as $user->canViewFeature('pictures-of-my-dog')
Installation
You can install the package via composer:
composer require rpillz/laravel-feature-access
Publish the config file. This is where your features and tiers are defined.
php artisan vendor:publish --tag=feature-access-config
Publish and run migrations. This will create a 'features' table for storing access information attached to your models.
php artisan vendor:publish --tag=feature-access-migrations php artisan migrate
Basic Usage
1. Define Features (and Tiers) in the Config
Start by defining your app features in the feature-access.php config file.
This is an example of a feature mapped out in the config file:
return [ 'sample-feature' => [ // begin with the feature key name. This is used to request permissions. 'name' => 'Test Feature', // Human readable name 'read' => true, // Can read/view items in this feature 'update' => false, // cannot edit/change/update items 'create' => false, // cannot create new items 'destroy' => false, // cannot destroy/delete items 'limit' => 3, // limit on number of items allowed 'levels' => [ // Override the base feature permissions with levels or packages (eg: basic, pro, plus) 'pro' => [ // level/package key 'name' => 'Extra Stuff!', // human readable name 'update' => true, // pro level can edit/change/update items 'create' => true, // pro level can create items 'limit' => 5, // limit is increased // other permissions will default to base feature definition (above) ], ] ], ];
The base level of this feature will be the permission used for all users and guests unless they have been explicitly granted an upgraded access level, or override.
2. Add Trait To Model
In most cases, you would add this trait to your User model, which would allow that user to access (or not access) features.
use RPillz\FeatureAccess\Traits\HasFeatureAccess; class User extends Authenticatable { use HasFeatureAccess; ...
3. Add Permission Checks to your App Logic
An example, in a blade template, of adding a button to create a new item, only if the current user is allowed to create on the feature sample-feature.
@if(Auth::user()->canCreateFeature('sample-feature')) <button>Add New Sample Item</button> @endif
4. Grant Higher Access To Users
You can set permission acces for your User (or any model) with the setFeatureAccess() method.
$user->setFeatureAccess('sample-feature', 'basic'); // give this user 'basic' level access to 'sample-feature' (which does not exist) $user->setFeatureAccess('sample-feature', 'pro'); // give this user 'pro' level access to 'sample-feature' $user->setFeatureAccess('sample-feature', 'pro', [ 'update' => false ]); // give this user 'pro' level access to 'sample-feature', but override the default setting to allow edits just for this user.
In the first example above, the user is granted basic level access to sample-feature. However, there is no basic level defined in the feature config file. Their permissions for sample-feature will default to the basic settings. These same default settings would be used if no level has been explicitly set for a user.
More Usage Options
Using Team Model
Your app may make use of Teams (a la Jetstream) in which case you may want to have the Team model using this trait, and accessing user permissions via their team.
Add the HasFeatureAccess trait to your Team model. Now any user on that team will have access to the same features, through the team.
Auth::user()->currentTeam->canReadFeature('maple-syrup');
This trait can be applied to any Model, or even multiple models if you want to be able apply permissions to both Users and Teams individually. (There is no built-in permission inheritance in such a case, just one or the other.)
Using Other Models
You can grant feature access to any model using the HasFeatureAccess trait. In most cases, that model would somehow be associated with the authenticated user. However, it could be adapted to other uses:
- On a Tenant or Domain model, which would grant features depending upon the domain through which your app was being accessed.
- On a particular Page or Post to change what features are displayed in the layout.
You can be using this on multiple models within the same app, so you may bend it to your will!
Methods to Check Permission On Your Model
$user->canUseFeature('feature-name', 'permission-requested'); // alias functions $user->canCreateFeature('sample-feature'); // permission-requested = create $user->canReadFeature('sample-feature'); // permission-requested = read $user->canViewFeature('sample-feature'); // permission-requested = read $user->canUpdateFeature('sample-feature'); // permission-requested = update $user->canEditFeature('sample-feature'); // permission-requested = update $user->canDestroyFeature('sample-feature'); // permission-requested = destroy $user->canDeleteFeature('sample-feature'); // permission-requested = destroy $user->withinFeatureLimit('sample-feature', $user->items->count()); // compares against feature limit $user->withinFeatureLimit('sample-feature', $user->items->count(), 1); // is there room to add 1 more item? // or get all the permission data for your model $user->getFeatureData('sample-feature'); // return array
Using The Facade
You can use the Facade methods to check permissions. However, this only works for the User and/or Team models. The upside is these will work whether or not a visitor is signed in to an account, and returns base access for guests.
FeatureAccess::userCan(string $feature_name, string $permission); // check for permission to 'create', 'read', 'update', or 'destroy' // shortcut aliases FeatureAccess::userCanCreate('sample-feature'); // returns boolean true/false FeatureAccess::userCanRead('sample-feature'); FeatureAccess::userCanView('sample-feature'); // alias of 'read' permission FeatureAccess::userCanUpdate('sample-feature'); FeatureAccess::userCanEdit('sample-feature'); // alias of 'update' permission FeatureAccess::userCanDestroy('sample-feature'); FeatureAccess::userCanDelete('sample-feature'); // alias of 'destroy' permission // it also works for Teams! (a la Jetstream) FeatureAccess::teamCan(string $feature_name, string $permission); FeatureAccess::teamCanCreate('sample-feature'); // and all the other aliases as per above
If you need to get the full array of current permission access, you can use this:
FeatureAccess::userFeature('sample-feature'); // returns array FeatureAccess::teamFeature('sample-feature'); // returns array
Integrating with Subscriptions (from Cashier, SPark, etc.)
Rather than creating/updating duplicate database rows to change a user's feature access level, there is a feature to dynamically check the active subscription level for a user, and get the feature permissions accordingly.
This feature must be activated in the feature-access.php config file.
// Check for active subscription to grant access 'subscriptions' => true,
Then you need to add a function on your User model (or Team model, or whatever is using the HasFeatureAccess trait) which will return a name corresponding to the appropriate level, as defined in your feature config.
public function getFeatureSubscriptionLevel(string $feature_name = null): ?string { // this logic needs to be customized according to your subscription set-up if ($subscription = $this->subscriptions->active()->first()){ return $subscription->name; // ie: "pro" } return null; }
Once you have this set, the subscription will be consulted when returning a User's current feature access.
$free_user->canCreateFeature('sample-feature'); //false $pro_user->canCreateFeature('sample-feature'); //true
In order of priority, the user's feature level is first determined by anything saved in the database (by setFeatureAccesss() for example), then checking the subscription level (if activated) and finally defaulting to the config file. This means it is possible to upgrade access above and beyond a paid subscription for your VIP users on a case-by-case basis. (technically, you could also downgrade them, you monster!)
Here's an example of the function to go along with Laravel Spark subscriptions.
public function getFeatureSubscriptionLevel(string $feature_name = null): ?string { // If you're using Laravel Spark with this model as a Billable. // Note: The plan name from Spark is likely capitalized. Make sure it matches your feature-access config level names exactly. return $this->sparkPlan()->name ?: null; }
Notice, also, that this function accepts an optional $feature_name argument, which allows you to be more granular in your subscription and logic if needed. (ie: if the subscription to access Pages is different from the subscription to access Blogs)
Super-Admin Permission
If the current model property matches anything in the array, all permission tests will return true. They get to do everything.
return [ // grant all access to models which match... 'super_admin_property' => 'email', // this property... 'super_admin_access' => [ // to any of these. 'admin@example.com', ], ]
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.