ACL برای Lumen و Laravel 7+

نقش‌ها و مجوزها همیشه یه بخش مهم از هر وب‌سایت و وب‌اپ رو تشکیل می‌دن. برای این‌کار پلاگین‌ها و ماژول‌های زیادی نوشته شدن ولی وقتی که می‌خواین از یه فریم‌ورک مثل Lumen استفاده کنین، یکم پیدا کردن یه ماژول که بی‌دردسر با پروژتون یک‌پارچه بشه کار راحتی نیست. تازگی‌ها به یه پروژه برخوردم که توش باشد ACL رو پیاده می‌کردم و طبق معمول رفتم سراغ Packagist ولی هرچی نصب کردم اونی نشد که می‌خواستم! آخرشم گفتم کس نخارد پشت من جز نخن انگشت من و شروع کردم به نوشتن این سیستم خیلی ساده و بی‌دردسر.قدم اول: ساخت مدل‌هابرای شروع این‌کار ما به دو مدل اصلی نیاز داریم: Permission و Role. (مدل User هم که قاعدتا وقتی نباشه که چرا پس داریم این‌کارو انجام می‌دیم؟ ?)php artisan make:model Role -m php artisan make:model Permission -mاون کلید m- هم کارش ساخت فایل‌های migration هستش.قدم دوم: ویرایش فایل‌های migrationمحتوای فایل‌های migration تون رو به شکل زیر تغییر بدین:مدل User (این مدل خیلی لازم نیست این شکلی باشه، شما می‌تونید بر اساس نیازتون هر فیلدی که دوست داشتید رو بهش اضافه یا ازش کم کنید):public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('email',191)->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); }مدل Permission:use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreatePermissionsTable extends Migration { public function up() { Schema::create('permissions', function (Blueprint $table) { $table->increments('id'); $table->string('name'); // edit posts $table->string('slug'); //edit-posts $table->timestamps(); }); } public function down() { Schema::dropIfExists('permissions'); } }مدل Role:use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateRolesTable extends Migration { public function up() { Schema::create('roles', function (Blueprint $table) { $table->increments('id'); $table->string('name'); // edit posts $table->string('slug'); //edit-posts $table->timestamps(); }); } public function down() { Schema::dropIfExists('roles'); } }قدم سوم: ساخت جداول pivot:ارتباط بین user و permissionها:php artisan make:migration create_users_permissions_table --create=users_permissionsو محتواش:use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersPermissionsTable extends Migration { public function up() { Schema::create('users_permissions', function (Blueprint $table) { $table->unsignedInteger('user_id'); $table->unsignedInteger('permission_id'); //FOREIGN KEY CONSTRAINTS $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade'); //SETTING THE PRIMARY KEYS $table->primary(['user_id','permission_id']); }); } public function down() { Schema::dropIfExists('users_permissions'); } }حالا یکی دیگه، ارتباط بین user و roleها:php artisan make:migration create_users_roles_table --create=users_rolesو محتواش:use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUsersRolesTable extends Migration { public function up() { Schema::create('users_roles', function (Blueprint $table) { $table->unsignedInteger('user_id'); $table->unsignedInteger('role_id'); //FOREIGN KEY CONSTRAINTS $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); //SETTING THE PRIMARY KEYS $table->primary(['user_id','role_id']); }); } public function down() { Schema::dropIfExists('users_roles'); } }و آخری برای ارتباط بین role و permission:php artisan make:migration create_roles_permissions_table --create=roles_permissionsو محتوای آخری:use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateRolesPermissionsTable extends Migration { public function up() { Schema::create('roles_permissions', function (Blueprint $table) { $table->unsignedInteger('role_id'); $table->unsignedInteger('permission_id'); //FOREIGN KEY CONSTRAINTS $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade'); //SETTING THE PRIMARY KEYS $table->primary(['role_id','permission_id']); }); } public function down() { Schema::dropIfExists('roles_permissions'); } }حالا نوبت چیه؟ migrate کردن!php artisan migrateقدم چهارم: راه‌اندازی ارتباطات در مدل‌هامدل Rolepublic function permissions() { return $this->belongsToMany(Permission::class,'roles_permissions'); } public function users() { return $this->belongsToMany(User::class,'users_roles'); }مدل Permission:public function roles() { return $this->belongsToMany(Role::class,'roles_permissions'); } public function users() { return $this->belongsToMany(User::class,'users_permissions'); }حالا باید یه trait درست کنیم و متدهای مورد نیاز ACL رو توش اضافه کنیم.داخل پوشه app یه پوشه درست کنین به این Permissions و توش یه trait بسازین به اسم HasPermissionsTrait.php.حالا توی مدل User این trait رو import کنین:namespace App; use App\Permissions\HasPermissionsTrait; class User extends Authenticatable { use HasPermissionsTrait; //Import The Trait }محتوای فایل HasPermissionsTrait.php رو به شکل زیر تایپ کنید:namespace App\Permissions; use App\Permission; use App\Role; trait HasPermissionsTrait { public function givePermissionsTo(... $permissions) { $permissions = $this->getAllPermissions($permissions); if($permissions === null) { return $this; } $this->permissions()->saveMany($permissions); return $this; } public function withdrawPermissionsFrom( ... $permissions ) { $permissions = $this->getAllPermissions($permissions); $this->permissions()->detach($permissions); return $this; } public function refreshPermissions( ... $permissions ) { $this->permissions()->detach(); return $this->givePermissionsTo($permissions); } public function hasPermissionTo($permission) { return $this->hasPermissionThroughRole($permission) || $this->hasPermission($permission); } public function hasPermissionThroughRole($permission) { foreach ($permission->roles as $role){ if($this->roles->contains($role)) { return true; } } return false; } public function hasRole( ... $roles ) { foreach ($roles as $role) { if ($this->roles->contains('slug', $role)) { return true; } } return false; } public function roles() { return $this->belongsToMany(Role::class,'users_roles'); } public function permissions() { return $this->belongsToMany(Permission::class,'users_permissions'); } protected function hasPermission($permission) { return (bool) $this->permissions->where('slug', $permission->slug)->count(); } protected function getAllPermissions(array $permissions) { return Permission::whereIn('slug',$permissions)->get(); } }با استفاده از این trait می‌تونیم این‌جوری ازش استفاده کنیم:$user = $request->user(); //getting the current logged in user dd($user->hasRole('admin','editor')); // and so onقدم پنجم: ساخت Service Providerلاراول یه متد قشنگ داره به اسم can، ولی ما یه متد داریم به اسم hasPermissionTo. قشنگ نیست نه؟ خب باید یکاری کنیم که بتونیم از اون can استفاده کنیم:php artisan make:provider PermissionsServiceProviderتوی این Provider این محتوا رو تایپ کنین:namespace App\Providers; use App\Permission; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; class PermissionsServiceProvider extends ServiceProvider { public function register() { // } public function boot() { try { Permission::get()->map(function ($permission) { Gate::define($permission->slug, function ($user) use ($permission) { return $user->hasPermissionTo($permission); }); }); } catch (\Exception $e) { report($e); return false; } // اگر Lumen استفاده می‌کنین این پایین رو بیخیال شین //Blade directives Blade::directive('role', function ($role) { return &quotif(auth()->check() && auth()->user()->hasRole({$role})) :&quot //return this if statement inside php tag }); Blade::directive('endrole', function ($role) { return &quotendif;&quot //return this endif statement inside php tag }); } }config\app.php'providers' => [ App\Providers\PermissionsServiceProvider::class, ],توی Lumen:bootstrap/app.php$app->register(App\Providers\PermissionServiceProvider::class);قدم ششم: ساخت Middlewareبرای اینکه بتونیم چک کنیم چه کاربری با چه سطح دسترسی‌ای می‌تونه به کجا دسترسی پیدا کنه و یا نه باید یه Middleware بسازیم و توی اون این چک رو انجام بدیم. این Middleware رو به اسم role برای لاراول توی:app/Http/Kernel.phpو برای لومن توی:bootstrap/app.phpمحتوای این ‌Middleware باید به شکل زیر باشه:<?php namespace App\Http\Middleware; use Closure; class RoleMiddleware { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next, $roles = &quot&quot, $permissions = &quot&quot) { // Allow user with this permissions to pass if ($request->user()->hasRole(&quotMASTER&quot) || $request->user()->can(&quotFULL_CONTROL&quot)) { return $next($request); } // Check user roles $roles = explode(&quot|&quot, $roles); if ($request->user()->hasRole($roles)) { return $next($request); } // Check user permissions $permissions = explode(&quot|&quot, str_replace(&quotpermissions:&quot, &quot&quot, $permissions)); foreach ($permissions as $permission) { if ($request->user()->can($permission)) { return $next($request); } } // send failed message to user abort(403, trans('validations.you_are_not_authorized')); } }حالا برای استفاده از این Middleware کافیه که روی route هاتون این رو بنویسید:Route::get(&quottest&quot, &quotTestController@yeMethod&quot) ->middleware(['auth', 'role:role1|role2,permissions:permission1,permission2'])همین! به همین سادگی و قشنگی بدون استفاده از پکیج شخص ثالث و درگیری با Dependency ها یه سیستم ACL مجلسی برای پروژتون که با لاراول یا لومن +7 نوشته بودین ساختین!خیلی سعی کردم ساده و مختصر بنویسم و مثل خارجیا از بیخ و بن composer ننویسم ولی امیدوارم گیج کننده نشده باشه!