├── 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 | [](https://packagist.org/packages/yiisoft/rbac-php)
10 | [](https://packagist.org/packages/yiisoft/rbac-php)
11 | [](https://github.com/yiisoft/rbac-php/actions/workflows/build.yml)
12 | [](https://codecov.io/gh/yiisoft/rbac-php)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/rbac-php/master)
14 | [](https://github.com/yiisoft/rbac-php/actions?query=workflow%3A%22static+analysis%22)
15 | [](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 | [](https://opencollective.com/yiisoft)
373 |
374 | ## Follow updates
375 |
376 | [](https://www.yiiframework.com/)
377 | [](https://twitter.com/yiiframework)
378 | [](https://t.me/yii3en)
379 | [](https://www.facebook.com/groups/yiitalk)
380 | [](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 |
--------------------------------------------------------------------------------