diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index a17aa78..8cecc8a 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -31,5 +31,5 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.0', '8.1'] + ['8.1'] extensions: uopz diff --git a/.github/workflows/mssql.yml b/.github/workflows/mssql.yml index e90524b..6c7ef27 100644 --- a/.github/workflows/mssql.yml +++ b/.github/workflows/mssql.yml @@ -26,7 +26,7 @@ jobs: name: PHP ${{ matrix.php }}-mssql-${{ matrix.mssql }} env: - extensions: pdo, pdo_sqlsrv-5.10.1, uopz + extensions: pdo, pdo_sqlsrv-5.11.1, uopz runs-on: ${{ matrix.os }} @@ -36,7 +36,6 @@ jobs: - ubuntu-latest php: - - 8.0 - 8.1 mssql: @@ -95,7 +94,7 @@ jobs: run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - name: Install db-mssql - run: composer require --dev yiisoft/db-mssql --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + run: composer require --dev "yiisoft/db-mssql:^1.1" --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - name: Run tests with phpunit run: vendor/bin/phpunit --testsuite Mssql --coverage-clover=coverage.xml --colors=always diff --git a/.github/workflows/mysql.yml b/.github/workflows/mysql.yml index 6023822..4b28b3c 100644 --- a/.github/workflows/mysql.yml +++ b/.github/workflows/mysql.yml @@ -36,7 +36,6 @@ jobs: - ubuntu-latest php: - - 8.0 - 8.1 mysql: @@ -85,7 +84,7 @@ jobs: run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - name: Install db-mysql - run: composer require --dev yiisoft/db-mysql --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + run: composer require --dev "yiisoft/db-mysql:^1.1" --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - name: Run tests with phpunit run: vendor/bin/phpunit --testsuite Mysql --coverage-clover=coverage.xml --colors=always diff --git a/.github/workflows/oracle.yml b/.github/workflows/oracle.yml index 9442c2c..a39b855 100644 --- a/.github/workflows/oracle.yml +++ b/.github/workflows/oracle.yml @@ -37,7 +37,6 @@ jobs: - ubuntu-latest php: - - 8.0 - 8.1 - 8.2 @@ -91,7 +90,7 @@ jobs: run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - name: Install db-oracle - run: composer require --dev yiisoft/db-oracle --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + run: composer require --dev "yiisoft/db-oracle:^1.2" --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - name: Run tests with phpunit run: vendor/bin/phpunit --testsuite Oracle --coverage-clover=coverage.xml --colors=always diff --git a/.github/workflows/pgsql.yml b/.github/workflows/pgsql.yml index 233ba53..cca3251 100644 --- a/.github/workflows/pgsql.yml +++ b/.github/workflows/pgsql.yml @@ -36,7 +36,6 @@ jobs: - ubuntu-latest php: - - 8.0 - 8.1 pgsql: @@ -90,7 +89,7 @@ jobs: run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - name: Install db-pgsql - run: composer require --dev yiisoft/db-pgsql --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + run: composer require --dev "yiisoft/db-pgsql:^1.2" --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - name: Run tests with phpunit run: vendor/bin/phpunit --testsuite Pgsql --coverage-clover=coverage.xml --colors=always diff --git a/.github/workflows/sqlite.yml b/.github/workflows/sqlite.yml index 0ddfc15..f7b72aa 100644 --- a/.github/workflows/sqlite.yml +++ b/.github/workflows/sqlite.yml @@ -34,7 +34,6 @@ jobs: - windows-latest php: - - 8.0 - 8.1 - 8.2 diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 8ff1572..651b627 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -28,5 +28,5 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.0', '8.1'] + ['8.1'] extensions: uopz diff --git a/.scrutinizer.yml b/.scrutinizer.yml index f86aaa7..0b8d28f 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -10,7 +10,7 @@ build: environment: php: - version: 8.0.11 + version: 8.1 ini: "xdebug.mode": coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index 445e0a7..b33353b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - `userHasItem()`. (@arogachev) - Bug #54: Fix ignoring of using `Assignment::$createdAt` in `AssignmentsStorage::add()` (@arogachev) +- Chg #?: Raise PHP version to 8.1 (@arogachev) ## 1.0.0 April 20, 2023 diff --git a/composer.json b/composer.json index 46ab40c..7b1c01c 100644 --- a/composer.json +++ b/composer.json @@ -21,15 +21,15 @@ "minimum-stability": "dev", "require": { "ext-pdo": "*", - "php": "^8.0", - "yiisoft/db": "^1.0", - "yiisoft/rbac": "dev-master" + "php": "^8.1", + "yiisoft/db": "^1.2", + "yiisoft/rbac": "dev-master#cba39d8471677970282fdad302271c858f5c7912" }, "require-dev": { "ext-pdo_sqlite": "*", "ext-uopz": "*", "maglnet/composer-require-checker": "^4.3", - "phpunit/phpunit": "^9.5", + "phpunit/phpunit": "^10.5.5", "rector/rector": "^0.19.0", "roave/infection-static-analysis-plugin": "^1.25", "slope-it/clock-mock": "0.4.0", @@ -55,7 +55,8 @@ "psr-4": { "Yiisoft\\Rbac\\Db\\Tests\\": "tests", "Yiisoft\\Rbac\\Tests\\": "vendor/yiisoft/rbac/tests" - } + }, + "files": ["tests/bootstrap.php"] }, "config": { "sort-packages": true, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 463a022..7f3c9c3 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,22 +1,16 @@ - - - - ./ - - - ./tests - ./vendor - - + @@ -37,4 +31,13 @@ ./tests/Oracle + + + ./ + + + ./tests + ./vendor + + diff --git a/psalm.xml b/psalm.xml index 277e73d..d091d59 100644 --- a/psalm.xml +++ b/psalm.xml @@ -13,4 +13,7 @@ + + + diff --git a/src/AssignmentsStorage.php b/src/AssignmentsStorage.php index c37106d..7ebc69d 100644 --- a/src/AssignmentsStorage.php +++ b/src/AssignmentsStorage.php @@ -128,6 +128,18 @@ public function userHasItem(string $userId, array $itemNames): bool ->exists(); } + public function filterUserItemNames(string $userId, array $itemNames): array + { + /** @var array{itemName: string} $rows */ + $rows = (new Query($this->database)) + ->select('itemName') + ->from($this->tableName) + ->where(['userId' => $userId, 'itemName' => $itemNames]) + ->all(); + + return array_column($rows, 'itemName'); + } + public function add(Assignment $assignment): void { $this diff --git a/src/ItemTreeTraversal/CteItemTreeTraversal.php b/src/ItemTreeTraversal/CteItemTreeTraversal.php index bf8dac7..16cd4c6 100644 --- a/src/ItemTreeTraversal/CteItemTreeTraversal.php +++ b/src/ItemTreeTraversal/CteItemTreeTraversal.php @@ -20,6 +20,7 @@ * @internal * * @psalm-import-type RawItem from ItemsStorage + * @psalm-import-type AccessTree from ItemTreeTraversalInterface */ abstract class CteItemTreeTraversal implements ItemTreeTraversalInterface { @@ -49,34 +50,60 @@ public function getParentRows(string $name): array return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery)->queryAll(); } - public function getChildrenRows(string $name): array + public function getAccessTree(string $name): array { - $baseOuterQuery = (new Query($this->database))->select('item.*')->where(['!=', 'item.name', $name]); + $baseOuterQuery = (new Query($this->database))->select(['item.*', 'parent_of.children']); + $cteSelectRelationQuery = (new Query($this->database)) + ->select(['parent', new Expression($this->getTrimConcatChildrenExpression())]) + ->from("$this->childrenTableName AS item_child_recursive") + ->innerJoin('parent_of', [ + 'item_child_recursive.child' => new Expression('{{parent_of}}.[[child_name]]'), + ]); + $cteSelectItemQuery = (new Query($this->database)) + ->select(['name', new Expression($this->getEmptyChildrenExpression())]) + ->from($this->tableName) + ->where(['name' => $name]) + ->union($cteSelectRelationQuery, all: true); + $quoter = $this->database->getQuoter(); + $outerQuery = $baseOuterQuery + ->withQuery( + $cteSelectItemQuery, + $quoter->quoteTableName('parent_of') . '(' . $quoter->quoteColumnName('child_name') . ',' . + $quoter->quoteColumnName('children') . ')', + recursive: $this->useRecursiveInWith, + ) + ->from('parent_of') + ->leftJoin( + ['item' => $this->tableName], + ['item.name' => new Expression('{{parent_of}}.[[child_name]]')], + ); + + /** @psalm-var AccessTree */ + return $outerQuery->all(); + } + + public function getChildrenRows(string|array $names): array + { + $baseOuterQuery = $this->getChildrenBaseOuterQuery($names); /** @psalm-var RawItem[] */ - return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll(); + return $this->getRowsCommand($names, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll(); } - public function getChildPermissionRows(string $name): array + public function getChildPermissionRows(string|array $names): array { - $baseOuterQuery = (new Query($this->database)) - ->select('item.*') - ->where(['!=', 'item.name', $name]) - ->andWhere(['item.type' => Item::TYPE_PERMISSION]); + $baseOuterQuery = $this->getChildrenBaseOuterQuery($names)->andWhere(['item.type' => Item::TYPE_PERMISSION]); /** @psalm-var RawItem[] */ - return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll(); + return $this->getRowsCommand($names, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll(); } - public function getChildRoleRows(string $name): array + public function getChildRoleRows(string|array $names): array { - $baseOuterQuery = (new Query($this->database)) - ->select('item.*') - ->where(['!=', 'item.name', $name]) - ->andWhere(['item.type' => Item::TYPE_ROLE]); + $baseOuterQuery = $this->getChildrenBaseOuterQuery($names)->andWhere(['item.type' => Item::TYPE_ROLE]); /** @psalm-var RawItem[] */ - return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll(); + return $this->getRowsCommand($names, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll(); } public function hasChild(string $parentName, string $childName): bool @@ -96,8 +123,27 @@ public function hasChild(string $parentName, string $childName): bool return $result !== false; } + /** + * @infection-ignore-all + * - ProtectedVisibility. + * + * @psalm-return non-empty-string + */ + protected function getEmptyChildrenExpression(): string + { + return "''"; + } + + /** + * @psalm-return non-empty-string + */ + protected function getTrimConcatChildrenExpression(): string + { + return "TRIM(',' FROM CONCAT(children, ',', item_child_recursive.child))"; + } + private function getRowsCommand( - string $name, + string|array $names, QueryInterface $baseOuterQuery, bool $areParents = true, ): CommandInterface { @@ -124,7 +170,7 @@ private function getRowsCommand( $cteSelectItemQuery = (new Query($this->database)) ->select('name') ->from($this->tableName) - ->where(['name' => $name]) + ->where(['name' => $names]) ->union($cteSelectRelationQuery, all: true); $quoter = $this->database->getQuoter(); $outerQuery = $baseOuterQuery @@ -141,4 +187,17 @@ private function getRowsCommand( return $outerQuery->createCommand(); } + + /** + * @psalm-param string|non-empty-array $names + */ + private function getChildrenBaseOuterQuery(string|array $names): QueryInterface + { + $baseOuterQuery = (new Query($this->database))->select('item.*')->distinct(); + if (is_string($names)) { + return $baseOuterQuery->where(['!=', 'item.name', $names]); + } + + return $baseOuterQuery->where(['not in', 'item.name', $names]); + } } diff --git a/src/ItemTreeTraversal/ItemTreeTraversalInterface.php b/src/ItemTreeTraversal/ItemTreeTraversalInterface.php index 45cfcd6..2e48df0 100644 --- a/src/ItemTreeTraversal/ItemTreeTraversalInterface.php +++ b/src/ItemTreeTraversal/ItemTreeTraversalInterface.php @@ -5,6 +5,7 @@ namespace Yiisoft\Rbac\Db\ItemTreeTraversal; use Yiisoft\Rbac\Db\ItemsStorage; +use Yiisoft\Rbac\Item; /** * An interface for retrieving hierarchical RBAC items' data in a more efficient way depending on used RDBMS and their @@ -12,6 +13,16 @@ * * @internal * + * @psalm-type AccessTree = non-empty-list + * * @psalm-import-type RawItem from ItemsStorage */ interface ItemTreeTraversalInterface @@ -26,35 +37,43 @@ interface ItemTreeTraversalInterface */ public function getParentRows(string $name): array; + /** + * @psalm-return AccessTree + */ + public function getAccessTree(string $name): array; + /** * Get all children rows for an item by the given name. * - * @param string $name Item name. + * @param string|string[] $names Item name / names. + * @psalm-param string|non-empty-array $names * * @return array Flat list of all children. * @psalm-return RawItem[] */ - public function getChildrenRows(string $name): array; + public function getChildrenRows(string|array $names): array; /** * Get all child permission rows for an item by the given name. * - * @param string $name Item name. + * @param string|string[] $names Item name / names. + * @psalm-param string|non-empty-array $names * * @return array Flat list of all child permissions. * @psalm-return RawItem[] */ - public function getChildPermissionRows(string $name): array; + public function getChildPermissionRows(string|array $names): array; /** * Get all child role rows for an item by the given name. * - * @param string $name Item name. + * @param string|string[] $names Item name / names. + * @psalm-param string|non-empty-array $names * * @return array Flat list of all child roles. * @psalm-return RawItem[] */ - public function getChildRoleRows(string $name): array; + public function getChildRoleRows(string|array $names): array; /** * Whether a selected parent has specific child. diff --git a/src/ItemTreeTraversal/MssqlCteItemTreeTraversal.php b/src/ItemTreeTraversal/MssqlCteItemTreeTraversal.php index 8394904..e0097ba 100644 --- a/src/ItemTreeTraversal/MssqlCteItemTreeTraversal.php +++ b/src/ItemTreeTraversal/MssqlCteItemTreeTraversal.php @@ -12,4 +12,9 @@ final class MssqlCteItemTreeTraversal extends CteItemTreeTraversal { protected bool $useRecursiveInWith = false; + + protected function getEmptyChildrenExpression(): string + { + return "CAST('' AS NVARCHAR(MAX))"; + } } diff --git a/src/ItemTreeTraversal/MysqlCteItemTreeTraversal.php b/src/ItemTreeTraversal/MysqlCteItemTreeTraversal.php index 1b7c324..d3b7281 100644 --- a/src/ItemTreeTraversal/MysqlCteItemTreeTraversal.php +++ b/src/ItemTreeTraversal/MysqlCteItemTreeTraversal.php @@ -12,4 +12,8 @@ */ final class MysqlCteItemTreeTraversal extends CteItemTreeTraversal { + protected function getEmptyChildrenExpression(): string + { + return "CAST('' AS CHAR(21844))"; + } } diff --git a/src/ItemTreeTraversal/MysqlItemTreeTraversal.php b/src/ItemTreeTraversal/MysqlItemTreeTraversal.php index b9a961c..b638ed5 100644 --- a/src/ItemTreeTraversal/MysqlItemTreeTraversal.php +++ b/src/ItemTreeTraversal/MysqlItemTreeTraversal.php @@ -19,6 +19,7 @@ * @internal * * @psalm-import-type RawItem from ItemsStorage + * @psalm-import-type AccessTree from ItemTreeTraversalInterface */ final class MysqlItemTreeTraversal implements ItemTreeTraversalInterface { @@ -42,9 +43,8 @@ public function getParentRows(string $name): array { $sql = "SELECT DISTINCT item.* FROM ( SELECT @r AS child_name, - (SELECT @r := parent FROM $this->childrenTableName WHERE child = child_name LIMIT 1) AS parent, - @l := @l + 1 AS level - FROM (SELECT @r := :name, @l := 0) val, $this->childrenTableName + (SELECT @r := parent FROM $this->childrenTableName WHERE child = child_name LIMIT 1) AS parent + FROM (SELECT @r := :name) val, $this->childrenTableName ) s LEFT JOIN $this->tableName AS item ON item.name = s.child_name WHERE item.name != :name"; @@ -56,34 +56,45 @@ public function getParentRows(string $name): array ->queryAll(); } - public function getChildrenRows(string $name): array + public function getAccessTree(string $name): array { - $baseOuterQuery = (new Query($this->database))->select([new Expression('item.*')])->distinct(); + $sql = "SELECT item.*, access_tree_base.children FROM ( + SELECT child_name, MIN(TRIM(BOTH ',' FROM TRIM(BOTH child_name FROM raw_children))) as children FROM ( + SELECT @r AS child_name, @path := concat(@path, ',', @r) as raw_children, + (SELECT @r := parent FROM $this->childrenTableName WHERE child = child_name LIMIT 1) AS parent + FROM (SELECT @r := :name, @path := '') val, $this->childrenTableName + ) raw_access_tree_base + GROUP BY child_name + ) access_tree_base + LEFT JOIN $this->tableName AS item ON item.name = access_tree_base.child_name"; + + /** @psalm-var AccessTree */ + return $this + ->database + ->createCommand($sql, [':name' => $name]) + ->queryAll(); + } + public function getChildrenRows(string|array $names): array + { /** @psalm-var RawItem[] */ - return $this->getChildrenRowsCommand($name, baseOuterQuery: $baseOuterQuery)->queryAll(); + return $this->getChildrenRowsCommand($names, baseOuterQuery: $this->getChildrenBaseOuterQuery())->queryAll(); } - public function getChildPermissionRows(string $name): array + public function getChildPermissionRows(string|array $names): array { - $baseOuterQuery = (new Query($this->database)) - ->select([new Expression('item.*')]) - ->distinct() - ->where(['item.type' => Item::TYPE_PERMISSION]); + $baseOuterQuery = $this->getChildrenBaseOuterQuery()->where(['item.type' => Item::TYPE_PERMISSION]); /** @psalm-var RawItem[] */ - return $this->getChildrenRowsCommand($name, baseOuterQuery: $baseOuterQuery)->queryAll(); + return $this->getChildrenRowsCommand($names, baseOuterQuery: $baseOuterQuery)->queryAll(); } - public function getChildRoleRows(string $name): array + public function getChildRoleRows(string|array $names): array { - $baseOuterQuery = (new Query($this->database)) - ->select([new Expression('item.*')]) - ->distinct() - ->where(['item.type' => Item::TYPE_ROLE]); + $baseOuterQuery = $this->getChildrenBaseOuterQuery()->where(['item.type' => Item::TYPE_ROLE]); /** @psalm-var RawItem[] */ - return $this->getChildrenRowsCommand($name, baseOuterQuery: $baseOuterQuery)->queryAll(); + return $this->getChildrenRowsCommand($names, baseOuterQuery: $baseOuterQuery)->queryAll(); } public function hasChild(string $parentName, string $childName): bool @@ -97,17 +108,50 @@ public function hasChild(string $parentName, string $childName): bool return $result !== false; } - private function getChildrenRowsCommand(string $name, QueryInterface $baseOuterQuery): CommandInterface + /** + * @param string|string[] $names + */ + private function getChildrenRowsCommand(string|array $names, QueryInterface $baseOuterQuery): CommandInterface { + $names = (array) $names; $fromSql = "SELECT DISTINCT child - FROM (SELECT * FROM $this->childrenTableName ORDER by parent) item_child_sorted, - (SELECT @pv := :name) init - WHERE find_in_set(parent, @pv) AND length(@pv := concat(@pv, ',', child))"; + FROM (SELECT * FROM $this->childrenTableName ORDER by parent) item_child_sorted,\n"; + $where = ''; + $excludedNamesStr = ''; + $parameters = []; + $lastNameIndex = array_key_last($names); + + foreach ($names as $index => $name) { + $fromSql .= "(SELECT @pv$index := :name$index) init$index"; + $excludedNamesStr .= "@pv$index"; + + if ($index !== $lastNameIndex) { + $fromSql .= ','; + $excludedNamesStr .= ', '; + } + + $fromSql .= "\n"; + + if ($index !== 0) { + $where .= ' OR '; + } + + $where .= "(find_in_set(parent, @pv$index) AND length(@pv$index := concat(@pv$index, ',', child)))"; + + $parameters[":name$index"] = $name; + } + + $where = "($where) AND child NOT IN ($excludedNamesStr)"; + $fromSql .= "WHERE $where"; $outerQuery = $baseOuterQuery - ->from(['s' => "($fromSql)"]) - ->leftJoin(['item' => $this->tableName], ['item.name' => new Expression('s.child')]) - ->addParams([':name' => $name]); + ->from(new Expression("($fromSql) s")) + ->leftJoin(['item' => $this->tableName], ['item.name' => new Expression('s.child')]); + /** @psalm-var non-empty-string $outerQuerySql */ + return $outerQuery->addParams($parameters)->createCommand(); + } - return $outerQuery->createCommand(); + private function getChildrenBaseOuterQuery(): QueryInterface + { + return (new Query($this->database))->select('item.*')->distinct(); } } diff --git a/src/ItemTreeTraversal/OracleCteItemTreeTraversal.php b/src/ItemTreeTraversal/OracleCteItemTreeTraversal.php index 24122b9..3e186d6 100644 --- a/src/ItemTreeTraversal/OracleCteItemTreeTraversal.php +++ b/src/ItemTreeTraversal/OracleCteItemTreeTraversal.php @@ -12,4 +12,14 @@ final class OracleCteItemTreeTraversal extends CteItemTreeTraversal { protected bool $useRecursiveInWith = false; + + protected function getTrimConcatChildrenExpression(): string + { + $quoter = $this->database->getQuoter(); + $childrenColumnString = $quoter->quoteColumnName('children'); + $childColumnString = $quoter->quoteTableName('item_child_recursive') . '.' . + $quoter->quoteColumnName('child'); + + return "TRIM (',' FROM $childrenColumnString || ',' || $childColumnString)"; + } } diff --git a/src/ItemTreeTraversal/SqliteCteItemTreeTraversal.php b/src/ItemTreeTraversal/SqliteCteItemTreeTraversal.php index bca3d82..d5694ce 100644 --- a/src/ItemTreeTraversal/SqliteCteItemTreeTraversal.php +++ b/src/ItemTreeTraversal/SqliteCteItemTreeTraversal.php @@ -12,4 +12,8 @@ */ final class SqliteCteItemTreeTraversal extends CteItemTreeTraversal { + protected function getTrimConcatChildrenExpression(): string + { + return "TRIM(children || ',' || item_child_recursive.child, ',')"; + } } diff --git a/src/ItemsStorage.php b/src/ItemsStorage.php index 23ecd8d..ce8292f 100644 --- a/src/ItemsStorage.php +++ b/src/ItemsStorage.php @@ -92,13 +92,25 @@ public function getAll(): array ->from($this->tableName) ->all(); - return array_map( - fn(array $row): Item => $this->createItem(...$row), - $rows, - ); + return $this->getItemsIndexedByName($rows); } - public function get(string $name): ?Item + public function getByNames(array $names): array + { + if (empty($names)) { + return []; + } + + /** @psalm-var RawItem[] $rawItems */ + $rawItems = (new Query($this->database)) + ->from($this->tableName) + ->where(['name' => $names]) + ->all(); + + return $this->getItemsIndexedByName($rawItems); + } + + public function get(string $name): Permission|Role|null { /** @psalm-var RawItem|null $row */ $row = (new Query($this->database)) @@ -271,6 +283,31 @@ public function getParents(string $name): array return $this->getItemsIndexedByName($rawItems); } + public function getAccessTree(string $name): array + { + $tree = []; + $childrenNamesMap = []; + + foreach ($this->getTreeTraversal()->getAccessTree($name) as $data) { + $childrenNamesMap[$data['name']] = $data['children'] !== '' && $data['children'] !== null + ? explode(',', $data['children']) + : []; + unset($data['children']); + $tree[$data['name']] = ['item' => $this->createItem(...$data)]; + } + + foreach ($tree as $index => $_item) { + $children = []; + foreach ($childrenNamesMap[$index] as $childrenName) { + $children[$childrenName] = $tree[$childrenName]['item']; + } + + $tree[$index]['children'] = $children; + } + + return $tree; + } + public function getDirectChildren(string $name): array { $quoter = $this->database->getQuoter(); @@ -289,24 +326,36 @@ public function getDirectChildren(string $name): array return $this->getItemsIndexedByName($rawItems); } - public function getAllChildren(string $name): array + public function getAllChildren(string|array $names): array { - $rawItems = $this->getTreeTraversal()->getChildrenRows($name); + if (is_array($names) && empty($names)) { + return []; + } + + $rawItems = $this->getTreeTraversal()->getChildrenRows($names); return $this->getItemsIndexedByName($rawItems); } - public function getAllChildPermissions(string $name): array + public function getAllChildPermissions(string|array $names): array { - $rawItems = $this->getTreeTraversal()->getChildPermissionRows($name); + if (is_array($names) && empty($names)) { + return []; + } + + $rawItems = $this->getTreeTraversal()->getChildPermissionRows($names); /** @psalm-var array */ return $this->getItemsIndexedByName($rawItems); } - public function getAllChildRoles(string $name): array + public function getAllChildRoles(string|array $names): array { - $rawItems = $this->getTreeTraversal()->getChildRoleRows($name); + if (is_array($names) && empty($names)) { + return []; + } + + $rawItems = $this->getTreeTraversal()->getChildRoleRows($names); /** @psalm-var array */ return $this->getItemsIndexedByName($rawItems); diff --git a/src/TransactionalManagerDecorator.php b/src/TransactionalManagerDecorator.php index 2337661..1381cf7 100644 --- a/src/TransactionalManagerDecorator.php +++ b/src/TransactionalManagerDecorator.php @@ -56,6 +56,11 @@ public function hasChild(string $parentName, string $childName): bool return $this->manager->hasChild($parentName, $childName); } + public function hasChildren(string $parentName): bool + { + return $this->manager->hasChildren($parentName); + } + public function assign(string $itemName, int|Stringable|string $userId, ?int $createdAt = null): ManagerInterface { $this->manager->assign($itemName, $userId, $createdAt); @@ -77,6 +82,11 @@ public function revokeAll(int|Stringable|string $userId): ManagerInterface return $this; } + public function getItemsByUserId(int|Stringable|string $userId): array + { + return $this->manager->getItemsByUserId($userId); + } + public function getRolesByUserId(int|Stringable|string $userId): array { return $this->manager->getRolesByUserId($userId); @@ -109,6 +119,11 @@ public function addRole(Role $role): ManagerInterface return $this; } + public function getRole(string $name): ?Role + { + return $this->manager->getRole($name); + } + public function removeRole(string $name): ManagerInterface { $this->manager->removeRole($name); @@ -131,9 +146,14 @@ public function addPermission(Permission $permission): ManagerInterface return $this; } - public function removePermission(string $permissionName): ManagerInterface + public function getPermission(string $name): ?Permission { - $this->manager->removePermission($permissionName); + return $this->manager->getPermission($name); + } + + public function removePermission(string $name): ManagerInterface + { + $this->manager->removePermission($name); return $this; } @@ -169,4 +189,14 @@ public function setGuestRoleName(?string $name): ManagerInterface return $this; } + + public function getGuestRoleName(): ?string + { + return $this->manager->getGuestRoleName(); + } + + public function getGuestRole(): ?Role + { + return $this->manager->getGuestRole(); + } } diff --git a/tests/Base/DbSchemaManagerTest.php b/tests/Base/DbSchemaManagerTest.php index 8e0e1fe..cd75b21 100644 --- a/tests/Base/DbSchemaManagerTest.php +++ b/tests/Base/DbSchemaManagerTest.php @@ -18,15 +18,15 @@ protected function setUp(): void protected function tearDown(): void { - if (str_starts_with($this->getName(), 'testInitWithEmptyTableNames')) { + if (str_starts_with($this->name(), 'testInitWithEmptyTableNames')) { return; } - if ($this->getName() === 'testHasTableWithEmptyString' || $this->getName() === 'testDropTableWithEmptyString') { + if ($this->name() === 'testHasTableWithEmptyString' || $this->name() === 'testDropTableWithEmptyString') { return; } - if (str_starts_with($this->getName(), 'testGet')) { + if (str_starts_with($this->name(), 'testGet')) { return; } @@ -38,7 +38,7 @@ protected function populateDatabase(): void // Skip } - public function dataInitWithEmptyTableNames(): array + public static function dataInitWithEmptyTableNames(): array { return [ [ diff --git a/tests/Base/ManagerTransactionErrorTest.php b/tests/Base/ManagerTransactionErrorTest.php index b40ed30..7749386 100644 --- a/tests/Base/ManagerTransactionErrorTest.php +++ b/tests/Base/ManagerTransactionErrorTest.php @@ -35,26 +35,26 @@ public function renameItem(string $oldName, string $newName): void public function testUpdateRoleTransactionError(): void { $manager = $this->createFilledManager(); - $role = $this->itemsStorage->getRole('reader')->withName('new reader'); + $role = $manager->getRole('reader')->withName('new reader'); try { $manager->updateRole('reader', $role); } catch (RuntimeException) { - $this->assertNotNull($this->itemsStorage->getRole('reader')); - $this->assertNull($this->itemsStorage->getRole('new reader')); + $this->assertNotNull($manager->getRole('reader')); + $this->assertNull($manager->getRole('new reader')); } } public function testUpdatePermissionTransactionError(): void { $manager = $this->createFilledManager(); - $permission = $this->itemsStorage->getPermission('updatePost')->withName('newUpdatePost'); + $permission = $manager->getPermission('updatePost')->withName('newUpdatePost'); try { $manager->updatePermission('updatePost', $permission); } catch (RuntimeException) { - $this->assertNotNull($this->itemsStorage->getPermission('updatePost')); - $this->assertNull($this->itemsStorage->getPermission('newUpdatePost')); + $this->assertNotNull($manager->getPermission('updatePost')); + $this->assertNull($manager->getPermission('newUpdatePost')); } } } diff --git a/tests/Base/ManagerTransactionSuccessTest.php b/tests/Base/ManagerTransactionSuccessTest.php index 7bab3a8..77dbfe5 100644 --- a/tests/Base/ManagerTransactionSuccessTest.php +++ b/tests/Base/ManagerTransactionSuccessTest.php @@ -29,7 +29,7 @@ protected function createAssignmentsStorage(): AssignmentsStorageInterface public function testUpdateRoleTransactionError(): void { $manager = $this->createFilledManager(); - $role = $this->itemsStorage->getRole('reader')->withName('new reader'); + $role = $manager->getRole('reader')->withName('new reader'); $manager->updateRole('reader', $role); $this->assertTransaction(); @@ -38,7 +38,7 @@ public function testUpdateRoleTransactionError(): void public function testUpdatePermissionTransactionError(): void { $manager = $this->createFilledManager(); - $permission = $this->itemsStorage->getPermission('updatePost')->withName('newUpdatePost'); + $permission = $manager->getPermission('updatePost')->withName('newUpdatePost'); $manager->updatePermission('updatePost', $permission); $this->assertTransaction(); diff --git a/tests/Sqlite/SchemaTrait.php b/tests/Sqlite/SchemaTrait.php index 0a62b0b..c68fc14 100644 --- a/tests/Sqlite/SchemaTrait.php +++ b/tests/Sqlite/SchemaTrait.php @@ -13,14 +13,20 @@ protected function checkItemsChildrenTable(): void parent::checkItemsChildrenTable(); $this->assertCount( - 1, + 2, $this->getDatabase()->getSchema()->getTableForeignKeys(DbSchemaManager::ITEMS_CHILDREN_TABLE), ); $this->assertForeignKey( table: DbSchemaManager::ITEMS_CHILDREN_TABLE, - expectedColumnNames: ['parent', 'child'], + expectedColumnNames: ['parent'], + expectedForeignTableName: DbSchemaManager::ITEMS_TABLE, + expectedForeignColumnNames: ['name'], + ); + $this->assertForeignKey( + table: DbSchemaManager::ITEMS_CHILDREN_TABLE, + expectedColumnNames: ['child'], expectedForeignTableName: DbSchemaManager::ITEMS_TABLE, - expectedForeignColumnNames: ['name', 'name'], + expectedForeignColumnNames: ['name'], ); $this->assertCount( diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..f7cb39e --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,10 @@ +