tipo: modificación del cpanel y gitignore de la carpeta vendor

This commit is contained in:
2026-07-07 11:35:09 -06:00
parent a0c91dc567
commit 676ef0d506
8510 changed files with 1132803 additions and 4 deletions
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Spatie bvba <info@spatie.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+99
View File
@@ -0,0 +1,99 @@
<div align="left">
<a href="https://spatie.be/open-source?utm_source=github&utm_medium=banner&utm_campaign=laravel-permission">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://spatie.be/packages/header/laravel-permission/html/dark.webp">
<img alt="Logo for laravel-permission" src="https://spatie.be/packages/header/laravel-permission/html/light.webp">
</picture>
</a>
<h1>Associate users with permissions and roles</h1>
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission)
[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-permission/run-tests.yml?branch=main&label=Tests)](https://github.com/spatie/laravel-permission/actions?query=workflow%3ATests+branch%3Amain)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-permission.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-permission)
</div>
## Documentation, Installation, and Usage Instructions
See the [documentation](https://spatie.be/docs/laravel-permission/) for detailed installation and usage instructions.
## What It Does
This package allows you to manage user permissions and roles in a database.
Once installed you can do stuff like this:
```php
// Adding permissions to a user
$user->givePermissionTo('edit articles');
// Adding permissions via a role
$user->assignRole('writer');
$role->givePermissionTo('edit articles');
```
Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function:
```php
$user->can('edit articles');
```
## Support us
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/laravel-permission.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/laravel-permission)
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).
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
## Contributing
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
### Testing
``` bash
composer test
```
### Security
If you discover any security-related issues, please email [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker.
## Postcardware
You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using.
Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium.
We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards).
## Credits
- [Chris Brown](https://github.com/drbyte)
- [Freek Van der Herten](https://github.com/freekmurze)
- [All Contributors](../../contributors)
This package is heavily based on [Jeffrey Way](https://twitter.com/jeffrey_way)'s awesome [Laracasts](https://laracasts.com) lessons
on [permissions and roles](https://laracasts.com/series/whats-new-in-laravel-5-1/episodes/16). His original code
can be found [in this repo on GitHub](https://github.com/laracasts/laravel-5-roles-and-permissions-demo).
Special thanks to [Alex Vanderbist](https://github.com/AlexVanderbist) who greatly helped with `v2`, and to [Chris Brown](https://github.com/drbyte) for his longtime support helping us maintain the package.
Special thanks to [Caneco](https://twitter.com/caneco) for the original logo.
## Alternatives
- [Povilas Korop](https://twitter.com/@povilaskorop) did an excellent job listing the alternatives [in an article on Laravel News](https://laravel-news.com/two-best-roles-permissions-packages). In that same article, he compares laravel-permission to [Joseph Silber](https://github.com/JosephSilber)'s [Bouncer]((https://github.com/JosephSilber/bouncer)), which in our book is also an excellent package.
- [santigarcor/laratrust](https://github.com/santigarcor/laratrust) implements team support
- [ultraware/roles](https://github.com/ultraware/roles) (archived) takes a slightly different approach to its features.
- [zizaco/entrust](https://github.com/zizaco/entrust) offers some wildcard pattern matching
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
+71
View File
@@ -0,0 +1,71 @@
{
"name": "spatie/laravel-permission",
"description": "Permission handling for Laravel 8.0 and up",
"license": "MIT",
"keywords": [
"spatie",
"laravel",
"permission",
"permissions",
"roles",
"acl",
"rbac",
"security"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"homepage": "https://github.com/spatie/laravel-permission",
"require": {
"php": "^8.0",
"illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0",
"illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0",
"illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0",
"illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0"
},
"require-dev": {
"laravel/passport": "^11.0|^12.0",
"laravel/pint": "^1.0",
"orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0",
"phpunit/phpunit": "^9.4|^10.1|^11.5"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Spatie\\Permission\\": "src"
},
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Spatie\\Permission\\Tests\\": "tests"
}
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-main": "6.x-dev",
"dev-master": "6.x-dev"
},
"laravel": {
"providers": [
"Spatie\\Permission\\PermissionServiceProvider"
]
}
},
"scripts": {
"test": "phpunit",
"format": "pint",
"analyse": "echo 'Checking dependencies...' && composer require --dev larastan/larastan && phpstan analyse"
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
return [
'models' => [
/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/
'permission' => Spatie\Permission\Models\Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/
'role' => Spatie\Permission\Models\Role::class,
],
'table_names' => [
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'roles' => 'roles',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'permissions' => 'permissions',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_permissions' => 'model_has_permissions',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_roles' => 'model_has_roles',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
/*
* Change this if you want to name the related pivots other than defaults
*/
'role_pivot_key' => null, // default 'role_id',
'permission_pivot_key' => null, // default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
'model_morph_key' => 'model_id',
/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/
'team_foreign_key' => 'team_id',
],
/*
* When set to true, the method for checking permissions will be registered on the gate.
* Set this to false if you want to implement custom logic for checking permissions.
*/
'register_permission_check_method' => true,
/*
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
*/
'register_octane_reset_listener' => false,
/*
* Events will fire when a role or permission is assigned/unassigned:
* \Spatie\Permission\Events\RoleAttached
* \Spatie\Permission\Events\RoleDetached
* \Spatie\Permission\Events\PermissionAttached
* \Spatie\Permission\Events\PermissionDetached
*
* To enable, set to true, and then create listeners to watch these events.
*/
'events_enabled' => false,
/*
* Teams Feature.
* When set to true the package implements teams using the 'team_foreign_key'.
* If you want the migrations to register the 'team_foreign_key', you must
* set this to true before doing the migration.
* If you already did the migration then you must make a new migration to also
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
* (view the latest version of this package's migration file)
*/
'teams' => false,
/*
* The class to use to resolve the permissions team id
*/
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
/*
* Passport Client Credentials Grant
* When set to true the package will use Passports Client to check permissions
*/
'use_passport_client_credentials' => false,
/*
* When set to true, the required permission names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
/*
* When set to true, the required role names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_role_in_exception' => false,
/*
* By default wildcard permission lookups are disabled.
* See documentation to understand supported syntax.
*/
'enable_wildcard_permission' => false,
/*
* The class to use for interpreting wildcard permissions.
* If you need to modify delimiters, override the class and specify its name here.
*/
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
/* Cache-specific settings */
'cache' => [
/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
*/
'key' => 'spatie.permission.cache',
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/
'store' => 'default',
],
];
@@ -0,0 +1,91 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
if (! $teams) {
return;
}
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
if (empty($columnNames['team_foreign_key'] ?? null)) {
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
if (! Schema::hasColumn($tableNames['roles'], $columnNames['team_foreign_key'])) {
Schema::table($tableNames['roles'], function (Blueprint $table) use ($columnNames) {
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable()->after('id');
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
$table->dropUnique('roles_name_guard_name_unique');
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
});
}
if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) {
Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission) {
$table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
if (DB::getDriverName() !== 'sqlite') {
$table->dropForeign([$pivotPermission]);
}
$table->dropPrimary();
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
if (DB::getDriverName() !== 'sqlite') {
$table->foreign($pivotPermission)
->references('id')->on($tableNames['permissions'])->onDelete('cascade');
}
});
}
if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) {
Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole) {
$table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
if (DB::getDriverName() !== 'sqlite') {
$table->dropForeign([$pivotRole]);
}
$table->dropPrimary();
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
if (DB::getDriverName() !== 'sqlite') {
$table->foreign($pivotRole)
->references('id')->on($tableNames['roles'])->onDelete('cascade');
}
});
}
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
};
@@ -0,0 +1,134 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), Exception::class, 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
};
+72
View File
@@ -0,0 +1,72 @@
{
"$schema": "https://laravel-ide.com/schema/laravel-ide-v2.json",
"blade": {
"directives": [
{
"name": "role",
"prefix": "<?php if(\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper('hasRole', {",
"suffix": "})): ?>"
},
{
"name": "elserole",
"prefix": "<?php elseif(\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper('hasRole', {",
"suffix": "})): ?>"
},
{
"name": "endrole",
"prefix": "",
"suffix": ""
},
{
"name": "hasrole",
"prefix": "<?php if(\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper('hasRole', {",
"suffix": "})): ?>"
},
{
"name": "endhasrole",
"prefix": "",
"suffix": ""
},
{
"name": "hasanyrole",
"prefix": "<?php if(\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper('hasAnyRole', {",
"suffix": "})): ?>"
},
{
"name": "endhasanyrole",
"prefix": "",
"suffix": ""
},
{
"name": "hasallroles",
"prefix": "<?php if(\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper('hasAllRoles', {",
"suffix": "})): ?>"
},
{
"name": "endhasallroles",
"prefix": "",
"suffix": ""
},
{
"name": "unlessrole",
"prefix": "<?php if(! \\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper('hasRole', {",
"suffix": "})): ?>"
},
{
"name": "endunlessrole",
"prefix": "",
"suffix": ""
},
{
"name": "hasexactroles",
"prefix": "<?php if(\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper('hasExactRoles', {",
"suffix": "})): ?>"
},
{
"name": "endhasexactroles",
"prefix": "",
"suffix": ""
}
]
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"preset": "laravel",
"rules": {
"php_unit_method_casing": false
}
}
@@ -0,0 +1,51 @@
<?php
namespace Spatie\Permission\Commands;
use Illuminate\Console\Command;
use Spatie\Permission\Contracts\Role as RoleContract;
class AssignRole extends Command
{
protected $signature = 'permission:assign-role
{name : The name of the role}
{userId : The ID of the user to assign the role to}
{guard? : The name of the guard}
{userModelNamespace=App\Models\User : The fully qualified class name of the user model}';
protected $description = 'Assign a role to a user. (Note: does not support Teams.)';
public function handle()
{
$roleName = $this->argument('name');
$userId = $this->argument('userId');
$guardName = $this->argument('guard');
$userModelClass = $this->argument('userModelNamespace');
// Validate that the model class exists and is instantiable
if (! class_exists($userModelClass)) {
$this->error("User model class [{$userModelClass}] does not exist.");
return Command::FAILURE;
}
$user = (new $userModelClass)::find($userId);
if (! $user) {
$this->error("User with ID {$userId} not found.");
return Command::FAILURE;
}
/** @var \Spatie\Permission\Contracts\Role $roleClass */
$roleClass = app(RoleContract::class);
$role = $roleClass::findOrCreate($roleName, $guardName);
$user->assignRole($role);
$this->info("Role `{$role->name}` assigned to user ID {$userId} successfully.");
return Command::SUCCESS;
}
}
@@ -0,0 +1,25 @@
<?php
namespace Spatie\Permission\Commands;
use Illuminate\Console\Command;
use Spatie\Permission\PermissionRegistrar;
class CacheReset extends Command
{
protected $signature = 'permission:cache-reset';
protected $description = 'Reset the permission cache';
public function handle()
{
$permissionRegistrar = app(PermissionRegistrar::class);
$cacheExists = $permissionRegistrar->getCacheRepository()->has($permissionRegistrar->cacheKey);
if ($permissionRegistrar->forgetCachedPermissions()) {
$this->info('Permission cache flushed.');
} elseif ($cacheExists) {
$this->error('Unable to flush cache.');
}
}
}
@@ -0,0 +1,24 @@
<?php
namespace Spatie\Permission\Commands;
use Illuminate\Console\Command;
use Spatie\Permission\Contracts\Permission as PermissionContract;
class CreatePermission extends Command
{
protected $signature = 'permission:create-permission
{name : The name of the permission}
{guard? : The name of the guard}';
protected $description = 'Create a permission';
public function handle()
{
$permissionClass = app(PermissionContract::class);
$permission = $permissionClass::findOrCreate($this->argument('name'), $this->argument('guard'));
$this->info("Permission `{$permission->name}` ".($permission->wasRecentlyCreated ? 'created' : 'already exists'));
}
}
@@ -0,0 +1,67 @@
<?php
namespace Spatie\Permission\Commands;
use Illuminate\Console\Command;
use Spatie\Permission\Contracts\Permission as PermissionContract;
use Spatie\Permission\Contracts\Role as RoleContract;
use Spatie\Permission\PermissionRegistrar;
class CreateRole extends Command
{
protected $signature = 'permission:create-role
{name : The name of the role}
{guard? : The name of the guard}
{permissions? : A list of permissions to assign to the role, separated by | }
{--team-id=}';
protected $description = 'Create a role';
public function handle(PermissionRegistrar $permissionRegistrar)
{
$roleClass = app(RoleContract::class);
$teamIdAux = getPermissionsTeamId();
setPermissionsTeamId($this->option('team-id') ?: null);
if (! $permissionRegistrar->teams && $this->option('team-id')) {
$this->warn('Teams feature disabled, argument --team-id has no effect. Either enable it in permissions config file or remove --team-id parameter');
return;
}
$role = $roleClass::findOrCreate($this->argument('name'), $this->argument('guard'));
setPermissionsTeamId($teamIdAux);
$teams_key = $permissionRegistrar->teamsKey;
if ($permissionRegistrar->teams && $this->option('team-id') && is_null($role->$teams_key)) {
$this->warn("Role `{$role->name}` already exists on the global team; argument --team-id has no effect");
}
$role->givePermissionTo($this->makePermissions($this->argument('permissions')));
$this->info("Role `{$role->name}` ".($role->wasRecentlyCreated ? 'created' : 'updated'));
}
/**
* @param array|null|string $string
*/
protected function makePermissions($string = null)
{
if (empty($string)) {
return;
}
$permissionClass = app(PermissionContract::class);
$permissions = explode('|', $string);
$models = [];
foreach ($permissions as $permission) {
$models[] = $permissionClass::findOrCreate(trim($permission), $this->argument('guard'));
}
return collect($models);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace Spatie\Permission\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Permission as PermissionContract;
use Spatie\Permission\Contracts\Role as RoleContract;
use Symfony\Component\Console\Helper\TableCell;
class Show extends Command
{
protected $signature = 'permission:show
{guard? : The name of the guard}
{style? : The display style (default|borderless|compact|box)}';
protected $description = 'Show a table of roles and permissions per guard';
public function handle()
{
$permissionClass = app(PermissionContract::class);
$roleClass = app(RoleContract::class);
$teamsEnabled = config('permission.teams');
$team_key = config('permission.column_names.team_foreign_key');
$style = $this->argument('style') ?? 'default';
$guard = $this->argument('guard');
if ($guard) {
$guards = Collection::make([$guard]);
} else {
$guards = $permissionClass::pluck('guard_name')->merge($roleClass::pluck('guard_name'))->unique();
}
foreach ($guards as $guard) {
$this->info("Guard: $guard");
$roles = $roleClass::whereGuardName($guard)
->with('permissions')
->when($teamsEnabled, fn ($q) => $q->orderBy($team_key))
->orderBy('name')->get()->mapWithKeys(fn ($role) => [
$role->name.'_'.($teamsEnabled ? ($role->$team_key ?: '') : '') => [
'permissions' => $role->permissions->pluck($permissionClass->getKeyName()),
$team_key => $teamsEnabled ? $role->$team_key : null,
],
]);
$permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', $permissionClass->getKeyName());
$body = $permissions->map(fn ($permission, $id) => $roles->map(
fn (array $role_data) => $role_data['permissions']->contains($id) ? ' ✔' : ' ·'
)->prepend($permission)
);
if ($teamsEnabled) {
$teams = $roles->groupBy($team_key)->values()->map(
fn ($group, $id) => new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()])
);
}
$this->table(
array_merge(
isset($teams) ? $teams->prepend(new TableCell(''))->toArray() : [],
$roles->keys()->map(function ($val) {
$name = explode('_', $val);
array_pop($name);
return implode('_', $name);
})
->prepend(new TableCell(''))->toArray(),
),
$body->toArray(),
$style
);
}
}
}
@@ -0,0 +1,122 @@
<?php
namespace Spatie\Permission\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
class UpgradeForTeams extends Command
{
protected $signature = 'permission:setup-teams';
protected $description = 'Setup the teams feature by generating the associated migration.';
protected $migrationSuffix = 'add_teams_fields.php';
public function handle()
{
if (! Config::get('permission.teams')) {
$this->error('Teams feature is disabled in your permission.php file.');
$this->warn('Please enable the teams setting in your configuration.');
return;
}
$this->line('');
$this->info('The teams feature setup is going to add a migration and a model');
$existingMigrations = $this->alreadyExistingMigrations();
if ($existingMigrations) {
$this->line('');
$this->warn($this->getExistingMigrationsWarning($existingMigrations));
}
$this->line('');
if (! $this->confirm('Proceed with the migration creation?', true)) {
return;
}
$this->line('');
$this->line('Creating migration');
if ($this->createMigration()) {
$this->info('Migration created successfully.');
} else {
$this->error(
"Couldn't create migration.\n".
'Check the write permissions within the database/migrations directory.'
);
}
$this->line('');
}
/**
* Create the migration.
*
* @return bool
*/
protected function createMigration()
{
try {
$migrationStub = __DIR__."/../../database/migrations/{$this->migrationSuffix}.stub";
copy($migrationStub, $this->getMigrationPath());
return true;
} catch (\Throwable $e) {
$this->error($e->getMessage());
return false;
}
}
/**
* Build a warning regarding possible duplication
* due to already existing migrations.
*
* @return string
*/
protected function getExistingMigrationsWarning(array $existingMigrations)
{
if (count($existingMigrations) > 1) {
$base = "Setup teams migrations already exist.\nFollowing files were found: ";
} else {
$base = "Setup teams migration already exists.\nFollowing file was found: ";
}
return $base.array_reduce($existingMigrations, fn ($carry, $fileName) => $carry."\n - ".$fileName);
}
/**
* Check if there is another migration
* with the same suffix.
*
* @return array
*/
protected function alreadyExistingMigrations()
{
$matchingFiles = glob($this->getMigrationPath('*'));
return array_map(fn ($path) => basename($path), $matchingFiles);
}
/**
* Get the migration path.
*
* The date parameter is optional for ability
* to provide a custom value or a wildcard.
*
* @param string|null $date
* @return string
*/
protected function getMigrationPath($date = null)
{
$date = $date ?: date('Y_m_d_His');
return database_path("migrations/{$date}_{$this->migrationSuffix}");
}
}
@@ -0,0 +1,43 @@
<?php
namespace Spatie\Permission\Contracts;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int|string $id
* @property string $name
* @property string|null $guard_name
*
* @mixin \Spatie\Permission\Models\Permission
*
* @phpstan-require-extends \Spatie\Permission\Models\Permission
*/
interface Permission
{
/**
* A permission can be applied to roles.
*/
public function roles(): BelongsToMany;
/**
* Find a permission by its name.
*
*
* @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist
*/
public static function findByName(string $name, ?string $guardName): self;
/**
* Find a permission by its id.
*
*
* @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist
*/
public static function findById(int|string $id, ?string $guardName): self;
/**
* Find or Create a permission by its name and guard name.
*/
public static function findOrCreate(string $name, ?string $guardName): self;
}
@@ -0,0 +1,15 @@
<?php
namespace Spatie\Permission\Contracts;
interface PermissionsTeamResolver
{
public function getPermissionsTeamId(): int|string|null;
/**
* Set the team id for teams/groups support, this id is used when querying permissions/roles
*
* @param int|string|\Illuminate\Database\Eloquent\Model|null $id
*/
public function setPermissionsTeamId($id): void;
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace Spatie\Permission\Contracts;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int|string $id
* @property string $name
* @property string|null $guard_name
*
* @mixin \Spatie\Permission\Models\Role
*
* @phpstan-require-extends \Spatie\Permission\Models\Role
*/
interface Role
{
/**
* A role may be given various permissions.
*/
public function permissions(): BelongsToMany;
/**
* Find a role by its name and guard name.
*
*
* @throws \Spatie\Permission\Exceptions\RoleDoesNotExist
*/
public static function findByName(string $name, ?string $guardName): self;
/**
* Find a role by its id and guard name.
*
*
* @throws \Spatie\Permission\Exceptions\RoleDoesNotExist
*/
public static function findById(int|string $id, ?string $guardName): self;
/**
* Find or create a role by its name and guard name.
*/
public static function findOrCreate(string $name, ?string $guardName): self;
/**
* Determine if the user may perform the given permission.
*
* @param string|int|\Spatie\Permission\Contracts\Permission|\BackedEnum $permission
*/
public function hasPermissionTo($permission, ?string $guardName): bool;
}
@@ -0,0 +1,14 @@
<?php
namespace Spatie\Permission\Contracts;
use Illuminate\Database\Eloquent\Model;
interface Wildcard
{
public function __construct(Model $record);
public function getIndex(): array;
public function implies(string $permission, string $guardName, array $index): bool;
}
@@ -0,0 +1,28 @@
<?php
namespace Spatie\Permission;
use Spatie\Permission\Contracts\PermissionsTeamResolver;
class DefaultTeamResolver implements PermissionsTeamResolver
{
protected int|string|null $teamId = null;
/**
* Set the team id for teams/groups support, this id is used when querying permissions/roles
*
* @param int|string|\Illuminate\Database\Eloquent\Model|null $id
*/
public function setPermissionsTeamId($id): void
{
if ($id instanceof \Illuminate\Database\Eloquent\Model) {
$id = $id->getKey();
}
$this->teamId = $id;
}
public function getPermissionsTeamId(): int|string|null
{
return $this->teamId;
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Spatie\Permission\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Permission;
class PermissionAttached
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/**
* Internally the HasPermissions trait passes an array of permission ids (eg: int's or uuid's)
* Theoretically one could register the event to other places and pass an Eloquent record.
* So a Listener should inspect the type of $permissionsOrIds received before using.
*
* @param array|int[]|string[]|Permission|Permission[]|Collection $permissionsOrIds
*/
public function __construct(public Model $model, public mixed $permissionsOrIds) {}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Spatie\Permission\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Permission;
class PermissionDetached
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/**
* Internally the HasPermissions trait passes $permissionsOrIds as an Eloquent record.
* Theoretically one could register the event to other places and pass an array etc.
* So a Listener should inspect the type of $permissionsOrIds received before using.
*
* @param array|int[]|string[]|Permission|Permission[]|Collection $permissionsOrIds
*/
public function __construct(public Model $model, public mixed $permissionsOrIds) {}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Spatie\Permission\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Role;
class RoleAttached
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/**
* Internally the HasRoles trait passes an array of role ids (eg: int's or uuid's)
* Theoretically one could register the event to other places passing other types
* So a Listener should inspect the type of $rolesOrIds received before using.
*
* @param array|int[]|string[]|Role|Role[]|Collection $rolesOrIds
*/
public function __construct(public Model $model, public mixed $rolesOrIds) {}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Spatie\Permission\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Role;
class RoleDetached
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/**
* Internally the HasRoles trait passes an array of role ids (eg: int's or uuid's)
* Theoretically one could register the event to other places passing other types
* So a Listener should inspect the type of $rolesOrIds received before using.
*
* @param array|int[]|string[]|Role|Role[]|Collection $rolesOrIds
*/
public function __construct(public Model $model, public mixed $rolesOrIds) {}
}
@@ -0,0 +1,17 @@
<?php
namespace Spatie\Permission\Exceptions;
use Illuminate\Support\Collection;
use InvalidArgumentException;
class GuardDoesNotMatch extends InvalidArgumentException
{
public static function create(string $givenGuard, Collection $expectedGuards)
{
return new static(__('The given role or permission should use guard `:expected` instead of `:given`.', [
'expected' => $expectedGuards->implode(', '),
'given' => $givenGuard,
]));
}
}
@@ -0,0 +1,16 @@
<?php
namespace Spatie\Permission\Exceptions;
use InvalidArgumentException;
class PermissionAlreadyExists extends InvalidArgumentException
{
public static function create(string $permissionName, string $guardName)
{
return new static(__('A `:permission` permission already exists for guard `:guard`.', [
'permission' => $permissionName,
'guard' => $guardName,
]));
}
}
@@ -0,0 +1,28 @@
<?php
namespace Spatie\Permission\Exceptions;
use InvalidArgumentException;
class PermissionDoesNotExist extends InvalidArgumentException
{
public static function create(string $permissionName, ?string $guardName)
{
return new static(__('There is no permission named `:permission` for guard `:guard`.', [
'permission' => $permissionName,
'guard' => $guardName,
]));
}
/**
* @param int|string $permissionId
* @return static
*/
public static function withId($permissionId, ?string $guardName)
{
return new static(__('There is no [permission] with ID `:id` for guard `:guard`.', [
'id' => $permissionId,
'guard' => $guardName,
]));
}
}
@@ -0,0 +1,16 @@
<?php
namespace Spatie\Permission\Exceptions;
use InvalidArgumentException;
class RoleAlreadyExists extends InvalidArgumentException
{
public static function create(string $roleName, string $guardName)
{
return new static(__('A role `:role` already exists for guard `:guard`.', [
'role' => $roleName,
'guard' => $guardName,
]));
}
}
@@ -0,0 +1,28 @@
<?php
namespace Spatie\Permission\Exceptions;
use InvalidArgumentException;
class RoleDoesNotExist extends InvalidArgumentException
{
public static function named(string $roleName, ?string $guardName)
{
return new static(__('There is no role named `:role` for guard `:guard`.', [
'role' => $roleName,
'guard' => $guardName,
]));
}
/**
* @param int|string $roleId
* @return static
*/
public static function withId($roleId, ?string $guardName)
{
return new static(__('There is no role with ID `:id` for guard `:guard`.', [
'id' => $roleId,
'guard' => $guardName,
]));
}
}
@@ -0,0 +1,79 @@
<?php
namespace Spatie\Permission\Exceptions;
use Illuminate\Contracts\Auth\Access\Authorizable;
use Symfony\Component\HttpKernel\Exception\HttpException;
class UnauthorizedException extends HttpException
{
private $requiredRoles = [];
private $requiredPermissions = [];
public static function forRoles(array $roles): self
{
$message = __('User does not have the right roles.');
if (config('permission.display_role_in_exception')) {
$message .= ' '.__('Necessary roles are :roles', ['roles' => implode(', ', $roles)]);
}
$exception = new static(403, $message, null, []);
$exception->requiredRoles = $roles;
return $exception;
}
public static function forPermissions(array $permissions): self
{
$message = __('User does not have the right permissions.');
if (config('permission.display_permission_in_exception')) {
$message .= ' '.__('Necessary permissions are :permissions', ['permissions' => implode(', ', $permissions)]);
}
$exception = new static(403, $message, null, []);
$exception->requiredPermissions = $permissions;
return $exception;
}
public static function forRolesOrPermissions(array $rolesOrPermissions): self
{
$message = __('User does not have any of the necessary access rights.');
if (config('permission.display_permission_in_exception') && config('permission.display_role_in_exception')) {
$message .= ' '.__('Necessary roles or permissions are :values', ['values' => implode(', ', $rolesOrPermissions)]);
}
$exception = new static(403, $message, null, []);
$exception->requiredPermissions = $rolesOrPermissions;
return $exception;
}
public static function missingTraitHasRoles(Authorizable $user): self
{
$class = get_class($user);
return new static(403, __('Authorizable class `:class` must use Spatie\\Permission\\Traits\\HasRoles trait.', [
'class' => $class,
]), null, []);
}
public static function notLoggedIn(): self
{
return new static(403, __('User is not logged in.'), null, []);
}
public function getRequiredRoles(): array
{
return $this->requiredRoles;
}
public function getRequiredPermissions(): array
{
return $this->requiredPermissions;
}
}
@@ -0,0 +1,13 @@
<?php
namespace Spatie\Permission\Exceptions;
use InvalidArgumentException;
class WildcardPermissionInvalidArgument extends InvalidArgumentException
{
public static function create()
{
return new static(__('Wildcard permission must be string, permission id or permission instance'));
}
}
@@ -0,0 +1,13 @@
<?php
namespace Spatie\Permission\Exceptions;
use InvalidArgumentException;
class WildcardPermissionNotImplementsContract extends InvalidArgumentException
{
public static function create()
{
return new static(__('Wildcard permission class must implement Spatie\\Permission\\Contracts\\Wildcard contract'));
}
}
@@ -0,0 +1,15 @@
<?php
namespace Spatie\Permission\Exceptions;
use InvalidArgumentException;
class WildcardPermissionNotProperlyFormatted extends InvalidArgumentException
{
public static function create(string $permission)
{
return new static(__('Wildcard permission `:permission` is not properly formatted.', [
'permission' => $permission,
]));
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
namespace Spatie\Permission;
use Illuminate\Contracts\Auth\Access\Authorizable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
class Guard
{
/**
* Return a collection of guard names suitable for the $model,
* as indicated by the presence of a $guard_name property or a guardName() method on the model.
*
* @param string|Model $model model class object or name
*/
public static function getNames($model): Collection
{
$class = is_object($model) ? get_class($model) : $model;
if (is_object($model)) {
if (\method_exists($model, 'guardName')) {
$guardName = $model->guardName();
} else {
$guardName = $model->getAttributeValue('guard_name');
}
}
if (! isset($guardName)) {
$guardName = (new \ReflectionClass($class))->getDefaultProperties()['guard_name'] ?? null;
}
if ($guardName) {
return collect($guardName);
}
return self::getConfigAuthGuards($class);
}
/**
* Get the model class associated with a given provider.
*/
protected static function getProviderModel(string $provider): ?string
{
// Get the provider configuration
$providerConfig = config("auth.providers.{$provider}");
// Handle LDAP provider or standard Eloquent provider
if (isset($providerConfig['driver']) && $providerConfig['driver'] === 'ldap') {
return $providerConfig['database']['model'] ?? null;
}
return $providerConfig['model'] ?? null;
}
/**
* Get list of relevant guards for the $class model based on config(auth) settings.
*
* Lookup flow:
* - get names of models for guards defined in auth.guards where a provider is set
* - filter for provider models matching the model $class being checked (important for Lumen)
* - keys() gives just the names of the matched guards
* - return collection of guard names
*/
protected static function getConfigAuthGuards(string $class): Collection
{
return collect(config('auth.guards'))
->map(function ($guard) {
if (! isset($guard['provider'])) {
return null;
}
return static::getProviderModel($guard['provider']);
})
->filter(fn ($model) => $class === $model)
->keys();
}
/**
* Get the model associated with a given guard name.
*/
public static function getModelForGuard(string $guard): ?string
{
// Get the provider configuration for the given guard
$provider = config("auth.guards.{$guard}.provider");
if (! $provider) {
return null;
}
return static::getProviderModel($provider);
}
/**
* Lookup a guard name relevant for the $class model and the current user.
*
* @param string|Model $class model class object or name
* @return string guard name
*/
public static function getDefaultName($class): string
{
$default = config('auth.defaults.guard');
$possible_guards = static::getNames($class);
// return current-detected auth.defaults.guard if it matches one of those that have been checked
if ($possible_guards->contains($default)) {
return $default;
}
return $possible_guards->first() ?: $default;
}
/**
* Lookup a passport guard
*/
public static function getPassportClient($guard): ?Authorizable
{
$guards = collect(config('auth.guards'))->where('driver', 'passport');
if (! $guards->count()) {
return null;
}
$authGuard = Auth::guard($guards->keys()[0]);
if (! \method_exists($authGuard, 'client')) {
return null;
}
$client = $authGuard->client();
if (! $guard || ! $client) {
return $client;
}
if (self::getNames($client)->contains($guard)) {
return $client;
}
return null;
}
}
@@ -0,0 +1,81 @@
<?php
namespace Spatie\Permission\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Spatie\Permission\Exceptions\UnauthorizedException;
use Spatie\Permission\Guard;
class PermissionMiddleware
{
public function handle($request, Closure $next, $permission, $guard = null)
{
$authGuard = Auth::guard($guard);
$user = $authGuard->user();
// For machine-to-machine Passport clients
if (! $user && $request->bearerToken() && config('permission.use_passport_client_credentials')) {
$user = Guard::getPassportClient($guard);
}
if (! $user) {
throw UnauthorizedException::notLoggedIn();
}
if (! method_exists($user, 'hasAnyPermission')) {
throw UnauthorizedException::missingTraitHasRoles($user);
}
$permissions = explode('|', self::parsePermissionsToString($permission));
if (! $user->canAny($permissions)) {
throw UnauthorizedException::forPermissions($permissions);
}
return $next($request);
}
/**
* Specify the permission and guard for the middleware.
*
* @param array|string|\BackedEnum $permission
* @param string|null $guard
* @return string
*/
public static function using($permission, $guard = null)
{
// Convert Enum to its value if an Enum is passed
if ($permission instanceof \BackedEnum) {
$permission = $permission->value;
}
$permissionString = self::parsePermissionsToString($permission);
$args = is_null($guard) ? $permissionString : "$permissionString,$guard";
return static::class.':'.$args;
}
/**
* Convert array or string of permissions to string representation.
*
* @return string
*/
protected static function parsePermissionsToString(array|string|\BackedEnum $permission)
{
// Convert Enum to its value if an Enum is passed
if ($permission instanceof \BackedEnum) {
$permission = $permission->value;
}
if (is_array($permission)) {
$permission = array_map(fn ($r) => $r instanceof \BackedEnum ? $r->value : $r, $permission);
return implode('|', $permission);
}
return (string) $permission;
}
}
@@ -0,0 +1,76 @@
<?php
namespace Spatie\Permission\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Spatie\Permission\Exceptions\UnauthorizedException;
use Spatie\Permission\Guard;
class RoleMiddleware
{
public function handle($request, Closure $next, $role, $guard = null)
{
$authGuard = Auth::guard($guard);
$user = $authGuard->user();
// For machine-to-machine Passport clients
if (! $user && $request->bearerToken() && config('permission.use_passport_client_credentials')) {
$user = Guard::getPassportClient($guard);
}
if (! $user) {
throw UnauthorizedException::notLoggedIn();
}
if (! method_exists($user, 'hasAnyRole')) {
throw UnauthorizedException::missingTraitHasRoles($user);
}
$roles = explode('|', self::parseRolesToString($role));
if (! $user->hasAnyRole($roles)) {
throw UnauthorizedException::forRoles($roles);
}
return $next($request);
}
/**
* Specify the role and guard for the middleware.
*
* @param array|string|\BackedEnum $role
* @param string|null $guard
* @return string
*/
public static function using($role, $guard = null)
{
$roleString = self::parseRolesToString($role);
$args = is_null($guard) ? $roleString : "$roleString,$guard";
return static::class.':'.$args;
}
/**
* Convert array or string of roles to string representation.
*
* @return string
*/
protected static function parseRolesToString(array|string|\BackedEnum $role)
{
// Convert Enum to its value if an Enum is passed
if ($role instanceof \BackedEnum) {
$role = $role->value;
}
if (is_array($role)) {
$role = array_map(fn ($r) => $r instanceof \BackedEnum ? $r->value : $r, $role);
return implode('|', $role);
}
return (string) $role;
}
}
@@ -0,0 +1,75 @@
<?php
namespace Spatie\Permission\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Spatie\Permission\Exceptions\UnauthorizedException;
use Spatie\Permission\Guard;
class RoleOrPermissionMiddleware
{
public function handle($request, Closure $next, $roleOrPermission, $guard = null)
{
$authGuard = Auth::guard($guard);
$user = $authGuard->user();
// For machine-to-machine Passport clients
if (! $user && $request->bearerToken() && config('permission.use_passport_client_credentials')) {
$user = Guard::getPassportClient($guard);
}
if (! $user) {
throw UnauthorizedException::notLoggedIn();
}
if (! method_exists($user, 'hasAnyRole') || ! method_exists($user, 'hasAnyPermission')) {
throw UnauthorizedException::missingTraitHasRoles($user);
}
$rolesOrPermissions = explode('|', self::parseRoleOrPermissionToString($roleOrPermission));
if (! $user->canAny($rolesOrPermissions) && ! $user->hasAnyRole($rolesOrPermissions)) {
throw UnauthorizedException::forRolesOrPermissions($rolesOrPermissions);
}
return $next($request);
}
/**
* Specify the role or permission and guard for the middleware.
*
* @param array|string|\BackedEnum $roleOrPermission
* @param string|null $guard
* @return string
*/
public static function using($roleOrPermission, $guard = null)
{
$roleOrPermissionString = self::parseRoleOrPermissionToString($roleOrPermission);
$args = is_null($guard) ? $roleOrPermissionString : "$roleOrPermissionString,$guard";
return static::class.':'.$args;
}
/**
* Convert array or string of roles/permissions to string representation.
*
* @return string
*/
protected static function parseRoleOrPermissionToString(array|string|\BackedEnum $roleOrPermission)
{
// Convert Enum to its value if an Enum is passed
if ($roleOrPermission instanceof \BackedEnum) {
$roleOrPermission = $roleOrPermission->value;
}
if (is_array($roleOrPermission)) {
$roleOrPermission = array_map(fn ($r) => $r instanceof \BackedEnum ? $r->value : $r, $roleOrPermission);
return implode('|', $roleOrPermission);
}
return (string) $roleOrPermission;
}
}
@@ -0,0 +1,158 @@
<?php
namespace Spatie\Permission\Models;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Spatie\Permission\Contracts\Permission as PermissionContract;
use Spatie\Permission\Exceptions\PermissionAlreadyExists;
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
use Spatie\Permission\Guard;
use Spatie\Permission\PermissionRegistrar;
use Spatie\Permission\Traits\HasRoles;
use Spatie\Permission\Traits\RefreshesPermissionCache;
/**
* @property ?\Illuminate\Support\Carbon $created_at
* @property ?\Illuminate\Support\Carbon $updated_at
*/
class Permission extends Model implements PermissionContract
{
use HasRoles;
use RefreshesPermissionCache;
protected $guarded = [];
public function __construct(array $attributes = [])
{
$attributes['guard_name'] ??= Guard::getDefaultName(static::class);
parent::__construct($attributes);
$this->guarded[] = $this->primaryKey;
$this->table = config('permission.table_names.permissions') ?: parent::getTable();
}
/**
* @return PermissionContract|Permission
*
* @throws PermissionAlreadyExists
*/
public static function create(array $attributes = [])
{
$attributes['guard_name'] ??= Guard::getDefaultName(static::class);
$permission = static::getPermission(['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']]);
if ($permission) {
throw PermissionAlreadyExists::create($attributes['name'], $attributes['guard_name']);
}
return static::query()->create($attributes);
}
/**
* A permission can be applied to roles.
*/
public function roles(): BelongsToMany
{
$registrar = app(PermissionRegistrar::class);
return $this->belongsToMany(
config('permission.models.role'),
config('permission.table_names.role_has_permissions'),
$registrar->pivotPermission,
$registrar->pivotRole
);
}
/**
* A permission belongs to some users of the model associated with its guard.
*/
public function users(): BelongsToMany
{
return $this->morphedByMany(
getModelForGuard($this->attributes['guard_name'] ?? config('auth.defaults.guard')),
'model',
config('permission.table_names.model_has_permissions'),
app(PermissionRegistrar::class)->pivotPermission,
config('permission.column_names.model_morph_key')
);
}
/**
* Find a permission by its name (and optionally guardName).
*
* @return PermissionContract|Permission
*
* @throws PermissionDoesNotExist
*/
public static function findByName(string $name, ?string $guardName = null): PermissionContract
{
$guardName ??= Guard::getDefaultName(static::class);
$permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]);
if (! $permission) {
throw PermissionDoesNotExist::create($name, $guardName);
}
return $permission;
}
/**
* Find a permission by its id (and optionally guardName).
*
* @return PermissionContract|Permission
*
* @throws PermissionDoesNotExist
*/
public static function findById(int|string $id, ?string $guardName = null): PermissionContract
{
$guardName ??= Guard::getDefaultName(static::class);
$permission = static::getPermission([(new static)->getKeyName() => $id, 'guard_name' => $guardName]);
if (! $permission) {
throw PermissionDoesNotExist::withId($id, $guardName);
}
return $permission;
}
/**
* Find or create permission by its name (and optionally guardName).
*
* @return PermissionContract|Permission
*/
public static function findOrCreate(string $name, ?string $guardName = null): PermissionContract
{
$guardName ??= Guard::getDefaultName(static::class);
$permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]);
if (! $permission) {
return static::query()->create(['name' => $name, 'guard_name' => $guardName]);
}
return $permission;
}
/**
* Get the current cached permissions.
*/
protected static function getPermissions(array $params = [], bool $onlyOne = false): Collection
{
return app(PermissionRegistrar::class)
->setPermissionClass(static::class)
->getPermissions($params, $onlyOne);
}
/**
* Get the current cached first permission.
*
* @return PermissionContract|Permission|null
*/
protected static function getPermission(array $params = []): ?PermissionContract
{
/** @var PermissionContract|null */
return static::getPermissions($params, true)->first();
}
}
+210
View File
@@ -0,0 +1,210 @@
<?php
namespace Spatie\Permission\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Spatie\Permission\Contracts\Role as RoleContract;
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
use Spatie\Permission\Exceptions\RoleAlreadyExists;
use Spatie\Permission\Exceptions\RoleDoesNotExist;
use Spatie\Permission\Guard;
use Spatie\Permission\PermissionRegistrar;
use Spatie\Permission\Traits\HasPermissions;
use Spatie\Permission\Traits\RefreshesPermissionCache;
/**
* @property ?\Illuminate\Support\Carbon $created_at
* @property ?\Illuminate\Support\Carbon $updated_at
*/
class Role extends Model implements RoleContract
{
use HasPermissions;
use RefreshesPermissionCache;
protected $guarded = [];
public function __construct(array $attributes = [])
{
$attributes['guard_name'] ??= Guard::getDefaultName(static::class);
parent::__construct($attributes);
$this->guarded[] = $this->primaryKey;
$this->table = config('permission.table_names.roles') ?: parent::getTable();
}
/**
* @return RoleContract|Role
*
* @throws RoleAlreadyExists
*/
public static function create(array $attributes = [])
{
$attributes['guard_name'] ??= Guard::getDefaultName(static::class);
$params = ['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']];
$registrar = app(PermissionRegistrar::class);
if ($registrar->teams) {
$teamsKey = $registrar->teamsKey;
if (array_key_exists($teamsKey, $attributes)) {
$params[$teamsKey] = $attributes[$teamsKey];
} else {
$attributes[$teamsKey] = getPermissionsTeamId();
}
}
if (static::findByParam($params)) {
throw RoleAlreadyExists::create($attributes['name'], $attributes['guard_name']);
}
return static::query()->create($attributes);
}
/**
* A role may be given various permissions.
*/
public function permissions(): BelongsToMany
{
$registrar = app(PermissionRegistrar::class);
return $this->belongsToMany(
config('permission.models.permission'),
config('permission.table_names.role_has_permissions'),
$registrar->pivotRole,
$registrar->pivotPermission
);
}
/**
* A role belongs to some users of the model associated with its guard.
*/
public function users(): BelongsToMany
{
return $this->morphedByMany(
getModelForGuard($this->attributes['guard_name'] ?? config('auth.defaults.guard')),
'model',
config('permission.table_names.model_has_roles'),
app(PermissionRegistrar::class)->pivotRole,
config('permission.column_names.model_morph_key')
);
}
/**
* Find a role by its name and guard name.
*
* @return RoleContract|Role
*
* @throws RoleDoesNotExist
*/
public static function findByName(string $name, ?string $guardName = null): RoleContract
{
$guardName ??= Guard::getDefaultName(static::class);
$role = static::findByParam(['name' => $name, 'guard_name' => $guardName]);
if (! $role) {
throw RoleDoesNotExist::named($name, $guardName);
}
return $role;
}
/**
* Find a role by its id (and optionally guardName).
*
* @return RoleContract|Role
*/
public static function findById(int|string $id, ?string $guardName = null): RoleContract
{
$guardName ??= Guard::getDefaultName(static::class);
$role = static::findByParam([(new static)->getKeyName() => $id, 'guard_name' => $guardName]);
if (! $role) {
throw RoleDoesNotExist::withId($id, $guardName);
}
return $role;
}
/**
* Find or create role by its name (and optionally guardName).
*
* @return RoleContract|Role
*/
public static function findOrCreate(string $name, ?string $guardName = null): RoleContract
{
$guardName ??= Guard::getDefaultName(static::class);
$attributes = ['name' => $name, 'guard_name' => $guardName];
$role = static::findByParam($attributes);
if (! $role) {
$registrar = app(PermissionRegistrar::class);
if ($registrar->teams) {
$teamsKey = $registrar->teamsKey;
$attributes[$teamsKey] = getPermissionsTeamId();
}
return static::query()->create($attributes);
}
return $role;
}
/**
* Finds a role based on an array of parameters.
*
* @return RoleContract|Role|null
*/
protected static function findByParam(array $params = []): ?RoleContract
{
$query = static::query();
$registrar = app(PermissionRegistrar::class);
if ($registrar->teams) {
$teamsKey = $registrar->teamsKey;
$query->where(fn ($q) => $q->whereNull($teamsKey)
->orWhere($teamsKey, $params[$teamsKey] ?? getPermissionsTeamId())
);
unset($params[$teamsKey]);
}
foreach ($params as $key => $value) {
$query->where($key, $value);
}
return $query->first();
}
/**
* Determine if the role may perform the given permission.
*
* @param string|int|\Spatie\Permission\Contracts\Permission|\BackedEnum $permission
*
* @throws PermissionDoesNotExist|GuardDoesNotMatch
*/
public function hasPermissionTo($permission, ?string $guardName = null): bool
{
if ($this->getWildcardClass()) {
return $this->hasWildcardPermission($permission, $guardName);
}
$permission = $this->filterPermission($permission, $guardName);
if (! $this->getGuardNames()->contains($permission->guard_name)) {
throw GuardDoesNotMatch::create($permission->guard_name, $guardName ? collect([$guardName]) : $this->getGuardNames());
}
return $this->loadMissing('permissions')->permissions
->contains($permission->getKeyName(), $permission->getKey());
}
}
@@ -0,0 +1,429 @@
<?php
namespace Spatie\Permission;
use Illuminate\Cache\CacheManager;
use Illuminate\Contracts\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\Contracts\Permission;
use Spatie\Permission\Contracts\PermissionsTeamResolver;
use Spatie\Permission\Contracts\Role;
class PermissionRegistrar
{
protected Repository $cache;
protected CacheManager $cacheManager;
protected string $permissionClass;
protected string $roleClass;
/** @var Collection|array|null */
protected $permissions;
public string $pivotRole;
public string $pivotPermission;
/** @var \DateInterval|int */
public $cacheExpirationTime;
public bool $teams;
protected PermissionsTeamResolver $teamResolver;
public string $teamsKey;
public string $cacheKey;
private array $cachedRoles = [];
private array $alias = [];
private array $except = [];
private array $wildcardPermissionsIndex = [];
private bool $isLoadingPermissions = false;
/**
* PermissionRegistrar constructor.
*/
public function __construct(CacheManager $cacheManager)
{
$this->permissionClass = config('permission.models.permission');
$this->roleClass = config('permission.models.role');
$this->teamResolver = new (config('permission.team_resolver', DefaultTeamResolver::class));
$this->cacheManager = $cacheManager;
$this->initializeCache();
}
public function initializeCache(): void
{
$this->cacheExpirationTime = config('permission.cache.expiration_time') ?: \DateInterval::createFromDateString('24 hours');
$this->teams = config('permission.teams', false);
$this->teamsKey = config('permission.column_names.team_foreign_key', 'team_id');
$this->cacheKey = config('permission.cache.key');
$this->pivotRole = config('permission.column_names.role_pivot_key') ?: 'role_id';
$this->pivotPermission = config('permission.column_names.permission_pivot_key') ?: 'permission_id';
$this->cache = $this->getCacheStoreFromConfig();
}
protected function getCacheStoreFromConfig(): Repository
{
// the 'default' fallback here is from the permission.php config file,
// where 'default' means to use config(cache.default)
$cacheDriver = config('permission.cache.store', 'default');
// when 'default' is specified, no action is required since we already have the default instance
if ($cacheDriver === 'default') {
return $this->cacheManager->store();
}
// if an undefined cache store is specified, fallback to 'array' which is Laravel's closest equiv to 'none'
if (! \array_key_exists($cacheDriver, config('cache.stores'))) {
$cacheDriver = 'array';
}
return $this->cacheManager->store($cacheDriver);
}
/**
* Set the team id for teams/groups support, this id is used when querying permissions/roles
*
* @param int|string|\Illuminate\Database\Eloquent\Model|null $id
*/
public function setPermissionsTeamId($id): void
{
$this->teamResolver->setPermissionsTeamId($id);
}
/**
* @return int|string|null
*/
public function getPermissionsTeamId()
{
return $this->teamResolver->getPermissionsTeamId();
}
/**
* Register the permission check method on the gate.
* We resolve the Gate fresh here, for benefit of long-running instances.
*/
public function registerPermissions(Gate $gate): bool
{
$gate->before(function (Authorizable $user, string $ability, array &$args = []) {
if (is_string($args[0] ?? null) && ! class_exists($args[0])) {
$guard = array_shift($args);
}
if (method_exists($user, 'checkPermissionTo')) {
return $user->checkPermissionTo($ability, $guard ?? null) ?: null;
}
});
return true;
}
/**
* Flush the cache.
*/
public function forgetCachedPermissions()
{
$this->permissions = null;
$this->forgetWildcardPermissionIndex();
return $this->cache->forget($this->cacheKey);
}
public function forgetWildcardPermissionIndex(?Model $record = null): void
{
if ($record) {
unset($this->wildcardPermissionsIndex[get_class($record)][$record->getKey()]);
return;
}
$this->wildcardPermissionsIndex = [];
}
public function getWildcardPermissionIndex(Model $record): array
{
if (isset($this->wildcardPermissionsIndex[get_class($record)][$record->getKey()])) {
return $this->wildcardPermissionsIndex[get_class($record)][$record->getKey()];
}
return $this->wildcardPermissionsIndex[get_class($record)][$record->getKey()] = app($record->getWildcardClass(), ['record' => $record])->getIndex();
}
/**
* Clear already-loaded permissions collection.
* This is only intended to be called by the PermissionServiceProvider on boot,
* so that long-running instances like Octane or Swoole don't keep old data in memory.
*/
public function clearPermissionsCollection(): void
{
$this->permissions = null;
$this->wildcardPermissionsIndex = [];
$this->isLoadingPermissions = false;
}
/**
* @deprecated
*
* @alias of clearPermissionsCollection()
*/
public function clearClassPermissions()
{
$this->clearPermissionsCollection();
}
/**
* Load permissions from cache
* And turns permissions array into a \Illuminate\Database\Eloquent\Collection
*
* Thread-safe implementation to prevent race conditions in concurrent environments
* (e.g., Laravel Octane, Swoole, parallel requests)
*/
private function loadPermissions(int $retries = 0): void
{
// First check (without lock) - fast path for already loaded permissions
if ($this->permissions) {
return;
}
// Prevent concurrent loading using a flag-based lock
// This protects against cache stampede and duplicate database queries
if ($this->isLoadingPermissions && $retries < 10) {
// Another thread is loading, wait and retry
usleep(10000); // Wait 10ms
$retries++;
// After wait, recursively check again if permissions were loaded
$this->loadPermissions($retries);
return;
}
// Set loading flag to prevent concurrent loads
$this->isLoadingPermissions = true;
try {
$this->permissions = $this->cache->remember(
$this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache()
);
$this->alias = $this->permissions['alias'];
$this->hydrateRolesCache();
$this->permissions = $this->getHydratedPermissionCollection();
$this->cachedRoles = $this->alias = $this->except = [];
} finally {
// Always release the loading flag, even if an exception occurs
$this->isLoadingPermissions = false;
}
}
/**
* Get the permissions based on the passed params.
*/
public function getPermissions(array $params = [], bool $onlyOne = false): Collection
{
$this->loadPermissions();
$method = $onlyOne ? 'first' : 'filter';
$permissions = $this->permissions->$method(static function ($permission) use ($params) {
foreach ($params as $attr => $value) {
if ($permission->getAttribute($attr) != $value) {
return false;
}
}
return true;
});
if ($onlyOne) {
$permissions = new Collection($permissions ? [$permissions] : []);
}
return $permissions;
}
public function getPermissionClass(): string
{
return $this->permissionClass;
}
public function setPermissionClass($permissionClass)
{
$this->permissionClass = $permissionClass;
config()->set('permission.models.permission', $permissionClass);
app()->bind(Permission::class, $permissionClass);
return $this;
}
public function getRoleClass(): string
{
return $this->roleClass;
}
public function setRoleClass($roleClass)
{
$this->roleClass = $roleClass;
config()->set('permission.models.role', $roleClass);
app()->bind(Role::class, $roleClass);
return $this;
}
public function getCacheRepository(): Repository
{
return $this->cache;
}
public function getCacheStore(): Store
{
return $this->cache->getStore();
}
protected function getPermissionsWithRoles(): Collection
{
return $this->permissionClass::select()->with('roles')->get();
}
/**
* Changes array keys with alias
*/
private function aliasedArray($model): array
{
return collect(is_array($model) ? $model : $model->getAttributes())->except($this->except)
->keyBy(fn ($value, $key) => $this->alias[$key] ?? $key)
->all();
}
/**
* Array for cache alias
*/
private function aliasModelFields($newKeys = []): void
{
$i = 0;
$alphas = ! count($this->alias) ? range('a', 'h') : range('j', 'p');
foreach (array_keys($newKeys->getAttributes()) as $value) {
if (! isset($this->alias[$value])) {
$this->alias[$value] = $alphas[$i++] ?? $value;
}
}
$this->alias = array_diff_key($this->alias, array_flip($this->except));
}
/*
* Make the cache smaller using an array with only required fields
*/
private function getSerializedPermissionsForCache(): array
{
$this->except = config('permission.cache.column_names_except', ['created_at', 'updated_at', 'deleted_at']);
$permissions = $this->getPermissionsWithRoles()
->map(function ($permission) {
if (! $this->alias) {
$this->aliasModelFields($permission);
}
return $this->aliasedArray($permission) + $this->getSerializedRoleRelation($permission);
})->all();
$roles = array_values($this->cachedRoles);
$this->cachedRoles = [];
return ['alias' => array_flip($this->alias)] + compact('permissions', 'roles');
}
private function getSerializedRoleRelation($permission): array
{
if (! $permission->roles->count()) {
return [];
}
if (! isset($this->alias['roles'])) {
$this->alias['roles'] = 'r';
$this->aliasModelFields($permission->roles[0]);
}
return [
'r' => $permission->roles->map(function ($role) {
if (! isset($this->cachedRoles[$role->getKey()])) {
$this->cachedRoles[$role->getKey()] = $this->aliasedArray($role);
}
return $role->getKey();
})->all(),
];
}
private function getHydratedPermissionCollection(): Collection
{
$permissionInstance = (new ($this->getPermissionClass())())->newInstance([], true);
return Collection::make(array_map(
fn ($item) => (clone $permissionInstance)
->setRawAttributes($this->aliasedArray(array_diff_key($item, ['r' => 0])), true)
->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])),
$this->permissions['permissions']
));
}
private function getHydratedRoleCollection(array $roles): Collection
{
return Collection::make(array_values(
array_intersect_key($this->cachedRoles, array_flip($roles))
));
}
private function hydrateRolesCache(): void
{
$roleInstance = (new ($this->getRoleClass())())->newInstance([], true);
array_map(function ($item) use ($roleInstance) {
$role = (clone $roleInstance)
->setRawAttributes($this->aliasedArray($item), true);
$this->cachedRoles[$role->getKey()] = $role;
}, $this->permissions['roles']);
$this->permissions['roles'] = [];
}
public static function isUid($value): bool
{
if (! is_string($value) || empty(trim($value))) {
return false;
}
// check if is UUID/GUID
$uid = preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0;
if ($uid) {
return true;
}
// check if is ULID
$ulid = strlen($value) == 26 && strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz') == 26 && $value[0] <= '7';
if ($ulid) {
return true;
}
return false;
}
}
@@ -0,0 +1,217 @@
<?php
namespace Spatie\Permission;
use Composer\InstalledVersions;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Routing\Route;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
use Illuminate\View\Compilers\BladeCompiler;
use Spatie\Permission\Contracts\Permission as PermissionContract;
use Spatie\Permission\Contracts\Role as RoleContract;
class PermissionServiceProvider extends ServiceProvider
{
public function boot()
{
$this->offerPublishing();
$this->registerMacroHelpers();
$this->registerCommands();
$this->registerModelBindings();
$this->registerOctaneListener();
$this->callAfterResolving(Gate::class, function (Gate $gate, Application $app) {
if ($this->app['config']->get('permission.register_permission_check_method')) {
/** @var PermissionRegistrar $permissionLoader */
$permissionLoader = $app->get(PermissionRegistrar::class);
$permissionLoader->clearPermissionsCollection();
$permissionLoader->registerPermissions($gate);
}
});
$this->app->singleton(PermissionRegistrar::class);
$this->registerAbout();
}
public function register()
{
$this->mergeConfigFrom(
__DIR__.'/../config/permission.php',
'permission'
);
$this->callAfterResolving('blade.compiler', fn (BladeCompiler $bladeCompiler) => $this->registerBladeExtensions($bladeCompiler));
}
protected function offerPublishing(): void
{
if (! $this->app->runningInConsole()) {
return;
}
if (! function_exists('config_path')) {
// function not available and 'publish' not relevant in Lumen
return;
}
$this->publishes([
__DIR__.'/../config/permission.php' => config_path('permission.php'),
], 'permission-config');
$this->publishes([
__DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName('create_permission_tables.php'),
], 'permission-migrations');
}
protected function registerCommands(): void
{
$this->commands([
Commands\CacheReset::class,
]);
if (! $this->app->runningInConsole()) {
return;
}
$this->commands([
Commands\CreateRole::class,
Commands\CreatePermission::class,
Commands\Show::class,
Commands\UpgradeForTeams::class,
Commands\AssignRole::class,
]);
}
protected function registerOctaneListener(): void
{
if ($this->app->runningInConsole() || ! $this->app['config']->get('octane.listeners')) {
return;
}
$dispatcher = $this->app[Dispatcher::class];
// @phpstan-ignore-next-line
$dispatcher->listen(function (\Laravel\Octane\Contracts\OperationTerminated $event) {
// @phpstan-ignore-next-line
$event->sandbox->make(PermissionRegistrar::class)->setPermissionsTeamId(null);
});
if (! $this->app['config']->get('permission.register_octane_reset_listener')) {
return;
}
// @phpstan-ignore-next-line
$dispatcher->listen(function (\Laravel\Octane\Contracts\OperationTerminated $event) {
// @phpstan-ignore-next-line
$event->sandbox->make(PermissionRegistrar::class)->clearPermissionsCollection();
});
}
protected function registerModelBindings(): void
{
$this->app->bind(PermissionContract::class, fn ($app) => $app->make($app->config['permission.models.permission']));
$this->app->bind(RoleContract::class, fn ($app) => $app->make($app->config['permission.models.role']));
}
public static function bladeMethodWrapper($method, $role, $guard = null): bool
{
return auth($guard)->check() && auth($guard)->user()->{$method}($role);
}
protected function registerBladeExtensions(BladeCompiler $bladeCompiler): void
{
$bladeMethodWrapper = '\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper';
// permission checks
$bladeCompiler->if('haspermission', fn () => $bladeMethodWrapper('checkPermissionTo', ...func_get_args()));
// role checks
$bladeCompiler->if('role', fn () => $bladeMethodWrapper('hasRole', ...func_get_args()));
$bladeCompiler->if('hasrole', fn () => $bladeMethodWrapper('hasRole', ...func_get_args()));
$bladeCompiler->if('hasanyrole', fn () => $bladeMethodWrapper('hasAnyRole', ...func_get_args()));
$bladeCompiler->if('hasallroles', fn () => $bladeMethodWrapper('hasAllRoles', ...func_get_args()));
$bladeCompiler->if('hasexactroles', fn () => $bladeMethodWrapper('hasExactRoles', ...func_get_args()));
$bladeCompiler->directive('endunlessrole', fn () => '<?php endif; ?>');
}
protected function registerMacroHelpers(): void
{
if (! method_exists(Route::class, 'macro')) { // @phpstan-ignore-line Lumen
return;
}
Route::macro('role', function ($roles = []) {
$roles = Arr::wrap($roles);
$roles = array_map(fn ($role) => $role instanceof \BackedEnum ? $role->value : $role, $roles);
/** @var Route $this */
return $this->middleware('role:'.implode('|', $roles));
});
Route::macro('permission', function ($permissions = []) {
$permissions = Arr::wrap($permissions);
$permissions = array_map(fn ($permission) => $permission instanceof \BackedEnum ? $permission->value : $permission, $permissions);
/** @var Route $this */
return $this->middleware('permission:'.implode('|', $permissions));
});
Route::macro('roleOrPermission', function ($rolesOrPermissions = []) {
$rolesOrPermissions = Arr::wrap($rolesOrPermissions);
$rolesOrPermissions = array_map(fn ($item) => $item instanceof \BackedEnum ? $item->value : $item, $rolesOrPermissions);
/** @var Route $this */
return $this->middleware('role_or_permission:'.implode('|', $rolesOrPermissions));
});
}
/**
* Returns existing migration file if found, else uses the current timestamp.
*/
protected function getMigrationFileName(string $migrationFileName): string
{
$timestamp = date('Y_m_d_His');
$filesystem = $this->app->make(Filesystem::class);
return Collection::make([$this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR])
->flatMap(fn ($path) => $filesystem->glob($path.'*_'.$migrationFileName))
->push($this->app->databasePath()."/migrations/{$timestamp}_{$migrationFileName}")
->first();
}
protected function registerAbout(): void
{
if (! class_exists(InstalledVersions::class) || ! class_exists(AboutCommand::class)) {
return;
}
// array format: 'Display Text' => 'boolean-config-key name'
$features = [
'Teams' => 'teams',
'Wildcard-Permissions' => 'enable_wildcard_permission',
'Octane-Listener' => 'register_octane_reset_listener',
'Passport' => 'use_passport_client_credentials',
];
$config = $this->app['config'];
AboutCommand::add('Spatie Permissions', static fn () => [
'Features Enabled' => collect($features)
->filter(fn (string $feature, string $name): bool => $config->get("permission.{$feature}"))
->keys()
->whenEmpty(fn (Collection $collection) => $collection->push('Default'))
->join(', '),
'Version' => InstalledVersions::getPrettyVersion('spatie/laravel-permission'),
]);
}
}
@@ -0,0 +1,593 @@
<?php
namespace Spatie\Permission\Traits;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Permission;
use Spatie\Permission\Contracts\Role;
use Spatie\Permission\Contracts\Wildcard;
use Spatie\Permission\Events\PermissionAttached;
use Spatie\Permission\Events\PermissionDetached;
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument;
use Spatie\Permission\Exceptions\WildcardPermissionNotImplementsContract;
use Spatie\Permission\Guard;
use Spatie\Permission\PermissionRegistrar;
use Spatie\Permission\WildcardPermission;
trait HasPermissions
{
private ?string $permissionClass = null;
private ?string $wildcardClass = null;
private array $wildcardPermissionsIndex;
public static function bootHasPermissions()
{
static::deleting(function ($model) {
if (method_exists($model, 'isForceDeleting') && ! $model->isForceDeleting()) {
return;
}
$teams = app(PermissionRegistrar::class)->teams;
app(PermissionRegistrar::class)->teams = false;
if (! is_a($model, Permission::class)) {
$model->permissions()->detach();
}
if (is_a($model, Role::class)) {
$model->users()->detach();
}
app(PermissionRegistrar::class)->teams = $teams;
});
}
public function getPermissionClass(): string
{
if (! $this->permissionClass) {
$this->permissionClass = app(PermissionRegistrar::class)->getPermissionClass();
}
return $this->permissionClass;
}
public function getWildcardClass()
{
if (! is_null($this->wildcardClass)) {
return $this->wildcardClass;
}
$this->wildcardClass = '';
if (config('permission.enable_wildcard_permission')) {
$this->wildcardClass = config('permission.wildcard_permission', WildcardPermission::class);
if (! is_subclass_of($this->wildcardClass, Wildcard::class)) {
throw WildcardPermissionNotImplementsContract::create();
}
}
return $this->wildcardClass;
}
/**
* A model may have multiple direct permissions.
*/
public function permissions(): BelongsToMany
{
$relation = $this->morphToMany(
config('permission.models.permission'),
'model',
config('permission.table_names.model_has_permissions'),
config('permission.column_names.model_morph_key'),
app(PermissionRegistrar::class)->pivotPermission
);
if (! app(PermissionRegistrar::class)->teams) {
return $relation;
}
$teamsKey = app(PermissionRegistrar::class)->teamsKey;
$relation->withPivot($teamsKey);
return $relation->wherePivot($teamsKey, getPermissionsTeamId());
}
/**
* Scope the model query to certain permissions only.
*
* @param string|int|array|Permission|Collection|\BackedEnum $permissions
* @param bool $without
*/
public function scopePermission(Builder $query, $permissions, $without = false): Builder
{
$permissions = $this->convertToPermissionModels($permissions);
$permissionKey = (new ($this->getPermissionClass())())->getKeyName();
$roleKey = (new (is_a($this, Role::class) ? static::class : $this->getRoleClass())())->getKeyName();
$rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique(
array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), [])
);
return $query->where(fn (Builder $query) => $query
->{! $without ? 'whereHas' : 'whereDoesntHave'}('permissions', fn (Builder $subQuery) => $subQuery
->whereIn(config('permission.table_names.permissions').".$permissionKey", \array_column($permissions, $permissionKey))
)
->when(count($rolesWithPermissions), fn ($whenQuery) => $whenQuery
->{! $without ? 'orWhereHas' : 'whereDoesntHave'}('roles', fn (Builder $subQuery) => $subQuery
->whereIn(config('permission.table_names.roles').".$roleKey", \array_column($rolesWithPermissions, $roleKey))
)
)
);
}
/**
* Scope the model query to only those without certain permissions,
* whether indirectly by role or by direct permission.
*
* @param string|int|array|Permission|Collection|\BackedEnum $permissions
*/
public function scopeWithoutPermission(Builder $query, $permissions): Builder
{
return $this->scopePermission($query, $permissions, true);
}
/**
* @param string|int|array|Permission|Collection|\BackedEnum $permissions
*
* @throws PermissionDoesNotExist
*/
protected function convertToPermissionModels($permissions): array
{
if ($permissions instanceof Collection) {
$permissions = $permissions->all();
}
return array_map(function ($permission) {
if ($permission instanceof Permission) {
return $permission;
}
if ($permission instanceof \BackedEnum) {
$permission = $permission->value;
}
$method = is_int($permission) || PermissionRegistrar::isUid($permission) ? 'findById' : 'findByName';
return $this->getPermissionClass()::{$method}($permission, $this->getDefaultGuardName());
}, Arr::wrap($permissions));
}
/**
* Find a permission.
*
* @param string|int|Permission|\BackedEnum $permission
* @return Permission
*
* @throws PermissionDoesNotExist
*/
public function filterPermission($permission, $guardName = null)
{
if ($permission instanceof \BackedEnum) {
$permission = $permission->value;
}
if (is_int($permission) || PermissionRegistrar::isUid($permission)) {
$permission = $this->getPermissionClass()::findById(
$permission,
$guardName ?? $this->getDefaultGuardName()
);
}
if (is_string($permission)) {
$permission = $this->getPermissionClass()::findByName(
$permission,
$guardName ?? $this->getDefaultGuardName()
);
}
if (! $permission instanceof Permission) {
throw new PermissionDoesNotExist;
}
return $permission;
}
/**
* Determine if the model may perform the given permission.
*
* @param string|int|Permission|\BackedEnum $permission
* @param string|null $guardName
*
* @throws PermissionDoesNotExist
*/
public function hasPermissionTo($permission, $guardName = null): bool
{
if ($this->getWildcardClass()) {
return $this->hasWildcardPermission($permission, $guardName);
}
$permission = $this->filterPermission($permission, $guardName);
return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission);
}
/**
* Validates a wildcard permission against all permissions of a user.
*
* @param string|int|Permission|\BackedEnum $permission
* @param string|null $guardName
*/
protected function hasWildcardPermission($permission, $guardName = null): bool
{
$guardName = $guardName ?? $this->getDefaultGuardName();
if ($permission instanceof \BackedEnum) {
$permission = $permission->value;
}
if (is_int($permission) || PermissionRegistrar::isUid($permission)) {
$permission = $this->getPermissionClass()::findById($permission, $guardName);
}
if ($permission instanceof Permission) {
$guardName = $permission->guard_name ?? $guardName;
$permission = $permission->name;
}
if (! is_string($permission)) {
throw WildcardPermissionInvalidArgument::create();
}
return app($this->getWildcardClass(), ['record' => $this])->implies(
$permission,
$guardName,
app(PermissionRegistrar::class)->getWildcardPermissionIndex($this),
);
}
/**
* An alias to hasPermissionTo(), but avoids throwing an exception.
*
* @param string|int|Permission|\BackedEnum $permission
* @param string|null $guardName
*/
public function checkPermissionTo($permission, $guardName = null): bool
{
try {
return $this->hasPermissionTo($permission, $guardName);
} catch (PermissionDoesNotExist $e) {
return false;
}
}
/**
* Determine if the model has any of the given permissions.
*
* @param string|int|array|Permission|Collection|\BackedEnum ...$permissions
*/
public function hasAnyPermission(...$permissions): bool
{
$permissions = collect($permissions)->flatten();
foreach ($permissions as $permission) {
if ($this->checkPermissionTo($permission)) {
return true;
}
}
return false;
}
/**
* Determine if the model has all of the given permissions.
*
* @param string|int|array|Permission|Collection|\BackedEnum ...$permissions
*/
public function hasAllPermissions(...$permissions): bool
{
$permissions = collect($permissions)->flatten();
foreach ($permissions as $permission) {
if (! $this->checkPermissionTo($permission)) {
return false;
}
}
return true;
}
/**
* Determine if the model has, via roles, the given permission.
*/
protected function hasPermissionViaRole(Permission $permission): bool
{
if (is_a($this, Role::class)) {
return false;
}
return $this->hasRole($permission->roles);
}
/**
* Determine if the model has the given permission.
*
* @param string|int|Permission|\BackedEnum $permission
*
* @throws PermissionDoesNotExist
*/
public function hasDirectPermission($permission): bool
{
$permission = $this->filterPermission($permission);
return $this->loadMissing('permissions')->permissions
->contains($permission->getKeyName(), $permission->getKey());
}
/**
* Return all the permissions the model has via roles.
*/
public function getPermissionsViaRoles(): Collection
{
if (is_a($this, Role::class) || is_a($this, Permission::class)) {
return collect();
}
return $this->loadMissing('roles', 'roles.permissions')
->roles->flatMap(fn ($role) => $role->permissions)
->sort()->values();
}
/**
* Return all the permissions the model has, both directly and via roles.
*/
public function getAllPermissions(): Collection
{
/** @var Collection $permissions */
$permissions = $this->permissions;
if (! is_a($this, Permission::class)) {
$permissions = $permissions->merge($this->getPermissionsViaRoles());
}
return $permissions->sort()->values();
}
/**
* Returns array of permissions ids
*
* @param string|int|array|Permission|Collection|\BackedEnum $permissions
*/
private function collectPermissions(...$permissions): array
{
return collect($permissions)
->flatten()
->reduce(function ($array, $permission) {
if (empty($permission)) {
return $array;
}
$permission = $this->getStoredPermission($permission);
if (! $permission instanceof Permission) {
return $array;
}
if (! in_array($permission->getKey(), $array)) {
$this->ensureModelSharesGuard($permission);
$array[] = $permission->getKey();
}
return $array;
}, []);
}
/**
* Grant the given permission(s) to a role.
*
* @param string|int|array|Permission|Collection|\BackedEnum $permissions
* @return $this
*/
public function givePermissionTo(...$permissions)
{
$permissions = $this->collectPermissions($permissions);
$model = $this->getModel();
$teamPivot = app(PermissionRegistrar::class)->teams && ! is_a($this, Role::class) ?
[app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : [];
if ($model->exists) {
$currentPermissions = $this->permissions->map(fn ($permission) => $permission->getKey())->toArray();
$this->permissions()->attach(array_diff($permissions, $currentPermissions), $teamPivot);
$model->unsetRelation('permissions');
} else {
$class = \get_class($model);
$saved = false;
$class::saved(
function ($object) use ($permissions, $model, $teamPivot, &$saved) {
if ($saved || $model->getKey() != $object->getKey()) {
return;
}
$model->permissions()->attach($permissions, $teamPivot);
$model->unsetRelation('permissions');
$saved = true;
}
);
}
if (is_a($this, Role::class)) {
$this->forgetCachedPermissions();
}
if (config('permission.events_enabled')) {
event(new PermissionAttached($this->getModel(), $permissions));
}
$this->forgetWildcardPermissionIndex();
return $this;
}
public function forgetWildcardPermissionIndex(): void
{
app(PermissionRegistrar::class)->forgetWildcardPermissionIndex(
is_a($this, Role::class) ? null : $this,
);
}
/**
* Remove all current permissions and set the given ones.
*
* @param string|int|array|Permission|Collection|\BackedEnum $permissions
* @return $this
*/
public function syncPermissions(...$permissions)
{
if ($this->getModel()->exists) {
$this->collectPermissions($permissions);
$this->permissions()->detach();
$this->setRelation('permissions', collect());
}
return $this->givePermissionTo($permissions);
}
/**
* Revoke the given permission(s).
*
* @param Permission|Permission[]|string|string[]|\BackedEnum $permission
* @return $this
*/
public function revokePermissionTo($permission)
{
$storedPermission = $this->getStoredPermission($permission);
$this->permissions()->detach($storedPermission);
if (is_a($this, Role::class)) {
$this->forgetCachedPermissions();
}
if (config('permission.events_enabled')) {
event(new PermissionDetached($this->getModel(), $storedPermission));
}
$this->forgetWildcardPermissionIndex();
$this->unsetRelation('permissions');
return $this;
}
public function getPermissionNames(): Collection
{
return $this->permissions->pluck('name');
}
/**
* @param string|int|array|Permission|Collection|\BackedEnum $permissions
* @return Permission|Permission[]|Collection
*/
protected function getStoredPermission($permissions)
{
if ($permissions instanceof \BackedEnum) {
$permissions = $permissions->value;
}
if (is_int($permissions) || PermissionRegistrar::isUid($permissions)) {
return $this->getPermissionClass()::findById($permissions, $this->getDefaultGuardName());
}
if (is_string($permissions)) {
return $this->getPermissionClass()::findByName($permissions, $this->getDefaultGuardName());
}
if (is_array($permissions)) {
$permissions = array_map(function ($permission) {
if ($permission instanceof \BackedEnum) {
return $permission->value;
}
return is_a($permission, Permission::class) ? $permission->name : $permission;
}, $permissions);
return $this->getPermissionClass()::whereIn('name', $permissions)
->whereIn('guard_name', $this->getGuardNames())
->get();
}
return $permissions;
}
/**
* @param Permission|Role $roleOrPermission
*
* @throws GuardDoesNotMatch
*/
protected function ensureModelSharesGuard($roleOrPermission)
{
if (! $this->getGuardNames()->contains($roleOrPermission->guard_name)) {
throw GuardDoesNotMatch::create($roleOrPermission->guard_name, $this->getGuardNames());
}
}
protected function getGuardNames(): Collection
{
return Guard::getNames($this);
}
protected function getDefaultGuardName(): string
{
return Guard::getDefaultName($this);
}
/**
* Forget the cached permissions.
*/
public function forgetCachedPermissions()
{
app(PermissionRegistrar::class)->forgetCachedPermissions();
}
/**
* Check if the model has All of the requested Direct permissions.
*
* @param string|int|array|Permission|Collection|\BackedEnum ...$permissions
*/
public function hasAllDirectPermissions(...$permissions): bool
{
$permissions = collect($permissions)->flatten();
foreach ($permissions as $permission) {
if (! $this->hasDirectPermission($permission)) {
return false;
}
}
return true;
}
/**
* Check if the model has Any of the requested Direct permissions.
*
* @param string|int|array|Permission|Collection|\BackedEnum ...$permissions
*/
public function hasAnyDirectPermission(...$permissions): bool
{
$permissions = collect($permissions)->flatten();
foreach ($permissions as $permission) {
if ($this->hasDirectPermission($permission)) {
return true;
}
}
return false;
}
}
+446
View File
@@ -0,0 +1,446 @@
<?php
namespace Spatie\Permission\Traits;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Permission;
use Spatie\Permission\Contracts\Role;
use Spatie\Permission\Events\RoleAttached;
use Spatie\Permission\Events\RoleDetached;
use Spatie\Permission\PermissionRegistrar;
trait HasRoles
{
use HasPermissions;
private ?string $roleClass = null;
public static function bootHasRoles()
{
static::deleting(function ($model) {
if (method_exists($model, 'isForceDeleting') && ! $model->isForceDeleting()) {
return;
}
$teams = app(PermissionRegistrar::class)->teams;
app(PermissionRegistrar::class)->teams = false;
$model->roles()->detach();
if (is_a($model, Permission::class)) {
$model->users()->detach();
}
app(PermissionRegistrar::class)->teams = $teams;
});
}
public function getRoleClass(): string
{
if (! $this->roleClass) {
$this->roleClass = app(PermissionRegistrar::class)->getRoleClass();
}
return $this->roleClass;
}
/**
* A model may have multiple roles.
*/
public function roles(): BelongsToMany
{
$relation = $this->morphToMany(
config('permission.models.role'),
'model',
config('permission.table_names.model_has_roles'),
config('permission.column_names.model_morph_key'),
app(PermissionRegistrar::class)->pivotRole
);
if (! app(PermissionRegistrar::class)->teams) {
return $relation;
}
$teamsKey = app(PermissionRegistrar::class)->teamsKey;
$relation->withPivot($teamsKey);
$teamField = config('permission.table_names.roles').'.'.$teamsKey;
return $relation->wherePivot($teamsKey, getPermissionsTeamId())
->where(fn ($q) => $q->whereNull($teamField)->orWhere($teamField, getPermissionsTeamId()));
}
/**
* Scope the model query to certain roles only.
*
* @param string|int|array|Role|Collection|\BackedEnum $roles
* @param string $guard
* @param bool $without
*/
public function scopeRole(Builder $query, $roles, $guard = null, $without = false): Builder
{
if ($roles instanceof Collection) {
$roles = $roles->all();
}
$roles = array_map(function ($role) use ($guard) {
if ($role instanceof Role) {
return $role;
}
if ($role instanceof \BackedEnum) {
$role = $role->value;
}
$method = is_int($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName';
return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName());
}, Arr::wrap($roles));
$key = (new ($this->getRoleClass())())->getKeyName();
return $query->{! $without ? 'whereHas' : 'whereDoesntHave'}('roles', fn (Builder $subQuery) => $subQuery
->whereIn(config('permission.table_names.roles').".$key", \array_column($roles, $key))
);
}
/**
* Scope the model query to only those without certain roles.
*
* @param string|int|array|Role|Collection|\BackedEnum $roles
* @param string $guard
*/
public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder
{
return $this->scopeRole($query, $roles, $guard, true);
}
/**
* Returns array of role ids
*
* @param string|int|array|Role|Collection|\BackedEnum $roles
*/
private function collectRoles(...$roles): array
{
return collect($roles)
->flatten()
->reduce(function ($array, $role) {
if (empty($role)) {
return $array;
}
$role = $this->getStoredRole($role);
if (! in_array($role->getKey(), $array)) {
$this->ensureModelSharesGuard($role);
$array[] = $role->getKey();
}
return $array;
}, []);
}
/**
* Assign the given role to the model.
*
* @param string|int|array|Role|Collection|\BackedEnum ...$roles
* @return $this
*/
public function assignRole(...$roles)
{
$roles = $this->collectRoles($roles);
$model = $this->getModel();
$teamPivot = app(PermissionRegistrar::class)->teams && ! is_a($this, Permission::class) ?
[app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : [];
if ($model->exists) {
if (app(PermissionRegistrar::class)->teams) {
// explicit reload in case team has been changed since last load
$this->load('roles');
}
$currentRoles = $this->roles->map(fn ($role) => $role->getKey())->toArray();
$this->roles()->attach(array_diff($roles, $currentRoles), $teamPivot);
$model->unsetRelation('roles');
} else {
$class = \get_class($model);
$saved = false;
$class::saved(
function ($object) use ($roles, $model, $teamPivot, &$saved) {
if ($saved || $model->getKey() != $object->getKey()) {
return;
}
$model->roles()->attach($roles, $teamPivot);
$model->unsetRelation('roles');
$saved = true;
}
);
}
if (is_a($this, Permission::class)) {
$this->forgetCachedPermissions();
}
if (config('permission.events_enabled')) {
event(new RoleAttached($this->getModel(), $roles));
}
return $this;
}
/**
* Revoke the given role from the model.
*
* @param string|int|array|Role|Collection|\BackedEnum ...$role
* @return $this
*/
public function removeRole(...$role)
{
$roles = $this->collectRoles($role);
$this->roles()->detach($roles);
$this->unsetRelation('roles');
if (is_a($this, Permission::class)) {
$this->forgetCachedPermissions();
}
if (config('permission.events_enabled')) {
event(new RoleDetached($this->getModel(), $roles));
}
return $this;
}
/**
* Remove all current roles and set the given ones.
*
* @param string|int|array|Role|Collection|\BackedEnum ...$roles
* @return $this
*/
public function syncRoles(...$roles)
{
if ($this->getModel()->exists) {
$this->collectRoles($roles);
if (config('permission.events_enabled')) {
$currentRoles = $this->roles()->get();
if ($currentRoles->isNotEmpty()) {
$this->removeRole($currentRoles);
}
} else {
$this->roles()->detach();
$this->setRelation('roles', collect());
}
}
return $this->assignRole($roles);
}
/**
* Determine if the model has (one of) the given role(s).
*
* @param string|int|array|Role|Collection|\BackedEnum $roles
*/
public function hasRole($roles, ?string $guard = null): bool
{
$this->loadMissing('roles');
if (is_string($roles) && strpos($roles, '|') !== false) {
$roles = $this->convertPipeToArray($roles);
}
if ($roles instanceof \BackedEnum) {
$roles = $roles->value;
return $this->roles
->when($guard, fn ($q) => $q->where('guard_name', $guard))
->pluck('name')
->contains(function ($name) use ($roles) {
/** @var string|\BackedEnum $name */
if ($name instanceof \BackedEnum) {
return $name->value == $roles;
}
return $name == $roles;
});
}
if (is_int($roles) || PermissionRegistrar::isUid($roles)) {
$key = (new ($this->getRoleClass())())->getKeyName();
return $guard
? $this->roles->where('guard_name', $guard)->contains($key, $roles)
: $this->roles->contains($key, $roles);
}
if (is_string($roles)) {
return $guard
? $this->roles->where('guard_name', $guard)->contains('name', $roles)
: $this->roles->contains('name', $roles);
}
if ($roles instanceof Role) {
return $this->roles->contains($roles->getKeyName(), $roles->getKey());
}
if (is_array($roles)) {
foreach ($roles as $role) {
if ($this->hasRole($role, $guard)) {
return true;
}
}
return false;
}
if ($roles instanceof Collection) {
return $roles->intersect($guard ? $this->roles->where('guard_name', $guard) : $this->roles)->isNotEmpty();
}
throw new \TypeError('Unsupported type for $roles parameter to hasRole().');
}
/**
* Determine if the model has any of the given role(s).
*
* Alias to hasRole() but without Guard controls
*
* @param string|int|array|Role|Collection|\BackedEnum $roles
*/
public function hasAnyRole(...$roles): bool
{
return $this->hasRole($roles);
}
/**
* Determine if the model has all of the given role(s).
*
* @param string|array|Role|Collection|\BackedEnum $roles
*/
public function hasAllRoles($roles, ?string $guard = null): bool
{
$this->loadMissing('roles');
if ($roles instanceof \BackedEnum) {
$roles = $roles->value;
}
if (is_string($roles) && strpos($roles, '|') !== false) {
$roles = $this->convertPipeToArray($roles);
}
if (is_string($roles)) {
return $this->hasRole($roles, $guard);
}
if ($roles instanceof Role) {
return $this->roles->contains($roles->getKeyName(), $roles->getKey());
}
$roles = collect()->make($roles)->map(function ($role) {
if ($role instanceof \BackedEnum) {
return $role->value;
}
return $role instanceof Role ? $role->name : $role;
});
$roleNames = $guard
? $this->roles->where('guard_name', $guard)->pluck('name')
: $this->getRoleNames();
$roleNames = $roleNames->transform(function ($roleName) {
if ($roleName instanceof \BackedEnum) {
return $roleName->value;
}
return $roleName;
});
return $roles->intersect($roleNames) == $roles;
}
/**
* Determine if the model has exactly all of the given role(s).
*
* @param string|array|Role|Collection|\BackedEnum $roles
*/
public function hasExactRoles($roles, ?string $guard = null): bool
{
$this->loadMissing('roles');
if (is_string($roles) && strpos($roles, '|') !== false) {
$roles = $this->convertPipeToArray($roles);
}
if (is_string($roles)) {
$roles = [$roles];
}
if ($roles instanceof Role) {
$roles = [$roles->name];
}
$roles = collect()->make($roles)->map(fn ($role) => $role instanceof Role ? $role->name : $role
);
return $this->roles->count() == $roles->count() && $this->hasAllRoles($roles, $guard);
}
/**
* Return all permissions directly coupled to the model.
*/
public function getDirectPermissions(): Collection
{
return $this->permissions;
}
public function getRoleNames(): Collection
{
$this->loadMissing('roles');
return $this->roles->pluck('name');
}
protected function getStoredRole($role): Role
{
if ($role instanceof \BackedEnum) {
$role = $role->value;
}
if (is_int($role) || PermissionRegistrar::isUid($role)) {
return $this->getRoleClass()::findById($role, $this->getDefaultGuardName());
}
if (is_string($role)) {
return $this->getRoleClass()::findByName($role, $this->getDefaultGuardName());
}
return $role;
}
protected function convertPipeToArray(string $pipeString)
{
$pipeString = trim($pipeString);
if (strlen($pipeString) <= 2) {
return [str_replace('|', '', $pipeString)];
}
$quoteCharacter = substr($pipeString, 0, 1);
$endCharacter = substr($quoteCharacter, -1, 1);
if ($quoteCharacter !== $endCharacter) {
return explode('|', $pipeString);
}
if (! in_array($quoteCharacter, ["'", '"'])) {
return explode('|', $pipeString);
}
return explode('|', trim($pipeString, $quoteCharacter));
}
}
@@ -0,0 +1,19 @@
<?php
namespace Spatie\Permission\Traits;
use Spatie\Permission\PermissionRegistrar;
trait RefreshesPermissionCache
{
public static function bootRefreshesPermissionCache()
{
static::saved(function () {
app(PermissionRegistrar::class)->forgetCachedPermissions();
});
static::deleted(function () {
app(PermissionRegistrar::class)->forgetCachedPermissions();
});
}
}
@@ -0,0 +1,118 @@
<?php
namespace Spatie\Permission;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Spatie\Permission\Contracts\Wildcard;
use Spatie\Permission\Exceptions\WildcardPermissionNotProperlyFormatted;
class WildcardPermission implements Wildcard
{
/** @var string */
public const WILDCARD_TOKEN = '*';
/** @var non-empty-string */
public const PART_DELIMITER = '.';
/** @var non-empty-string */
public const SUBPART_DELIMITER = ',';
protected Model $record;
public function __construct(Model $record)
{
$this->record = $record;
}
public function getIndex(): array
{
$index = [];
foreach ($this->record->getAllPermissions() as $permission) {
$index[$permission->guard_name] = $this->buildIndex(
$index[$permission->guard_name] ?? [],
explode(static::PART_DELIMITER, $permission->name),
$permission->name,
);
}
return $index;
}
protected function buildIndex(array $index, array $parts, string $permission): array
{
if (empty($parts)) {
$index[''] = true;
return $index;
}
$part = array_shift($parts);
if (blank($part)) {
throw WildcardPermissionNotProperlyFormatted::create($permission);
}
if (! Str::contains($part, static::SUBPART_DELIMITER)) {
$index[$part] = $this->buildIndex(
$index[$part] ?? [],
$parts,
$permission,
);
}
$subParts = explode(static::SUBPART_DELIMITER, $part);
foreach ($subParts as $subPart) {
if (blank($subPart)) {
throw WildcardPermissionNotProperlyFormatted::create($permission);
}
$index[$subPart] = $this->buildIndex(
$index[$subPart] ?? [],
$parts,
$permission,
);
}
return $index;
}
public function implies(string $permission, string $guardName, array $index): bool
{
if (! array_key_exists($guardName, $index)) {
return false;
}
$permission = explode(static::PART_DELIMITER, $permission);
return $this->checkIndex($permission, $index[$guardName]);
}
protected function checkIndex(array $permission, array $index): bool
{
if (array_key_exists(strval(null), $index)) {
return true;
}
if (empty($permission)) {
return false;
}
$firstPermission = array_shift($permission);
if (
array_key_exists($firstPermission, $index) &&
$this->checkIndex($permission, $index[$firstPermission])
) {
return true;
}
if (array_key_exists(static::WILDCARD_TOKEN, $index)) {
return $this->checkIndex($permission, $index[static::WILDCARD_TOKEN]);
}
return false;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
if (! function_exists('getModelForGuard')) {
function getModelForGuard(string $guard): ?string
{
return Spatie\Permission\Guard::getModelForGuard($guard);
}
}
if (! function_exists('setPermissionsTeamId')) {
/**
* @param int|string|null|\Illuminate\Database\Eloquent\Model $id
*/
function setPermissionsTeamId($id)
{
app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId($id);
}
}
if (! function_exists('getPermissionsTeamId')) {
/**
* @return int|string|null
*/
function getPermissionsTeamId()
{
return app(\Spatie\Permission\PermissionRegistrar::class)->getPermissionsTeamId();
}
}
+18
View File
@@ -0,0 +1,18 @@
# Changelog
All notable changes to `macroable` will be documented in this file
## 2.0.0 - 2021-03-26
- require PHP 8+
- drop support for PHP 7.x
- convert syntax to PHP 8
## 1.0.1 - 2020-11-03
- add support for PHP 8.0
- drop support for PHP 7.1 and below
## 1.0.0 - 2017-09-16
- initial release
+55
View File
@@ -0,0 +1,55 @@
# Contributing
Contributions are **welcome** and will be fully **credited**.
Please read and understand the contribution guide before creating an issue or pull request.
## Etiquette
This project is open source, and as such, the maintainers give their free time to build and maintain the source code
held within. They make the code freely available in the hope that it will be of use to other developers. It would be
extremely unfair for them to suffer abuse or anger for their hard work.
Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the
world that developers are civilized and selfless people.
It's the duty of the maintainer to ensure that all submissions to the project are of sufficient
quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.
## Viability
When requesting or submitting new features, first consider whether it might be useful to others. Open
source projects are used by many developers, who may have entirely different needs to your own. Think about
whether or not your feature is likely to be used by other users of the project.
## Procedure
Before filing an issue:
- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.
- Check to make sure your feature suggestion isn't already present within the project.
- Check the pull requests tab to ensure that the bug doesn't have a fix in progress.
- Check the pull requests tab to ensure that the feature isn't already in progress.
Before submitting a pull request:
- Check the codebase to ensure that your feature doesn't already exist.
- Check the pull requests to ensure that another person hasn't already submitted the feature or fix.
## Requirements
If the project maintainer has any additional requirements, you will find them listed here.
- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer).
- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
- **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.
**Happy coding**!
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Spatie bvba <info@spatie.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+135
View File
@@ -0,0 +1,135 @@
# A trait to dynamically add methods to a class
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/macroable.svg?style=flat-square)](https://packagist.org/packages/spatie/macroable)
![run-tests](https://github.com/spatie/macroable/workflows/run-tests/badge.svg)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/macroable.svg?style=flat-square)](https://packagist.org/packages/spatie/macroable)
This package provides a trait that, when applied to a class, makes it possible to add methods to that class at runtime.
Here's a quick example:
```php
$myClass = new class() {
use Spatie\Macroable\Macroable;
};
$myClass::macro('concatenate', function(... $strings) {
return implode('-', $strings);
});
$myClass->concatenate('one', 'two', 'three'); // returns 'one-two-three'
```
The idea of a macroable trait and the implementation is taken from [the `macroable` trait](https://github.com/laravel/framework/blob/master/src/Illuminate/Support/Traits/Macroable.php) of the [Laravel framework](https://laravel.com).
## Support us
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/macroable.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/macroable)
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).
## Installation
You can install the package via composer:
```bash
composer require spatie/macroable
```
## Usage
You can add a new method to a class using `macro`:
```php
$macroableClass = new class() {
use Spatie\Macroable\Macroable;
};
$macroableClass::macro('concatenate', function(... $strings) {
return implode('-', $strings);
};
$macroableClass->concatenate('one', 'two', 'three'); // returns 'one-two-three'
```
Callables passed to the `macro` function will be bound to the `class`
```php
$macroableClass = new class() {
protected $name = 'myName';
use Spatie\Macroable\Macroable;
};
$macroableClass::macro('getName', function() {
return $this->name;
};
$macroableClass->getName(); // returns 'myName'
```
You can also add multiple methods in one go by using a mixin class. A mixin class contains methods that return callables. Each method from the mixin will be registered on the macroable class.
```php
$mixin = new class() {
public function mixinMethod()
{
return function() {
return 'mixinMethod';
};
}
public function anotherMixinMethod()
{
return function() {
return 'anotherMixinMethod';
};
}
};
$macroableClass->mixin($mixin);
$macroableClass->mixinMethod() // returns 'mixinMethod';
$macroableClass->anotherMixinMethod() // returns 'anotherMixinMethod';
```
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Testing
``` bash
composer test
```
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
## Security
If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker.
## Postcardware
You're free to use this package (it's [MIT-licensed](LICENSE.md)), but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using.
Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium.
We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards).
## Credits
- [Freek Van der Herten](https://github.com/freekmurze)
- [All Contributors](../../contributors)
Idea and code is taken from [the `macroable` trait](https://github.com/laravel/framework/blob/master/src/Illuminate/Support/Traits/Macroable.php) of the [Laravel framework](https://laravel.com).
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
+40
View File
@@ -0,0 +1,40 @@
{
"name": "spatie/macroable",
"description": "A trait to dynamically add methods to a class",
"keywords": [
"spatie",
"macroable"
],
"homepage": "https://github.com/spatie/macroable",
"license": "MIT",
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"require": {
"php": "^8.0"
},
"require-dev": {
"phpunit/phpunit": "^8.0|^9.3"
},
"autoload": {
"psr-4": {
"Spatie\\Macroable\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Spatie\\Macroable\\Test\\": "tests"
}
},
"scripts": {
"test": "vendor/bin/phpunit"
},
"config": {
"sort-packages": true
}
}
View File
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace Spatie\Macroable;
use Closure;
use ReflectionClass;
use ReflectionMethod;
use BadMethodCallException;
trait Macroable
{
protected static array $macros = [];
public static function macro(string $name, object | callable $macro): void
{
static::$macros[$name] = $macro;
}
public static function mixin(object | string $mixin): void
{
$methods = (new ReflectionClass($mixin))->getMethods(
ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
);
foreach ($methods as $method) {
$method->setAccessible(true);
static::macro($method->name, $method->invoke($mixin));
}
}
public static function hasMacro(string $name): bool
{
return isset(static::$macros[$name]);
}
public static function __callStatic($method, $parameters)
{
if (! static::hasMacro($method)) {
throw new BadMethodCallException("Method {$method} does not exist.");
}
$macro = static::$macros[$method];
if ($macro instanceof Closure) {
return call_user_func_array(Closure::bind($macro, null, static::class), $parameters);
}
return call_user_func_array($macro, $parameters);
}
public function __call($method, $parameters)
{
if (! static::hasMacro($method)) {
throw new BadMethodCallException("Method {$method} does not exist.");
}
$macro = static::$macros[$method];
if ($macro instanceof Closure) {
return call_user_func_array($macro->bindTo($this, static::class), $parameters);
}
return call_user_func_array($macro, $parameters);
}
}
+21
View File
@@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) Spatie bvba <info@spatie.be>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
> THE SOFTWARE.
+159
View File
@@ -0,0 +1,159 @@
# Parse, build and manipulate URLs
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/url.svg?style=flat-square)](https://packagist.org/packages/spatie/url)
[![Tests](https://github.com/spatie/url/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spatie/url/actions/workflows/run-tests.yml)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/url.svg?style=flat-square)](https://packagist.org/packages/spatie/url)
A simple package to deal with URLs in your applications.
## Installation
You can install the package via composer:
```bash
composer require spatie/url
```
## Usage
### Parse and transform a URL
Retrieve any part of the URL:
```php
use Spatie\Url\Url;
$url = Url::fromString('https://spatie.be/opensource');
echo $url->getScheme(); // 'https'
echo $url->getHost(); // 'spatie.be'
echo $url->getPath(); // '/opensource'
```
Transform any part of the URL:
> **Note**
> the `Url` class is immutable.
```php
$url = Url::fromString('https://spatie.be/opensource');
echo $url->withHost('github.com')->withPath('spatie');
// 'https://github.com/spatie'
```
### Scheme
Transform the URL scheme.
```php
$url = Url::fromString('http://spatie.be/opensource');
echo $url->withScheme('https'); // 'https://spatie.be/opensource'
```
Use a list of allowed schemes.
> **Note**
> each scheme in the list will be sanitized
```php
$url = Url::fromString('https://spatie.be/opensource');
echo $url->withAllowedSchemes(['wss'])->withScheme('wss'); // 'wss://spatie.be/opensource'
```
or pass the list directly to `fromString` as the URL's scheme will be sanitized and validated immediately:
```php
$url = Url::fromString('https://spatie.be/opensource', [...SchemeValidator::VALID_SCHEMES, 'wss']);
echo $url->withScheme('wss'); // 'wss://spatie.be/opensource'
```
### Query parameters
Retrieve and transform query parameters:
```php
$url = Url::fromString('https://spatie.be/opensource?utm_source=github&utm_campaign=packages');
echo $url->getQuery(); // 'utm_source=github&utm_campaign=packages'
echo $url->getQueryParameter('utm_source'); // 'github'
echo $url->getQueryParameter('utm_medium'); // null
echo $url->getQueryParameter('utm_medium', 'social'); // 'social'
echo $url->getQueryParameter('utm_medium', function() {
//some logic
return 'email';
}); // 'email'
echo $url->withoutQueryParameter('utm_campaign'); // 'https://spatie.be/opensource?utm_source=github'
echo $url->withQueryParameters(['utm_campaign' => 'packages']); // 'https://spatie.be/opensource?utm_source=github&utm_campaign=packages'
```
### Path segments
Retrieve path segments:
```php
$url = Url::fromString('https://spatie.be/opensource/laravel');
echo $url->getSegment(1); // 'opensource'
echo $url->getSegment(2); // 'laravel'
```
### PSR-7 `UriInterface`
Implements PSR-7's `UriInterface` interface:
```php
class Url implements UriInterface { /* ... */ }
```
The [`league/uri`](https://github.com/thephpleague/uri) is a more powerful package than this one. The main reason this package exists, is because the alternatives requires non-standard php extensions. If you're dealing with special character encodings or need bulletproof validation, you're definitely better off using `league/uri`.
Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource).
## Testing
```bash
composer test
```
## Support us
[<img src="https://github-ads.s3.eu-central-1.amazonaws.com/url.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/url)
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).
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Contributing
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
## Security Vulnerabilities
Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
## Postcardware
You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using.
Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium.
We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards).
## Credits
- [Sebastian De Deyne](https://github.com/sebastiandedeyne)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
+48
View File
@@ -0,0 +1,48 @@
{
"name": "spatie/url",
"description": "Parse, build and manipulate URL's",
"license": "MIT",
"keywords": [
"spatie",
"url"
],
"authors": [
{
"name": "Sebastian De Deyne",
"email": "sebastian@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"homepage": "https://github.com/spatie/url",
"require": {
"php": "^8.0",
"psr/http-message": "^1.0 || ^2.0",
"spatie/macroable": "^1.0 || ^2.0"
},
"require-dev": {
"pestphp/pest": "^1.21"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Spatie\\Url\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Spatie\\Url\\Test\\": "tests"
}
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
},
"sort-packages": true
},
"scripts": {
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage-html coverage"
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Spatie\Url;
use Spatie\Url\Contracts\Validator;
abstract class BaseValidator implements Validator
{
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Spatie\Url\Contracts;
interface Validator
{
public function validate(): void;
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Spatie\Url\Exceptions;
use InvalidArgumentException;
class InvalidArgument extends InvalidArgumentException
{
public static function invalidScheme(string $scheme, array $allowedSchemes): static
{
$schemes = implode(', ', array_map(fn ($scheme) => "`{$scheme}`", $allowedSchemes));
return new static("The scheme `{$scheme}` isn't valid. It should be either {$schemes}.");
}
public static function invalidUrl(string $url): static
{
return new static("The string `{$url}` is no valid url.");
}
public static function segmentZeroDoesNotExist(): static
{
return new static("Segment 0 doesn't exist. Segments can be retrieved by using 1-based index or a negative index.");
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace Spatie\Url;
class QueryParameterBag implements \Stringable
{
public function __construct(
protected array $parameters = [],
) {
//
}
public static function fromString(string $query = ''): static
{
if ($query === '') {
return new static();
}
$parameters = [];
parse_str($query, $parameters);
$parameters = array_map(fn ($param) => $param !== '' ? $param : null, $parameters);
return new static($parameters);
}
public function get(string $key, mixed $default = null): mixed
{
if ($this->has($key)) {
return $this->parameters[$key];
}
return is_callable($default) ? $default() : $default;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->parameters);
}
public function set(string $key, string|array $value): self
{
$this->parameters[$key] = $value;
return $this;
}
public function unset(string $key): self
{
unset($this->parameters[$key]);
return $this;
}
public function unsetAll(): self
{
$this->parameters = [];
return $this;
}
public function all(): array
{
return $this->parameters;
}
public function __toString(): string
{
return http_build_query($this->parameters, '', '&', PHP_QUERY_RFC3986);
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Spatie\Url;
use Spatie\Macroable\Macroable;
use Stringable;
class Scheme implements Stringable
{
use Macroable;
protected string $scheme;
protected SchemeValidator $validator;
public function __construct(
string $scheme = '',
array|null $allowedSchemes = null,
) {
$this->validator = new SchemeValidator($allowedSchemes);
$this->setScheme($scheme);
}
protected function validate(string $scheme): void
{
$this->validator->setScheme($scheme);
$this->validator->validate();
}
public function getScheme(): string
{
return $this->scheme;
}
public function setScheme(string $scheme): void
{
$sanitizedScheme = $this->validator::sanitizeScheme($scheme);
$this->validate($sanitizedScheme);
$this->scheme = $sanitizedScheme;
}
public function getAllowedSchemes(): array
{
return $this->validator->getAllowedSchemes();
}
public function setAllowedSchemes(array $allowedSchemes): void
{
$this->validator->setAllowedSchemes($allowedSchemes);
}
public function __toString(): string
{
return $this->getScheme();
}
public function __clone()
{
$this->validator = clone $this->validator;
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace Spatie\Url;
use Spatie\Url\Exceptions\InvalidArgument;
class SchemeValidator extends BaseValidator
{
public const VALID_SCHEMES = ['http', 'https', 'mailto', 'tel'];
private string|null $scheme;
public function __construct(
private array|null $allowedSchemes = null
) {
$this->scheme = null;
$this->allowedSchemes = $allowedSchemes ?? self::VALID_SCHEMES;
}
public function validate(): void
{
// '' aka "no scheme" must always be valid
$alwaysAllowedSchemes = [''];
if (! in_array($this->scheme, [...$this->allowedSchemes, ...$alwaysAllowedSchemes])) {
throw InvalidArgument::invalidScheme($this->scheme, $this->allowedSchemes);
}
}
public static function sanitizeScheme(string $scheme): string
{
// TODO: regex to allow correct format according to https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
return strtolower($scheme);
}
public function getScheme(): string|null
{
return $this->scheme;
}
public function setScheme(string $scheme): void
{
$this->scheme = $scheme;
}
public function getAllowedSchemes(): array|null
{
return $this->allowedSchemes;
}
public function setAllowedSchemes(array $allowedSchemes): void
{
$this->allowedSchemes = array_map(
fn ($scheme) => static::sanitizeScheme($scheme),
$allowedSchemes
);
}
}
+373
View File
@@ -0,0 +1,373 @@
<?php
namespace Spatie\Url;
use Psr\Http\Message\UriInterface;
use Spatie\Macroable\Macroable;
use Spatie\Url\Exceptions\InvalidArgument;
use Stringable;
class Url implements UriInterface, Stringable
{
use Macroable;
protected Scheme $scheme;
protected string $host = '';
protected ?int $port = null;
protected string $user = '';
protected ?string $password = null;
protected string $path = '';
protected QueryParameterBag $query;
protected string $fragment = '';
public function __construct()
{
$this->scheme = new Scheme();
$this->query = new QueryParameterBag();
}
public static function create(): static
{
return new static();
}
public static function fromString(string $url, array|null $allowedSchemes = null): static
{
$toUrl = new static();
if($allowedSchemes !== null) {
$toUrl = $toUrl->withAllowedSchemes($allowedSchemes);
}
return static::make($url, $toUrl);
}
protected static function make(string $fromUrl, self $toUrl): static
{
if (! $parts = parse_url($fromUrl)) {
throw InvalidArgument::invalidUrl($fromUrl);
}
$toUrl->scheme->setScheme(isset($parts['scheme']) ? $parts['scheme'] : '');
$toUrl->host = $parts['host'] ?? '';
$toUrl->port = $parts['port'] ?? null;
$toUrl->user = $parts['user'] ?? '';
$toUrl->password = $parts['pass'] ?? null;
$toUrl->path = $parts['path'] ?? '/';
$toUrl->query = QueryParameterBag::fromString($parts['query'] ?? '');
$toUrl->fragment = $parts['fragment'] ?? '';
return $toUrl;
}
public function getScheme(): string
{
return $this->scheme;
}
public function getAuthority(): string
{
$authority = $this->host;
if ($this->getUserInfo()) {
$authority = $this->getUserInfo().'@'.$authority;
}
if ($this->port !== null) {
$authority .= ':'.$this->port;
}
return $authority;
}
public function getUserInfo(): string
{
$userInfo = $this->user;
if ($this->password !== null) {
$userInfo .= ':'.$this->password;
}
return $userInfo;
}
public function getHost(): string
{
return $this->host;
}
public function getPort(): ?int
{
return $this->port;
}
public function getPath(): string
{
return $this->path;
}
public function getBasename(): string
{
return $this->getSegment(-1);
}
public function getDirname(): string
{
$segments = $this->getSegments();
array_pop($segments);
return '/'.implode('/', $segments);
}
public function getQuery(): string
{
return (string) $this->query;
}
public function getQueryParameter(string $key, mixed $default = null): mixed
{
return $this->query->get($key, $default);
}
public function hasQueryParameter(string $key): bool
{
return $this->query->has($key);
}
public function getAllQueryParameters(): array
{
return $this->query->all();
}
public function withQueryParameter(string $key, string $value): static
{
$url = clone $this;
$url->query->unset($key);
$url->query->set($key, $value);
return $url;
}
public function withQueryParameters(array $parameters): static
{
$parameters = array_merge($this->getAllQueryParameters(), $parameters);
$url = clone $this;
$url->query = new QueryParameterBag($parameters);
return $url;
}
public function withoutQueryParameter(string $key): static
{
$url = clone $this;
$url->query->unset($key);
return $url;
}
public function withoutQueryParameters(): static
{
$url = clone $this;
$url->query->unsetAll();
return $url;
}
public function getFragment(): string
{
return $this->fragment;
}
public function getSegments(): array
{
return explode('/', trim($this->path, '/'));
}
public function getSegment(int $index, mixed $default = null): mixed
{
$segments = $this->getSegments();
if ($index === 0) {
throw InvalidArgument::segmentZeroDoesNotExist();
}
if ($index < 0) {
$segments = array_reverse($segments);
$index = abs($index);
}
return $segments[$index - 1] ?? $default;
}
public function getFirstSegment(): mixed
{
$segments = $this->getSegments();
return $segments[0] ?? null;
}
public function getLastSegment(): mixed
{
$segments = $this->getSegments();
return end($segments) ?? null;
}
public function withScheme($scheme): static
{
$url = clone $this;
$url->scheme->setScheme($scheme);
return $url;
}
public function withAllowedSchemes(array $schemes): static
{
$url = clone $this;
$url->scheme->setAllowedSchemes($schemes);
return $url;
}
public function withUserInfo($user, $password = null): static
{
$url = clone $this;
$url->user = $user;
$url->password = $password;
return $url;
}
public function withHost($host): static
{
$url = clone $this;
$url->host = $host;
return $url;
}
public function withPort($port): static
{
$url = clone $this;
$url->port = $port;
return $url;
}
public function withPath($path): static
{
$url = clone $this;
if (! str_starts_with($path, '/')) {
$path = '/'.$path;
}
$url->path = $path;
return $url;
}
public function withDirname(string $dirname): static
{
$dirname = trim($dirname, '/');
if (! $this->getBasename()) {
return $this->withPath($dirname);
}
return $this->withPath($dirname.'/'.$this->getBasename());
}
public function withBasename(string $basename): static
{
$basename = trim($basename, '/');
if ($this->getDirname() === '/') {
return $this->withPath('/'.$basename);
}
return $this->withPath($this->getDirname().'/'.$basename);
}
public function withQuery($query): static
{
$url = clone $this;
$url->query = QueryParameterBag::fromString($query);
return $url;
}
public function withFragment($fragment): static
{
$url = clone $this;
$url->fragment = $fragment;
return $url;
}
public function matches(self $url): bool
{
return (string) $this === (string) $url;
}
public function __toString(): string
{
$url = '';
if ($this->getScheme() !== '' && ! in_array($this->getScheme(), ['mailto', 'tel'], true)) {
$url .= $this->getScheme().'://';
}
if (in_array($this->getScheme(), ['mailto', 'tel'], true) && $this->getPath() !== '') {
$url .= $this->getScheme().':';
}
if ($this->getScheme() === '' && $this->getAuthority() !== '') {
$url .= '//';
}
if ($this->getAuthority() !== '') {
$url .= $this->getAuthority();
}
if ($this->getPath() !== '/') {
$path = in_array($this->getScheme(), ['mailto', 'tel'], true)
? ltrim($this->getPath(), '/')
: $this->getPath();
$url .= $path;
}
if ($this->getQuery() !== '') {
$url .= '?'.$this->getQuery();
}
if ($this->getFragment() !== '') {
$url .= '#'.$this->getFragment();
}
return $url;
}
public function __clone()
{
$this->query = clone $this->query;
$this->scheme = clone $this->scheme;
}
}