├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer-require-checker.json ├── composer.json ├── rector.php └── src ├── AssignmentsStorage.php ├── ConcurrentAssignmentsStorageDecorator.php ├── ConcurrentItemsStorageDecorator.php ├── ConcurrentStorageTrait.php ├── FileStorageInterface.php ├── FileStorageTrait.php └── ItemsStorage.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Yii RBAC PHP File Storage Change Log 2 | 3 | ## 2.0.1 under development 4 | 5 | - Enh #110: Bump `yiisoft/rbac` version to `^2.1` (@vjik) 6 | - Chg #111: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@vjik) 7 | 8 | ## 2.0.0 March 07, 2024 9 | 10 | - Enh #90, 91: Use 1 file path argument for storages (@arogachev) 11 | - Chg #63, #76: Raise PHP version to 8.1 (@arogachev) 12 | - Enh #50: Save `Assignment::$createdAt` (@arogachev) 13 | - Enh #51: Save `Item::$createdAt` and `Item::$updatedAt` (@arogachev) 14 | - Enh #52: Handle concurrency when working with storages (@arogachev) 15 | - Enh #63: Improve performance (@arogachev) 16 | - Enh #70, #94: Sync with base package (implement interface methods) (@arogachev) 17 | - Enh #76: Use simple storages for items and assignments from the base `rbac` package (@arogachev) 18 | - Enh #77: Use snake case for item attribute names (ease migration from Yii 2) (@arogachev) 19 | - Enh #87: Move handling same names during renaming item in `AssignmentsStorage` to base package (@arogachev) 20 | 21 | ## 1.0.0 April 08, 2022 22 | 23 | - Initial release. 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

Yii RBAC PHP File Storage

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/rbac-php/v)](https://packagist.org/packages/yiisoft/rbac-php) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/rbac-php/downloads)](https://packagist.org/packages/yiisoft/rbac-php) 11 | [![Build status](https://github.com/yiisoft/rbac-php/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/rbac-php/actions/workflows/build.yml) 12 | [![codecov](https://codecov.io/gh/yiisoft/rbac-php/graph/badge.svg?token=YU8LVBNCQ8)](https://codecov.io/gh/yiisoft/rbac-php) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Frbac-php%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/rbac-php/master) 14 | [![static analysis](https://github.com/yiisoft/rbac-php/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/rbac-php/actions?query=workflow%3A%22static+analysis%22) 15 | [![type-coverage](https://shepherd.dev/github/yiisoft/rbac-php/coverage.svg)](https://shepherd.dev/github/yiisoft/rbac-php) 16 | 17 | This package provides PHP file-based storage for [RBAC (Role-Based Access Control)](https://github.com/yiisoft/rbac) 18 | package. 19 | 20 | ## Requirements 21 | 22 | - PHP 8.1 or higher. 23 | 24 | ## Installation 25 | 26 | The package could be installed with [Composer](https://getcomposer.org): 27 | 28 | ```shell 29 | composer require yiisoft/rbac-php 30 | ``` 31 | 32 | See [yiisoft/rbac](https://github.com/yiisoft/rbac) for RBAC package installation instructions. 33 | 34 | ## General usage 35 | 36 | The storage is suitable for authorization data that is not too big (for example, the authorization data for a personal 37 | blog system) or for fairly static RBAC hierarchy. 38 | 39 | Authorization data is stored in PHP files. PHP should be able to read and write these files. Non-existing files will be 40 | created automatically on any write operation. 41 | 42 | ### Using storages 43 | 44 | The storages are not intended to be used directly. Instead, use them with `Manager` from 45 | [Yii RBAC](https://github.com/yiisoft/rbac) package: 46 | 47 | ```php 48 | use Yiisoft\Rbac\Manager; 49 | use Yiisoft\Rbac\Permission; 50 | use Yiisoft\Rbac\Php\AssignmentsStorage; 51 | use Yiisoft\Rbac\Php\ItemsStorage; 52 | use Yiisoft\Rbac\RuleFactoryInterface; 53 | 54 | $directory = __DIR__ . '/rbac'; 55 | $itemsStorage = new ItemsStorage($directory . '/items.php'); 56 | $assignmentsStorage = new AssignmentsStorage($directory . '/assignments.php'); 57 | /** @var RuleFactoryInterface $rulesContainer */ 58 | $manager = new Manager( 59 | itemsStorage: $itemsStorage, 60 | assignmentsStorage: $assignmentsStorage, 61 | // Requires https://github.com/yiisoft/rbac-rules-container or other compatible factory. 62 | ruleFactory: $rulesContainer, 63 | ), 64 | $manager->addPermission(new Permission('posts.create')); 65 | ``` 66 | 67 | > Note that it's not necessary to use both PHP storages. Combining different implementations is possible. A quite 68 | > popular case is to manage items via PHP file while store assignments in database (see 69 | > [Cycle](https://github.com/yiisoft/rbac-cycle-db) and [Yiisoft DB](https://github.com/yiisoft/rbac-db) 70 | > implementations). 71 | 72 | More examples can be found in [Yii RBAC](https://github.com/yiisoft/rbac) documentation. 73 | 74 | ### File structure 75 | 76 | In case you decide to manually edit the files, make sure to keep the following structure. 77 | 78 | #### Items 79 | 80 | Required and optional fields: 81 | 82 | ```php 83 | return [ 84 | [ 85 | 'name' => 'posts.update', 86 | 'description' => 'Update a post', // Optional 87 | 'rule_name' => 'is_author', // Optional 88 | 'type' => 'permission', // or 'role' 89 | 'created_at' => 1683707079, // UNIX timestamp, optional 90 | 'updated_at' => 1683707079, // UNIX timestamp, optional 91 | ], 92 | ]; 93 | ``` 94 | 95 | While it's recommended to maintain created and updated timestamps, if any is missing, the file modification time will 96 | be used instead as a fallback. 97 | 98 | The structure for an item with children: 99 | 100 | ```php 101 | return [ 102 | [ 103 | 'name' => 'posts.redactor', 104 | 'type' => 'role', 105 | 'created_at' => 1683707079, 106 | 'updated_at' => 1683707079, 107 | 'children' => [ 108 | 'posts.viewer', 109 | 'posts.create', 110 | 'posts.update', 111 | ], 112 | ], 113 | ]; 114 | ``` 115 | 116 | The complete example for managing posts: 117 | 118 | ```php 119 | return [ 120 | [ 121 | 'name' => 'posts.admin', 122 | 'type' => 'role', 123 | 'created_at' => 1683707079, 124 | 'updated_at' => 1683707079, 125 | 'children' => [ 126 | 'posts.redactor', 127 | 'posts.delete', 128 | 'posts.update.all', 129 | ], 130 | ], 131 | [ 132 | 'name' => 'posts.redactor', 133 | 'type' => 'role', 134 | 'created_at' => 1683707079, 135 | 'updated_at' => 1683707079, 136 | 'children' => [ 137 | 'posts.viewer', 138 | 'posts.create', 139 | 'posts.update', 140 | ], 141 | ], 142 | [ 143 | 'name' => 'posts.viewer', 144 | 'type' => 'role', 145 | 'created_at' => 1683707079, 146 | 'updated_at' => 1683707079, 147 | 'children' => [ 148 | 'posts.view', 149 | ], 150 | ], 151 | [ 152 | 'name' => 'posts.view', 153 | 'type' => 'permission', 154 | 'created_at' => 1683707079, 155 | 'updated_at' => 1683707079, 156 | ], 157 | [ 158 | 'name' => 'posts.create', 159 | 'type' => 'permission', 160 | 'created_at' => 1683707079, 161 | 'updated_at' => 1683707079, 162 | ], 163 | [ 164 | 'name' => 'posts.update', 165 | 'rule_name' => 'is_author', 166 | 'type' => 'permission', 167 | 'created_at' => 1683707079, 168 | 'updated_at' => 1683707079, 169 | ], 170 | [ 171 | 'name' => 'posts.delete', 172 | 'type' => 'permission', 173 | 'created_at' => 1683707079, 174 | 'updated_at' => 1683707079, 175 | ], 176 | [ 177 | 'name' => 'posts.update.all', 178 | 'type' => 'permission', 179 | 'created_at' => 1683707079, 180 | 'updated_at' => 1683707079, 181 | ], 182 | ]; 183 | ``` 184 | 185 | #### Assignments 186 | 187 | ```php 188 | return [ 189 | [ 190 | 'item_name' => 'posts.redactor', 191 | 'user_id' => 'john', 192 | 'created_at' => 1683707079, // Optional 193 | ], 194 | // ... 195 | [ 196 | 'item_name' => 'posts.admin', 197 | 'user_id' => 'jack', 198 | 'created_at' => 1683707079, 199 | ], 200 | ]; 201 | ``` 202 | 203 | While it's recommended to maintain created timestamps, if it is missing, the file modification time will be used 204 | instead as a fallback. 205 | 206 | ### Concurrency 207 | 208 | By default, working with PHP storage does not support concurrency. This might be OK if you store its files under VCS for 209 | example. If your scenario is different and, let's say, some kind of web interface is used - then, to enable concurrency, 210 | do not use the storage directly - wrap it with decorator instead: 211 | 212 | ```php 213 | use Yiisoft\Rbac\Manager; 214 | use Yiisoft\Rbac\Permission; 215 | use Yiisoft\Rbac\Php\AssignmentsStorage; 216 | use Yiisoft\Rbac\Php\ConcurrentAssignmentsStorageDecorator; 217 | use Yiisoft\Rbac\Php\ConcurrentItemsStorageDecorator; 218 | use Yiisoft\Rbac\Php\ItemsStorage; 219 | use Yiisoft\Rbac\RuleFactoryInterface; 220 | 221 | $directory = __DIR__ . DIRECTORY_SEPARATOR . 'rbac'; 222 | $itemsSstorage = new ConcurrentItemsStorageDecorator(ItemsStorage($directory)); 223 | $assignmentsStorage = new ConcurrentAssignmentsStorageDecorator(AssignmentsStorage($directory)); 224 | /** @var RuleFactoryInterface $rulesContainer */ 225 | $manager = new Manager( 226 | itemsStorage: $itemsStorage, 227 | assignmentsStorage: $assignmentsStorage, 228 | // Requires https://github.com/yiisoft/rbac-rules-container or other compatible factory. 229 | ruleFactory: $rulesContainer, 230 | ), 231 | ``` 232 | 233 | > Note that it will have an impact on performance so don't use it unless you really have to. 234 | 235 | #### Configuring file updated time 236 | 237 | A closure can be used to customize getting file modification time: 238 | 239 | ```php 240 | use Yiisoft\Rbac\Php\AssignmentsStorage; 241 | use Yiisoft\Rbac\Php\ItemsStorage; 242 | 243 | $directory = __DIR__ . '/rbac', 244 | $getFileUpdatedAt = static fn (string $filePath): int|false => @filemtime($filePath) 245 | $itemsStorage = new ItemsStorage( 246 | $directory . '/items.php', 247 | getFileUpdatedAt: static fn (string $filePath): int|false => @filemtime($filePath), 248 | ); 249 | $itemsStorage = new AssignmentsStorage( 250 | $directory . '/assignments.php', 251 | getFileUpdatedAt: static fn (string $filePath): int|false => @filemtime($filePath), 252 | ); 253 | ``` 254 | 255 | This is useful for 2 things: 256 | 257 | - Using for empty timestamps when files are edited manually. 258 | - Detection of file changes when concurrency is enabled. This helps to optimize perfomance by preventing of unnecessary 259 | loads (when file contents has not been changed). 260 | 261 | ### Syncing storages manually 262 | 263 | The storages stay synced thanks to manager, but there can be situations where you need to sync them manually. One of 264 | them is [editing storage manually](https://github.com/yiisoft/rbac-php/?tab=readme-ov-file#file-structure). 265 | 266 | Let's say PHP files are used for both items and assignments and some items were deleted. 267 | 268 | ```diff 269 | return [ 270 | [ 271 | 'name' => 'posts.admin', 272 | 'type' => 'role', 273 | 'created_at' => 1683707079, 274 | 'updated_at' => 1683707079, 275 | 'children' => [ 276 | 'posts.redactor', 277 | 'posts.delete', 278 | 'posts.update.all', 279 | ], 280 | ], 281 | - [ 282 | - 'name' => 'posts.redactor', 283 | - 'type' => 'role', 284 | - 'created_at' => 1683707079, 285 | - 'updated_at' => 1683707079, 286 | - 'children' => [ 287 | - 'posts.viewer', 288 | - 'posts.create', 289 | - 'posts.update', 290 | - ], 291 | - ], 292 | [ 293 | 'name' => 'posts.viewer', 294 | 'type' => 'role', 295 | 'created_at' => 1683707079, 296 | 'updated_at' => 1683707079, 297 | 'children' => [ 298 | 'posts.view', 299 | ], 300 | ], 301 | [ 302 | 'name' => 'posts.view', 303 | 'type' => 'permission', 304 | 'created_at' => 1683707079, 305 | 'updated_at' => 1683707079, 306 | ], 307 | [ 308 | 'name' => 'posts.create', 309 | 'type' => 'permission', 310 | 'created_at' => 1683707079, 311 | 'updated_at' => 1683707079, 312 | ], 313 | - [ 314 | - 'name' => 'posts.update', 315 | - 'rule_name' => 'is_author', 316 | - 'type' => 'permission', 317 | - 'created_at' => 1683707079, 318 | - 'updated_at' => 1683707079, 319 | - ], 320 | [ 321 | 'name' => 'posts.delete', 322 | 'type' => 'permission', 323 | 'created_at' => 1683707079, 324 | 'updated_at' => 1683707079, 325 | ], 326 | [ 327 | 'name' => 'posts.update.all', 328 | 'type' => 'permission', 329 | 'created_at' => 1683707079, 330 | 'updated_at' => 1683707079, 331 | ], 332 | ]; 333 | ``` 334 | 335 | Then related entries in assignments storage needs to be deleted as well: 336 | 337 | ```diff 338 | return [ 339 | - [ 340 | - 'item_name' => 'posts.redactor', 341 | - 'user_id' => 'john', 342 | - 'created_at' => 1683707079, 343 | - ], 344 | [ 345 | 'item_name' => 'posts.admin', 346 | 'user_id' => 'jack', 347 | 'created_at' => 1683707079, 348 | ], 349 | ]; 350 | ``` 351 | 352 | When using database as a second storage, this can be done within a migration. Depending on chosen implementation, refer 353 | to either [RBAC Cycle example](https://github.com/yiisoft/rbac-cycle-db?tab=readme-ov-file#syncing-storages-manually) or 354 | [RBAC DB example](https://github.com/yiisoft/rbac-db?tab=readme-ov-file#syncing-storages-manually). 355 | 356 | ## Documentation 357 | 358 | - [Internals](docs/internals.md) 359 | 360 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that. 361 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). 362 | 363 | ## License 364 | 365 | The Yii RBAC PHP File Storage is free software. It is released under the terms of the BSD License. 366 | Please see [`LICENSE`](./LICENSE.md) for more information. 367 | 368 | Maintained by [Yii Software](https://www.yiiframework.com/). 369 | 370 | ## Support the project 371 | 372 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 373 | 374 | ## Follow updates 375 | 376 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 377 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 378 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 379 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 380 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 381 | -------------------------------------------------------------------------------- /composer-require-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist" : [ 3 | "opcache_invalidate" 4 | ], 5 | "php-core-extensions" : [ 6 | "Core", 7 | "date", 8 | "json", 9 | "hash", 10 | "pcre", 11 | "Phar", 12 | "Reflection", 13 | "SPL", 14 | "random", 15 | "standard" 16 | ], 17 | "scan-files" : [] 18 | } 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/rbac-php", 3 | "type": "library", 4 | "description": "Yii RBAC PHP File Storage", 5 | "keywords": [ 6 | "yii", 7 | "rbac", 8 | "storage" 9 | ], 10 | "homepage": "https://www.yiiframework.com/", 11 | "license": "BSD-3-Clause", 12 | "support": { 13 | "issues": "https://github.com/yiisoft/rbac/issues", 14 | "forum": "https://www.yiiframework.com/forum/", 15 | "wiki": "https://www.yiiframework.com/wiki/", 16 | "irc": "ircs://irc.libera.chat:6697/yii", 17 | "chat": "https://t.me/yii3en", 18 | "source": "https://github.com/yiisoft/rbac" 19 | }, 20 | "funding": [ 21 | { 22 | "type": "opencollective", 23 | "url": "https://opencollective.com/yiisoft" 24 | }, 25 | { 26 | "type": "github", 27 | "url": "https://github.com/sponsors/yiisoft" 28 | } 29 | ], 30 | "require": { 31 | "php": "8.1 - 8.4", 32 | "yiisoft/rbac": "^2.1", 33 | "yiisoft/var-dumper": "^1.7" 34 | }, 35 | "require-dev": { 36 | "maglnet/composer-require-checker": "^4.7.1", 37 | "phpunit/phpunit": "^10.5.45", 38 | "psr/clock": "^1.0", 39 | "rector/rector": "^2.0.11", 40 | "roave/infection-static-analysis-plugin": "^1.35", 41 | "spatie/phpunit-watcher": "^1.24", 42 | "vimeo/psalm": "^5.26.1 || ^6.10", 43 | "yiisoft/files": "^1.0.2" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "Yiisoft\\Rbac\\Php\\": "src" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "Yiisoft\\Rbac\\Php\\Tests\\": "tests", 53 | "Yiisoft\\Rbac\\Tests\\": "vendor/yiisoft/rbac/tests" 54 | } 55 | }, 56 | "config": { 57 | "sort-packages": true, 58 | "allow-plugins": { 59 | "infection/extension-installer": true, 60 | "composer/package-versions-deprecated": true 61 | } 62 | }, 63 | "scripts": { 64 | "test": "phpunit --testdox --no-interaction", 65 | "test-watch": "phpunit-watcher watch" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 12 | __DIR__ . '/src', 13 | __DIR__ . '/tests', 14 | ]) 15 | ->withPhpSets(php80: true) 16 | ->withRules([ 17 | InlineConstructorDefaultToPropertyRector::class, 18 | ]) 19 | ->withSkip([ 20 | ClosureToArrowFunctionRector::class, 21 | AddParamBasedOnParentClassMethodRector::class, 22 | ]); 23 | -------------------------------------------------------------------------------- /src/AssignmentsStorage.php: -------------------------------------------------------------------------------- 1 | initFileProperties($filePath, $getFileUpdatedAt); 25 | $this->load(); 26 | } 27 | 28 | public function add(Assignment $assignment): void 29 | { 30 | parent::add($assignment); 31 | $this->save(); 32 | } 33 | 34 | public function renameItem(string $oldName, string $newName): void 35 | { 36 | parent::renameItem($oldName, $newName); 37 | $this->save(); 38 | } 39 | 40 | public function remove(string $itemName, string $userId): void 41 | { 42 | if (!$this->exists($itemName, $userId)) { 43 | return; 44 | } 45 | 46 | parent::remove($itemName, $userId); 47 | $this->save(); 48 | } 49 | 50 | public function removeByUserId(string $userId): void 51 | { 52 | parent::removeByUserId($userId); 53 | $this->save(); 54 | } 55 | 56 | public function removeByItemName(string $itemName): void 57 | { 58 | parent::removeByItemName($itemName); 59 | $this->save(); 60 | } 61 | 62 | public function clear(): void 63 | { 64 | parent::clear(); 65 | $this->save(); 66 | } 67 | 68 | public function load(): void 69 | { 70 | parent::clear(); 71 | 72 | /** @psalm-var list $assignments */ 73 | $assignments = $this->loadFromFile($this->filePath); 74 | if (empty($assignments)) { 75 | return; 76 | } 77 | 78 | $fileUpdatedAt = $this->getFileUpdatedAt(); 79 | foreach ($assignments as $assignment) { 80 | /** @psalm-suppress InvalidPropertyAssignmentValue */ 81 | $this->assignments[$assignment['user_id']][$assignment['item_name']] = new Assignment( 82 | userId: $assignment['user_id'], 83 | itemName: $assignment['item_name'], 84 | createdAt: $assignment['created_at'] ?? $fileUpdatedAt, 85 | ); 86 | } 87 | } 88 | 89 | private function save(): void 90 | { 91 | $assignmentData = []; 92 | foreach ($this->assignments as $userAssignments) { 93 | foreach ($userAssignments as $userAssignment) { 94 | $assignmentData[] = $userAssignment->getAttributes(); 95 | } 96 | } 97 | 98 | $this->saveToFile($assignmentData); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/ConcurrentAssignmentsStorageDecorator.php: -------------------------------------------------------------------------------- 1 | load(); 24 | 25 | return $this->storage->getAll(); 26 | } 27 | 28 | public function getByUserId(string $userId): array 29 | { 30 | $this->load(); 31 | 32 | return $this->storage->getByUserId($userId); 33 | } 34 | 35 | public function getByItemNames(array $itemNames): array 36 | { 37 | $this->load(); 38 | 39 | return $this->storage->getByItemNames($itemNames); 40 | } 41 | 42 | public function get(string $itemName, string $userId): ?Assignment 43 | { 44 | $this->load(); 45 | 46 | return $this->storage->get($itemName, $userId); 47 | } 48 | 49 | public function exists(string $itemName, string $userId): bool 50 | { 51 | $this->load(); 52 | 53 | return $this->storage->exists($itemName, $userId); 54 | } 55 | 56 | public function userHasItem(string $userId, array $itemNames): bool 57 | { 58 | $this->load(); 59 | 60 | return $this->storage->userHasItem($userId, $itemNames); 61 | } 62 | 63 | public function filterUserItemNames(string $userId, array $itemNames): array 64 | { 65 | $this->load(); 66 | 67 | return $this->storage->filterUserItemNames($userId, $itemNames); 68 | } 69 | 70 | public function add(Assignment $assignment): void 71 | { 72 | $this->load(); 73 | $this->storage->add($assignment); 74 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 75 | } 76 | 77 | public function hasItem(string $name): bool 78 | { 79 | $this->load(); 80 | 81 | return $this->storage->hasItem($name); 82 | } 83 | 84 | public function renameItem(string $oldName, string $newName): void 85 | { 86 | $this->load(); 87 | $this->storage->renameItem($oldName, $newName); 88 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 89 | } 90 | 91 | public function remove(string $itemName, string $userId): void 92 | { 93 | $this->load(); 94 | $this->storage->remove($itemName, $userId); 95 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 96 | } 97 | 98 | public function removeByUserId(string $userId): void 99 | { 100 | $this->load(); 101 | $this->storage->removeByUserId($userId); 102 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 103 | } 104 | 105 | public function removeByItemName(string $itemName): void 106 | { 107 | $this->load(); 108 | $this->storage->removeByItemName($itemName); 109 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 110 | } 111 | 112 | public function clear(): void 113 | { 114 | $this->storage->clear(); 115 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 116 | } 117 | 118 | public function load(): void 119 | { 120 | $this->loadInternal($this->storage); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/ConcurrentItemsStorageDecorator.php: -------------------------------------------------------------------------------- 1 | storage->clear(); 25 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 26 | } 27 | 28 | public function getAll(): array 29 | { 30 | $this->load(); 31 | 32 | return $this->storage->getAll(); 33 | } 34 | 35 | public function getByNames(array $names): array 36 | { 37 | $this->load(); 38 | 39 | return $this->storage->getByNames($names); 40 | } 41 | 42 | public function get(string $name): Permission|Role|null 43 | { 44 | $this->load(); 45 | 46 | return $this->storage->get($name); 47 | } 48 | 49 | public function exists(string $name): bool 50 | { 51 | $this->load(); 52 | 53 | return $this->storage->exists($name); 54 | } 55 | 56 | public function roleExists(string $name): bool 57 | { 58 | $this->load(); 59 | 60 | return $this->storage->roleExists($name); 61 | } 62 | 63 | public function add(Permission|Role $item): void 64 | { 65 | $this->load(); 66 | $this->storage->add($item); 67 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 68 | } 69 | 70 | public function update(string $name, Permission|Role $item): void 71 | { 72 | $this->load(); 73 | $this->storage->update($name, $item); 74 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 75 | } 76 | 77 | public function remove(string $name): void 78 | { 79 | $this->load(); 80 | $this->storage->remove($name); 81 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 82 | } 83 | 84 | public function getRoles(): array 85 | { 86 | $this->load(); 87 | 88 | return $this->storage->getRoles(); 89 | } 90 | 91 | public function getRolesByNames(array $names): array 92 | { 93 | $this->load(); 94 | 95 | return $this->storage->getRolesByNames($names); 96 | } 97 | 98 | public function getRole(string $name): ?Role 99 | { 100 | $this->load(); 101 | 102 | return $this->storage->getRole($name); 103 | } 104 | 105 | public function clearRoles(): void 106 | { 107 | $this->load(); 108 | $this->storage->clearRoles(); 109 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 110 | } 111 | 112 | public function getPermissions(): array 113 | { 114 | $this->load(); 115 | 116 | return $this->storage->getPermissions(); 117 | } 118 | 119 | public function getPermissionsByNames(array $names): array 120 | { 121 | $this->load(); 122 | 123 | return $this->storage->getPermissionsByNames($names); 124 | } 125 | 126 | public function getPermission(string $name): ?Permission 127 | { 128 | $this->load(); 129 | 130 | return $this->storage->getPermission($name); 131 | } 132 | 133 | public function clearPermissions(): void 134 | { 135 | $this->load(); 136 | $this->storage->clearPermissions(); 137 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 138 | } 139 | 140 | public function getParents(string $name): array 141 | { 142 | $this->load(); 143 | 144 | return $this->storage->getParents($name); 145 | } 146 | 147 | public function getHierarchy(string $name): array 148 | { 149 | $this->load(); 150 | 151 | return $this->storage->getHierarchy($name); 152 | } 153 | 154 | public function getDirectChildren(string $name): array 155 | { 156 | $this->load(); 157 | 158 | return $this->storage->getDirectChildren($name); 159 | } 160 | 161 | public function getAllChildren(string|array $names): array 162 | { 163 | $this->load(); 164 | 165 | return $this->storage->getAllChildren($names); 166 | } 167 | 168 | public function getAllChildRoles(string|array $names): array 169 | { 170 | $this->load(); 171 | 172 | return $this->storage->getAllChildRoles($names); 173 | } 174 | 175 | public function getAllChildPermissions(string|array $names): array 176 | { 177 | $this->load(); 178 | 179 | return $this->storage->getAllChildPermissions($names); 180 | } 181 | 182 | public function hasChildren(string $name): bool 183 | { 184 | $this->load(); 185 | 186 | return $this->storage->hasChildren($name); 187 | } 188 | 189 | public function hasChild(string $parentName, string $childName): bool 190 | { 191 | $this->load(); 192 | 193 | return $this->storage->hasChild($parentName, $childName); 194 | } 195 | 196 | public function hasDirectChild(string $parentName, string $childName): bool 197 | { 198 | $this->load(); 199 | 200 | return $this->storage->hasDirectChild($parentName, $childName); 201 | } 202 | 203 | public function addChild(string $parentName, string $childName): void 204 | { 205 | $this->load(); 206 | $this->storage->addChild($parentName, $childName); 207 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 208 | } 209 | 210 | public function removeChild(string $parentName, string $childName): void 211 | { 212 | $this->load(); 213 | $this->storage->removeChild($parentName, $childName); 214 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 215 | } 216 | 217 | public function removeChildren(string $parentName): void 218 | { 219 | $this->load(); 220 | $this->storage->removeChildren($parentName); 221 | $this->currentFileUpdatedAt = $this->getFileUpdatedAt(); 222 | } 223 | 224 | public function load(): void 225 | { 226 | $this->loadInternal($this->storage); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/ConcurrentStorageTrait.php: -------------------------------------------------------------------------------- 1 | storage->getFileUpdatedAt(); 16 | } 17 | 18 | private function loadInternal(FileStorageInterface $storage): void 19 | { 20 | try { 21 | $fileUpdatedAt = $storage->getFileUpdatedAt(); 22 | } catch (RuntimeException) { 23 | return; 24 | } 25 | 26 | if ($this->currentFileUpdatedAt === $fileUpdatedAt) { 27 | return; 28 | } 29 | 30 | $storage->load(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/FileStorageInterface.php: -------------------------------------------------------------------------------- 1 | getFileUpdatedAt; 24 | $fileUpdatedAt = $getFileUpdatedAt($this->filePath); 25 | if (!is_int($fileUpdatedAt)) { 26 | throw new RuntimeException('getFileUpdatedAt callable must return a UNIX timestamp.'); 27 | } 28 | 29 | return $fileUpdatedAt; 30 | } 31 | 32 | /** 33 | * Loads the authorization data from a PHP script file. 34 | * 35 | * @param string $file The file path. 36 | * 37 | * @return array The authorization data. 38 | * @psalm-suppress MixedInferredReturnType 39 | * @link https://github.com/yiisoft/rbac-php/issues/72 40 | * 41 | * @see saveToFile() 42 | */ 43 | private function loadFromFile(string $filePath): array 44 | { 45 | if (is_file($filePath)) { 46 | /** 47 | * @psalm-suppress MixedReturnStatement 48 | * @link https://github.com/yiisoft/rbac-php/issues/72 49 | */ 50 | return require $filePath; 51 | } 52 | 53 | return []; 54 | } 55 | 56 | /** 57 | * Saves the authorization data to a PHP script file. 58 | * 59 | * @param array $data The authorization data. 60 | * @param string $filePath The file path. 61 | * 62 | * @see loadFromFile() 63 | */ 64 | private function saveToFile(array $data): void 65 | { 66 | $directory = dirname($this->filePath); 67 | 68 | if (!is_dir($directory)) { 69 | set_error_handler(static function (int $errorNumber, string $errorString) use ($directory): void { 70 | if (!is_dir($directory)) { 71 | throw new RuntimeException( 72 | sprintf('Failed to create directory "%s". ', $directory) . $errorString, 73 | $errorNumber, 74 | ); 75 | } 76 | }); 77 | mkdir($directory, permissions: 0775, recursive: true); 78 | restore_error_handler(); 79 | } 80 | 81 | file_put_contents($this->filePath, "export() . ";\n", LOCK_EX); 82 | $this->invalidateScriptCache(); 83 | } 84 | 85 | /** 86 | * Invalidates precompiled script cache (such as OPCache) for the given file. 87 | * 88 | * @infection-ignore-all 89 | */ 90 | private function invalidateScriptCache(): void 91 | { 92 | if (function_exists('opcache_invalidate')) { 93 | opcache_invalidate($this->filePath, force: true); 94 | } 95 | } 96 | 97 | private function initFileProperties(string $filePath, ?callable $getFileUpdatedAt): void 98 | { 99 | $this->filePath = $filePath; 100 | $this->getFileUpdatedAt = $getFileUpdatedAt ?? static fn (string $filePath): int|false => @filemtime($filePath); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/ItemsStorage.php: -------------------------------------------------------------------------------- 1 | initFileProperties($filePath, $getFileUpdatedAt); 32 | $this->load(); 33 | } 34 | 35 | public function clear(): void 36 | { 37 | parent::clear(); 38 | $this->save(); 39 | } 40 | 41 | public function add(Permission|Role $item): void 42 | { 43 | parent::add($item); 44 | $this->save(); 45 | } 46 | 47 | public function remove(string $name): void 48 | { 49 | parent::remove($name); 50 | $this->save(); 51 | } 52 | 53 | public function addChild(string $parentName, string $childName): void 54 | { 55 | parent::addChild($parentName, $childName); 56 | $this->save(); 57 | } 58 | 59 | public function removeChild(string $parentName, string $childName): void 60 | { 61 | if (!$this->hasDirectChild($parentName, $childName)) { 62 | return; 63 | } 64 | 65 | parent::removeChild($parentName, $childName); 66 | 67 | $this->save(); 68 | } 69 | 70 | public function removeChildren(string $parentName): void 71 | { 72 | if (!$this->hasChildren($parentName)) { 73 | return; 74 | } 75 | 76 | parent::removeChildren($parentName); 77 | 78 | $this->save(); 79 | } 80 | 81 | public function load(): void 82 | { 83 | parent::clear(); 84 | 85 | /** @psalm-var array $items */ 86 | $items = $this->loadFromFile($this->filePath); 87 | if (empty($items)) { 88 | return; 89 | } 90 | 91 | $fileUpdatedAt = $this->getFileUpdatedAt(); 92 | foreach ($items as $item) { 93 | $this->items[$item['name']] = $this 94 | ->getInstanceFromAttributes($item) 95 | ->withCreatedAt($item['created_at'] ?? $fileUpdatedAt) 96 | ->withUpdatedAt($item['updated_at'] ?? $fileUpdatedAt); 97 | } 98 | 99 | foreach ($items as $item) { 100 | foreach ($item['children'] ?? [] as $childName) { 101 | if ($this->hasItem($childName)) { 102 | $this->children[$item['name']][$childName] = $this->items[$childName]; 103 | } 104 | } 105 | } 106 | } 107 | 108 | private function save(): void 109 | { 110 | $items = []; 111 | foreach ($this->items as $name => $item) { 112 | $data = array_filter($item->getAttributes()); 113 | if ($this->hasChildren($name)) { 114 | foreach ($this->getDirectChildren($name) as $child) { 115 | $data['children'][] = $child->getName(); 116 | } 117 | } 118 | 119 | $items[] = $data; 120 | } 121 | 122 | $this->saveToFile($items); 123 | } 124 | 125 | private function hasItem(string $name): bool 126 | { 127 | return isset($this->items[$name]); 128 | } 129 | 130 | /** 131 | * @psalm-param Item::TYPE_* $type 132 | * 133 | * @psalm-return ($type is Item::TYPE_PERMISSION ? Permission : Role) 134 | */ 135 | private function getInstanceByTypeAndName(string $type, string $name): Permission|Role 136 | { 137 | return $type === Item::TYPE_PERMISSION ? new Permission($name) : new Role($name); 138 | } 139 | 140 | /** 141 | * @psalm-param RawItem $attributes 142 | */ 143 | private function getInstanceFromAttributes(array $attributes): Permission|Role 144 | { 145 | $item = $this->getInstanceByTypeAndName($attributes['type'], $attributes['name']); 146 | 147 | $description = $attributes['description'] ?? null; 148 | if ($description !== null) { 149 | $item = $item->withDescription($description); 150 | } 151 | 152 | $ruleName = $attributes['rule_name'] ?? null; 153 | if ($ruleName !== null) { 154 | $item = $item->withRuleName($ruleName); 155 | } 156 | 157 | return $item; 158 | } 159 | } 160 | --------------------------------------------------------------------------------