malinichevvv / yii2-access
Powerful, flexible RBAC extension for Yii2 with role inheritance, dynamic rules, PHP 8 attributes, caching and audit log
Package info
github.com/malinichevvv/yii2-access
Type:yii2-extension
pkg:composer/malinichevvv/yii2-access
Requires
- php: >=8.1
- yiisoft/yii2: ~2.0.45
Requires (Dev)
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2026-05-18 11:41:17 UTC
README
A powerful, flexible RBAC extension for Yii2 with:
- Hierarchical role inheritance — recursive CTE for MySQL 8+ / PostgreSQL, automatic fallback for older MySQL
- Permission groups — organise permissions by module for clean UI trees
- Dynamic rules — attach a callable to any permission for contextual access decisions
- Two-layer caching — per-request in-memory pool + configurable persistent cache (Redis, etc.) with tag-based invalidation
- PHP 8 attributes —
#[RequirePermission]/#[RequireRole]for declarative, zero-boilerplate controller guards - Behaviors —
AccessControlBehavior(controller) andUserAccessBehavior(User model) - Config-based filter —
PermissionFilterfor teams who prefer rules inbehaviors() - Static facade —
Am::checkAccess()for concise one-liner checks anywhere - Optional audit log — append-only table for every access-changing operation
- Multi-tenant — optional
company_idcolumn scopes roles per tenant - Full AR models — for all tables, ready for use in admin UIs
- Two migrations — core tables + audit log (can be applied independently)
Requirements
| Requirement | Version |
|---|---|
| PHP | ≥ 8.1 |
| Yii2 | ~2.0 |
Installation
composer require malinichevvv/yii2-access
Run the migrations:
php yii migrate --migrationPath=@vendor/malinichevvv/yii2-access/src/migrations
Or register the path in your console config:
// console/config/main.php 'controllerMap' => [ 'migrate' => [ 'class' => 'yii\console\controllers\MigrateController', 'migrationPath' => [ '@app/migrations', '@vendor/malinichevvv/yii2-access/src/migrations', ], ], ],
Configuration
The extension auto-registers the access component via bootstrap. You can override it:
// common/config/main.php 'components' => [ 'access' => [ 'class' => \malinichevvv\access\AccessManager::class, 'db' => 'db', // DB component ID 'cache' => 'cache', // Cache component ID (false = disable) 'enableCache' => true, 'enableAuditLog' => true, 'multiTenant' => false, // Set true for company-scoped roles ], ], // Register bootstrap so the component is available everywhere: 'bootstrap' => ['log', 'access'],
Basic Usage
Via static facade
use malinichevvv\access\Am; // Check a single permission if (Am::checkAccess($userId, 'order.create')) { // ... } // Check multiple at once $map = Am::checkMultipleAccess($userId, ['order.create', 'order.delete', 'report.view']); // ['order.create' => true, 'order.delete' => false, 'report.view' => true] // Check by role code if (Am::hasRole($userId, 'admin')) { ... } if (Am::hasRole($userId, ['admin', 'super_admin'])) { ... } // OR logic
Via component
/** @var \malinichevvv\access\AccessManager $access */ $access = Yii::$app->access; $access->checkAccess($userId, 'order.create');
PHP 8 Attributes (Recommended)
Attach AccessControlBehavior to your controller, then annotate actions:
use malinichevvv\access\attributes\RequirePermission; use malinichevvv\access\attributes\RequireRole; use malinichevvv\access\behaviors\AccessControlBehavior; class OrderController extends \yii\web\Controller { public function behaviors(): array { return [ 'access' => AccessControlBehavior::class, ]; } #[RequirePermission('order.view')] public function actionIndex(): string { ... } #[RequirePermission('order.create')] public function actionCreate(): string { ... } // Both permissions must be held #[RequirePermission('order.view')] #[RequirePermission('report.generate')] public function actionReport(): string { ... } // Role check — at least one role in the array must match #[RequireRole(['admin', 'super_admin'])] public function actionDelete(): string { ... } // Role AND permission #[RequireRole('manager')] #[RequirePermission('order.approve')] public function actionApprove(): string { ... } }
Class-level attributes apply to all actions in the controller:
#[RequireRole('admin')] class AdminController extends \yii\web\Controller { public function behaviors(): array { return ['access' => AccessControlBehavior::class]; } // All actions automatically require 'admin' role public function actionIndex(): string { ... } public function actionUsers(): string { ... } }
Config-based Filter
For teams that prefer Yii2's declarative style:
use malinichevvv\access\filters\PermissionFilter; public function behaviors(): array { return [ 'permission' => [ 'class' => PermissionFilter::class, 'rules' => [ ['allow' => true, 'actions' => ['index', 'view']], ['allow' => true, 'actions' => ['create'], 'permissions' => ['order.create']], ['allow' => true, 'actions' => ['delete'], 'roles' => ['admin']], ['allow' => true, 'actions' => ['export'], 'roles' => ['manager'], 'permissions' => ['report.export']], ['allow' => false], // deny all others ], ], ]; }
User Model Behavior
Add UserAccessBehavior to your User ActiveRecord to call access checks directly on the model:
// In your User model: public function behaviors(): array { return [ 'access' => \malinichevvv\access\behaviors\UserAccessBehavior::class, ]; } // Usage: $user = User::findOne($id); $user->can('order.create'); // bool $user->hasRole('admin'); // bool $user->hasRole(['admin', 'super_admin']); // bool — OR $user->canAll(['order.create', 'order.view']); // ['order.create' => true, ...] $user->getPermissions(); // string[] $user->getPermissionsDetailed(); // grouped with inheritance info $user->getPermissionsForUI(); // UI-ready tree $user->getPermissionsWithSources(); // direct/inherited split $user->getRoles(); // role records (with inheritance) $user->getRoles(false); // direct roles only $user->getEffectiveRoleIds(); // int[] $user->getDirectRoleIds(); // int[] $user->assignRole($roleId); // void $user->revokeRole($roleId); // void
Managing Roles & Permissions
use malinichevvv\access\Am; // Create a permission group $groupId = Am::createPermissionGroup('Orders', 'crm', 'All order-related permissions'); // Create permissions $createId = Am::createPermission('order.create', 'Create a new order', $groupId); $deleteId = Am::createPermission('order.delete', 'Delete an order', $groupId); // Create a role $roleId = Am::createRole('Manager', 'manager', 'Manages orders and clients'); // Assign permissions to the role Am::addPermissionToRole($roleId, $createId); Am::addPermissionToRole($roleId, $deleteId); // Assign role to a user Am::assignRole($userId, $roleId); // Revoke role Am::revokeRole($userId, $roleId); // Update / delete Am::updateRole($roleId, ['name' => 'Senior Manager']); Am::deleteRole($roleId); // fails for system roles
Role Inheritance
Permissions flow upward: a child role inherits all permissions of its parent roles.
admin
└── manager (manager inherits admin's permissions)
└── operator (operator inherits manager's + admin's permissions)
// admin → manager (manager inherits admin) Am::addRoleInheritance($adminRoleId, $managerRoleId); // manager → operator Am::addRoleInheritance($managerRoleId, $operatorRoleId); // Cycle detection — throws Exception Am::canInherit($adminRoleId, $adminRoleId); // false (self) Am::canInherit($operatorRoleId, $adminRoleId); // false (would create cycle) // Query hierarchy Am::getParentRoles($managerRoleId); // direct parents Am::getParentRoles($operatorRoleId, true); // all ancestors recursively Am::getChildRoles($adminRoleId, true); // all descendants recursively Am::getRolePermissionsWithInheritance($roleId); // permissions with direct/inherited flags
Dynamic Rules
Attach a callable to any permission for contextual access decisions evaluated after the static permission check:
namespace App\Access\Rules; class OwnerRule { public static function check(int $userId, ?array $params): bool { $orderId = $params['order_id'] ?? null; if (!$orderId) { return false; } // Only the order's creator may delete it return Order::find()->where(['id' => $orderId, 'created_by' => $userId])->exists(); } }
// Register the rule Am::addDynamicRule($permissionId, 'App\Access\Rules\OwnerRule::check', 'Owner check for orders'); // Check with params — static check + dynamic rule both evaluated Am::checkAccess($userId, 'order.delete', ['order_id' => $orderId]); // Remove the rule Am::removeDynamicRule($permissionId);
Multi-Tenant Mode
Enable multiTenant = true to scope roles per company:
'access' => [ 'class' => \malinichevvv\access\AccessManager::class, 'multiTenant' => true, ],
// Roles are scoped to company_id $roleId = Am::createRole('Manager', 'manager', 'Company manager', false, false, $companyId); // Query company roles $roles = Am::getRolesByCompany($companyId); $role = Am::getRoleByCompanyWithCode($companyId, 'manager');
Analytics & Comparison
// Compare permissions between two users $diff = Am::compareUsersAccess($userId1, $userId2); // [ // 'only_user1' => ['order.delete', ...], // 'only_user2' => ['report.export', ...], // 'common' => ['order.view', 'order.create'], // 'similarity_percent' => 66.7, // ]
Console Commands
Register the controller in your console config once:
// console/config/main.php 'controllerMap' => [ 'access' => \malinichevvv\access\console\AccessController::class, ],
| Command | Description |
|---|---|
php yii access/flush-cache |
Invalidate all access:permissions and access:roles cache tags |
php yii access/list-roles |
List all roles with system/default flags |
php yii access/list-roles --company-id=5 |
Roles for a specific company (multi-tenant) |
php yii access/list-permissions |
List all permissions grouped by module |
php yii access/list-permissions --module=crm |
Permissions for a specific module |
php yii access/check 42 order.create |
Check if user #42 has order.create |
php yii access/user-roles 42 |
Show direct and inherited roles for user #42 |
php yii access/user-permissions 42 |
Show all permissions for user #42 with sources |
Configurable Cache TTLs
Cache durations are instance properties, not hardcoded constants, so you can tune them per environment:
'components' => [ 'access' => [ 'class' => \malinichevvv\access\AccessManager::class, // Defaults shown — override as needed: 'cacheDurationShort' => 1800, // 30 min — user role assignments 'cacheDurationMedium' => 3600, // 1 h — user permission sets, role details 'cacheDurationLong' => 7200, // 2 h — role hierarchy, role→permission links 'cacheDurationVeryLong' => 86400, // 24 h — dynamic rule metadata, module groups ], ],
The constants (AccessManager::CACHE_DURATION_*) remain available as documented defaults
and can be referenced in your own code.
Events
The AccessManager component extends yii\base\Component and fires events at every key point.
Attach listeners anywhere in your application bootstrap or module init().
Access check events
| Constant | Class | When |
|---|---|---|
EVENT_BEFORE_CHECK_ACCESS |
AccessCheckEvent |
Before any permission check; can short-circuit |
EVENT_AFTER_CHECK_ACCESS |
AccessCheckEvent |
After every check; carries $result |
EVENT_ACCESS_DENIED |
AccessCheckEvent |
Only when the check fails |
Super-admin bypass (short-circuit pattern):
Yii::$app->access->on( AccessManager::EVENT_BEFORE_CHECK_ACCESS, function (AccessCheckEvent $event) { if (isSuperAdmin($event->userId)) { $event->isHandled = true; // skip DB/cache entirely $event->result = true; } } );
Access denied logging:
Yii::$app->access->on( AccessManager::EVENT_ACCESS_DENIED, function (AccessCheckEvent $event) { Yii::warning( "Denied: user={$event->userId} perm={$event->permissionCode}", 'access' ); } );
Role events
| Constant | Cancellable | Properties on event |
|---|---|---|
EVENT_BEFORE_ASSIGN_ROLE |
yes | userId, roleId |
EVENT_AFTER_ASSIGN_ROLE |
— | userId, roleId |
EVENT_BEFORE_REVOKE_ROLE |
yes | userId, roleId |
EVENT_AFTER_REVOKE_ROLE |
— | userId, roleId |
EVENT_BEFORE_CREATE_ROLE |
yes | data |
EVENT_AFTER_CREATE_ROLE |
— | roleId, createdRoleId, data |
EVENT_BEFORE_UPDATE_ROLE |
yes | roleId, data |
EVENT_AFTER_UPDATE_ROLE |
— | roleId, data |
EVENT_BEFORE_DELETE_ROLE |
yes | roleId |
EVENT_AFTER_DELETE_ROLE |
— | roleId |
EVENT_BEFORE_ADD_ROLE_INHERITANCE |
yes | parentRoleId, childRoleId |
EVENT_AFTER_ADD_ROLE_INHERITANCE |
— | parentRoleId, childRoleId |
EVENT_BEFORE_REMOVE_ROLE_INHERITANCE |
yes | parentRoleId, childRoleId |
EVENT_AFTER_REMOVE_ROLE_INHERITANCE |
— | parentRoleId, childRoleId |
Cancellable example — guard system roles from deletion:
Yii::$app->access->on( AccessManager::EVENT_BEFORE_DELETE_ROLE, function (RoleEvent $event) { if ($event->roleId === MY_PROTECTED_ROLE_ID) { $event->isValid = false; // vetoes the delete } } );
After-event example — notify a user when their role changes:
Yii::$app->access->on( AccessManager::EVENT_AFTER_ASSIGN_ROLE, function (RoleEvent $event) { Notification::send($event->userId, 'Your access permissions have been updated.'); } );
Permission events
| Constant | Cancellable | Properties on event |
|---|---|---|
EVENT_BEFORE_CREATE_PERMISSION |
yes | permissionCode, data |
EVENT_AFTER_CREATE_PERMISSION |
— | permissionId, createdPermissionId, permissionCode, data |
EVENT_BEFORE_UPDATE_PERMISSION |
yes | permissionId, data |
EVENT_AFTER_UPDATE_PERMISSION |
— | permissionId, data |
EVENT_BEFORE_DELETE_PERMISSION |
yes | permissionId |
EVENT_AFTER_DELETE_PERMISSION |
— | permissionId |
EVENT_BEFORE_ADD_PERMISSION_TO_ROLE |
yes | roleId, permissionId |
EVENT_AFTER_ADD_PERMISSION_TO_ROLE |
— | roleId, permissionId |
EVENT_BEFORE_REMOVE_PERMISSION_FROM_ROLE |
yes | roleId, permissionId |
EVENT_AFTER_REMOVE_PERMISSION_FROM_ROLE |
— | roleId, permissionId |
Auto-grant to super-admin on permission creation:
Yii::$app->access->on( AccessManager::EVENT_AFTER_CREATE_PERMISSION, function (PermissionEvent $event) { Am::addPermissionToRole(SUPERADMIN_ROLE_ID, $event->createdPermissionId); } );
Block sensitive permission assignment:
Yii::$app->access->on( AccessManager::EVENT_BEFORE_ADD_PERMISSION_TO_ROLE, function (PermissionEvent $event) { $sensitive = ['payment.refund', 'user.delete', 'role.manage']; if (in_array($event->permissionCode, $sensitive, true)) { $event->isValid = false; Yii::warning("Blocked assignment of sensitive permission to role #{$event->roleId}"); } } );
Detaching listeners
$handler = function (RoleEvent $event) { ... }; Yii::$app->access->on(AccessManager::EVENT_AFTER_ASSIGN_ROLE, $handler); // Later: Yii::$app->access->off(AccessManager::EVENT_AFTER_ASSIGN_ROLE, $handler);
Database Schema
| Table | Description |
|---|---|
access_permission_groups |
Module-based groups for organising permissions |
access_permissions |
Individual permission codes |
access_roles |
Role definitions (optionally company-scoped) |
access_role_permissions |
Role ↔ permission pivot |
access_role_includes |
Role inheritance (parent → child) |
access_user_roles |
User ↔ role pivot |
access_dynamic_rules |
Callable-based contextual rules per permission |
access_audit_log |
Append-only audit trail (separate migration) |
Caching
The component uses a two-layer cache strategy:
- Per-request in-memory pool — eliminates repeated DB/cache round-trips within a single HTTP request
- Persistent cache (Redis, Memcache, etc.) — shared across requests with tag-based invalidation
All mutating operations automatically invalidate the relevant cache tags. You can also clear the request pool manually (e.g. in tests):
Am::clearRequestCache(); // or Yii::$app->access->clearRequestCache();
To disable caching entirely:
'access' => ['class' => AccessManager::class, 'enableCache' => false],
Audit Log
When enableAuditLog = true (default), every access-changing operation writes a row to access_audit_log. The table has no foreign keys to users so the log survives user deletion.
To disable:
'access' => ['class' => AccessManager::class, 'enableAuditLog' => false],
i18n
Error messages are translatable. Add to your i18n config:
'i18n' => [ 'translations' => [ 'access' => [ 'class' => 'yii\i18n\PhpMessageSource', 'basePath' => '@vendor/malinichevvv/yii2-access/src/messages', 'sourceLanguage' => 'en', ], ], ],
Supported languages: en, ru, uk. Contributions for other languages are welcome.
License
MIT