diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 3708651..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: CI -on: - push: - pull_request: - schedule: - - cron: '0 0 * * *' -jobs: - tests: - runs-on: 'ubuntu-latest' - name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - strategy: - matrix: - php: ['8.0', '8.1', '8.2'] - laravel: ['8', '9', '10'] - exclude: - - php: '8.2' - laravel: '8' - - php: '8.0' - laravel: '10' - steps: - - uses: actions/checkout@v3 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - ini-values: error_reporting=E_ALL - tools: phpunit, git - - - name: Install Composer dependencies - run: rm -f composer.lock - - - name: Install dependencies for Laravel ${{ matrix.laravel}} - run: composer require --no-progress --no-scripts --no-plugins illuminate/config ^${{ matrix.laravel }} illuminate/contracts ^${{ matrix.laravel }} illuminate/console ^${{ matrix.laravel }} -v - - - name: Update dependencies - run: composer update --no-interaction - - - name: PHPUnit - run: vendor/bin/phpunit diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 0000000..ff96e40 --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,20 @@ +name: "Coding Standards" + +on: + pull_request: + branches: + - "*.x" + - "main" + push: + branches: + - "*.x" + - "main" + +jobs: + coding-standards: + name: "Coding Standards" + uses: "doctrine/.github/.github/workflows/coding-standards.yml@5.2.0" + with: + php-version: '8.2' + composer-options: '--prefer-dist --ignore-platform-req=php' + diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..b5d5786 --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,61 @@ +name: "Continuous Integration" + +on: + pull_request: + branches: + - "*.x" + - "main" + push: + branches: + - "*.x" + - "main" + +jobs: + phpunit: + name: "PHPUnit" + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php-version: + - "8.2" + - "8.3" + - "8.4" + dependencies: + - "highest" + - "lowest" + optional-dependencies: + - true + - false + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + with: + fetch-depth: 2 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + coverage: "pcov" + ini-values: "zend.assertions=1" + extensions: "pdo_mysql" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v1" + with: + dependency-versions: "${{ matrix.dependencies }}" + composer-options: "--prefer-dist" + + - name: "Show Composer packages" + run: "composer show" + + - name: "Run PHPUnit" + run: "vendor/bin/phpunit --coverage-clover=coverage.xml" + + - name: "Upload coverage" + uses: "codecov/codecov-action@v5" + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..1f0c400 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,36 @@ +name: "Static Analysis" + +on: + pull_request: + push: + +jobs: + static-analysis-phpstan: + name: "Static Analysis with PHPStan" + runs-on: "ubuntu-22.04" + + strategy: + matrix: + php-version: + - "8.2" + - "8.3" + - "8.4" + + steps: + - name: "Checkout code" + uses: "actions/checkout@v3" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: "pdo_sqlite" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v2" + with: + dependency-versions: "${{ matrix.dependencies }}" + + - name: "Run a static analysis with phpstan/phpstan" + run: "vendor/bin/phpstan analyse src --level 1" diff --git a/.gitignore b/.gitignore index 6832801..493e917 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,21 @@ +/coverage /vendor composer.phar composer.lock .DS_Store -.php_cs.cache +.phpcs-cache +.phpunit.cache +.phpunit.result.cache + .idea +.vscode + +/tests/Stubs/storage/framework/views/* +!/tests/Stubs/storage/framework/views/.gitkeep +/tests/Stubs/storage/doctrine.generated.php laravel-doctrine-orm.iml +/workbench/bootstrap/cache/* +!/workbench/bootstrap/cache/.gitkeep +/workbench/storage/logs/* +/workbench/vendor \ No newline at end of file diff --git a/README.md b/README.md index b096e4f..2b2290b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,44 @@ -# Laravel Doctrine ACL +

+ + +

- +Laravel Doctrine ACL +==================== -[![GitHub release](https://img.shields.io/github/release/laravel-doctrine/acl.svg?style=flat-square)](https://packagist.org/packages/laravel-doctrine/acl) -[![Github actions](https://github.com/laravel-doctrine/acl/workflows/CI/badge.svg?branch=1.x)](https://github.com/laravel-doctrine/acl/actions?query=workflow%3ACI+branch%3A1.x) -[![Packagist](https://img.shields.io/packagist/dm/laravel-doctrine/acl.svg?style=flat-square)](https://packagist.org/packages/laravel-doctrine/acl) -[![Packagist](https://img.shields.io/packagist/dt/laravel-doctrine/acl.svg?style=flat-square)](https://packagist.org/packages/laravel-doctrine/acl) +Laravel Doctrine ACL is a package that provides RBAC (Role-Based Access Control) functionality for Laravel applications using Doctrine. It allows you to manage roles, permissions, and organisations, and seamlessly integrates with Laravel's Authorization system. -*ACL functionality for Laravel powered by Doctrine* +[![Build Status](https://github.com/laravel-doctrine/acl/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/laravel-doctrine/acl/actions) +[![Code Coverage](https://codecov.io/gh/laravel-doctrine/acl/graph/badge.svg?token=3CpQzDXOWX)](https://codecov.io/gh/laravel-doctrine/acl) +[![PHPStan](https://img.shields.io/badge/PHPStan-level%201-brightgreen.svg)](https://img.shields.io/badge/PHPStan-level%201-brightgreen.svg) +[![Documentation](https://readthedocs.org/projects/laravel-doctrine-acl-official/badge/?version=latest)](https://laravel-doctrine-acl-official.readthedocs.io/en/latest/) +[![Packagist Downloads](https://img.shields.io/packagist/dd/laravel-doctrine/acl)](https://packagist.org/packages/laravel-doctrine/acl) -* Roles -* Permissions -* Organisations -* Seamless integration with Laravel's Authorization system +Installation +------------ -## Versions +Via composer: -Version | Supported Laravel Versions -:---------|:---------- -~1.1.0 | 6.x -~1.2.0 | 7.x -~1.3.0 | 8.x -^1.4.0 | 6.x, 7.x, 8.x, 9.x, 10.x +```bash +composer require laravel-doctrine/acl +``` + +The ServiceProvider and Facades are autodiscovered. + +Publish the configuration: + +```bash +php artisan vendor:publish --tag="config" --provider="LaravelDoctrine\ACL\AclServiceProvider" +``` + +Documentation +------------- + +Full documentation at https://laravel-doctrine-acl.readthedocs.io/en/latest/index.html +or in the docs directory. + +Versions +-------- + +* Version 2 supports Laravel 11-12, ORM ^3.0, DBAL ^4.0, and PHP 8.2. +* Version 1 supports Laravel 6 - 11, DBAL ^2.0, ORM ^2.0, and PHP ^5.5 - ^8.0. diff --git a/composer.json b/composer.json index fb4ca3e..8374e5e 100644 --- a/composer.json +++ b/composer.json @@ -1,55 +1,97 @@ { - "name": "laravel-doctrine/acl", - "type": "library", - "description": "Doctrine ACL for Doctrine 2 and Laravel", - "license": "MIT", - "keywords": [ - "doctrine", - "laravel", - "orm", - "data mapper", - "database", - "acl", - "abilities", - "policies", - "permissions", - "roles", - "organisations" - ], - "authors": [ - { - "name": "Patrick Brouwers", - "email": "patrick@maatwebsite.nl" + "name": "laravel-doctrine/acl", + "type": "library", + "description": "ACL for Laravel and Doctrine", + "license": "MIT", + "keywords": [ + "doctrine", + "laravel", + "orm", + "data mapper", + "database", + "acl", + "abilities", + "policies", + "permissions", + "roles", + "organisations" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@maatwebsite.nl" + }, + { + "name": "Pavlo Zhytomyrskyi", + "email": "pavelz@scholarshipowl.com" + } + ], + "require": { + "php": "^8.2", + "illuminate/auth": "^11.0|^12.0", + "illuminate/config": "^11.0|^12.0", + "illuminate/contracts": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", + "laravel-doctrine/orm": "^3.1" + }, + "require-dev": { + "mockery/mockery": "^1.3.1", + "phpunit/phpunit": "^11.5", + "laravel/framework": "^11.0|^12.0", + "orchestra/testbench": "^10.2", + "laravel-doctrine/migrations": "^3.4", + "doctrine/coding-standard": "^12.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0" + }, + "autoload": { + "psr-4": { + "LaravelDoctrine\\ACL\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests", + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" + } + }, + "extra": { + "laravel": { + "providers": [ + "LaravelDoctrine\\ACL\\AclServiceProvider" + ] + } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "scripts": { + "test": [ + "vendor/bin/parallel-lint src tests", + "vendor/bin/phpcs", + "vendor/bin/phpunit", + "vendor/bin/phpstan analyze src --level 1" + ], + "coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage", + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "build": "@php vendor/bin/testbench workbench:build --ansi", + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@build", + "@php vendor/bin/testbench serve --ansi" + ], + "lint": [ + "@php vendor/bin/phpstan analyse --verbose --ansi" + ] } - ], - "require": { - "php": "^7.2.5|^8.0", - "illuminate/auth": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/config": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0", - "laravel-doctrine/orm": "^1|^2.0", - "doctrine/annotations": "^1.10|^2.0" - }, - "require-dev": { - "mockery/mockery": "^1.3.1", - "phpunit/phpunit": "^8.5" - }, - "autoload": { - "psr-4": { - "LaravelDoctrine\\ACL\\": "src/" - } - }, - "autoload-dev": { - "classmap": [ - "tests" - ] - }, - "extra": { - "laravel": { - "providers": [ - "LaravelDoctrine\\ACL\\AclServiceProvider" - ] - } - } } diff --git a/config/acl.php b/config/acl.php index 767a8e8..5f72a9b 100644 --- a/config/acl.php +++ b/config/acl.php @@ -2,14 +2,6 @@ return [ - /* - |-------------------------------------------------------------------------- - | Roles - |-------------------------------------------------------------------------- - */ - 'roles' => [ - 'entity' => App\Entities\Role::class, - ], /* |-------------------------------------------------------------------------- | Permissions @@ -24,6 +16,17 @@ 'entity' => LaravelDoctrine\ACL\Permissions\Permission::class, 'list' => [], ], + + + /* + |-------------------------------------------------------------------------- + | Roles + |-------------------------------------------------------------------------- + */ + 'roles' => [ + 'entity' => App\Entities\Role::class, + ], + /* |-------------------------------------------------------------------------- | Organisations diff --git a/docs/configurations.rst b/docs/configurations.rst new file mode 100644 index 0000000..ac5636f --- /dev/null +++ b/docs/configurations.rst @@ -0,0 +1,80 @@ +=========== +Config File +=========== + +This document describes the options available in the `config/acl.php` configuration file for the Laravel Doctrine ACL package. + +Permissions +=========== + +.. code-block:: php + + 'permissions' => [ + 'driver' => 'config', + 'entity' => LaravelDoctrine\ACL\Permissions\Permission::class, + 'list' => [], + ], + +- **driver**: The permissions driver to use. Supported drivers: + + - `config`: Permissions are defined statically in the `list` array below. + - `doctrine`: Permissions are managed as Doctrine entities in the database. + +- **list**: (Only for `config` driver) An array of permission names to be recognized by the system. Example: `['edit.posts', 'delete.posts']` + + .. code-block:: php + + 'list' => [ + 'edit.posts', + 'delete.posts', + ], + +- **entity**: (Only for `doctrine` driver) The fully qualified class name of your Permission entity. Defaults to `LaravelDoctrine\ACL\Permissions\Permission`. + +Roles +===== + +.. code-block:: php + + 'roles' => [ + 'entity' => App\Entities\Role::class, + ], + +- **entity**: The fully qualified class name of your Role entity. By default, this is `App\Entities\Role`. You may customize this to point to your own Role entity class implementing `LaravelDoctrine\ACL\Contracts\Role`. + + +Organisations +============= + +.. code-block:: php + + 'organisations' => [ + 'entity' => App\Entities\Organisation::class, + ], + +- **entity**: The fully qualified class name of your Organisation entity. By default, this is `App\Entities\Organisation`. You may customize this to point to your own Organisation entity class implementing `LaravelDoctrine\ACL\Contracts\Organisation`. + + +Entities +======== + +You can use the stubs as a starting point for your own entities. + +You may publish the stubs for the entities by running the following command: + +.. code-block:: bash + + php artisan vendor:publish --tag="acl-entities" + +This command will publish the stubs for the entities to the `app/Entities` directory. + + * [`app/Entities/Permission.php`](../stubs/Permission.php) - The stub for the Permission entity. + * [`app/Entities/Role.php`](../stubs/Role.php) - The stub for the Role entity. + * [`app/Entities/Organisation.php`](../stubs/Organisation.php) - The stub for the Organisation entity. + +> **Note**: Pay attention that we published a stub for Permission so you should update `acl.permission.entity` in the config file. + +.. role:: raw-html(raw) + :format: html + +.. include:: footer.rst diff --git a/docs/core-concepts.rst b/docs/core-concepts.rst index 2166764..84205b2 100644 --- a/docs/core-concepts.rst +++ b/docs/core-concepts.rst @@ -11,7 +11,7 @@ A permission is a singular ability to perform an action. Read more at `permissions `_. * Both users and roles can have permissions. -* Implement ``LaravelDoctrine\ACL\Contracts\HasPermissions`` and use the ``HasPermissions`` trait. +* Implement ``LaravelDoctrine\ACL\Contracts\HasPermissions`` and use the ``WithPermissions`` trait. * Permissions can be managed via config or Doctrine database tables (see below). @@ -45,14 +45,6 @@ Read more about `organisations `_. * Set ``acl.organisations.entity`` in your config. * Users can belong to one or multiple organisations (implement ``BelongsToOrganisation`` or ``BelongsToOrganisations``). - -Advanced Configuration -====================== - -* Override default entities in the config (``acl.roles.entity``, ``acl.permissions.entity``, etc.). -* Choose permission storage driver (``acl.permissions.driver``: ``config`` or ``doctrine``). -* Use custom permission logic by implementing the relevant contracts. - .. role:: raw-html(raw) :format: html diff --git a/docs/index.rst b/docs/index.rst index 1b299e6..5a1f72b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,7 +21,9 @@ For older versions use 1.x :caption: Table of Contents introduction - installation + install + configurations + usage core-concepts permissions diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 0000000..9b59670 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,35 @@ +======= +Install +======= + +Installation of this module uses composer. For composer documentation, please +refer to `getcomposer.org `_ :: + +.. code-block:: bash + + composer require laravel-doctrine/acl + +To publish the config use: + +.. code-block:: bash + + php artisan vendor:publish --tag="config" --provider="LaravelDoctrine\ACL\AclServiceProvider" + +Thanks to Laravel auto package discovery, the ServiceProvider is +automatically registered. However they can still be manually registered if +required (see below). + +Manual Registration +=================== + +After updating composer, add the ServiceProvider to the providers +array in ``bootstrap/providers.php`` + +.. code-block:: php + + LaravelDoctrine\ACL\AclServiceProvider::class, + +.. role:: raw-html(raw) + :format: html + +.. include:: footer.rst diff --git a/docs/installation.rst b/docs/installation.rst deleted file mode 100644 index f313c43..0000000 --- a/docs/installation.rst +++ /dev/null @@ -1,27 +0,0 @@ -============ -Installation -============ - -Install via Composer: - -.. code-block:: bash - - composer require laravel-doctrine/acl - -Package should be automatically registered but in case no add it to `bootstrap/providers.php`: - -.. code-block:: php - - LaravelDoctrine\ACL\AclServiceProvider::class, - -Publish the configuration: - -.. code-block:: bash - - php artisan vendor:publish --tag="config" --provider="LaravelDoctrine\ACL\AclServiceProvider" - - -.. role:: raw-html(raw) - :format: html - -.. include:: footer.rst diff --git a/docs/introduction.rst b/docs/introduction.rst index ad441f4..c936f38 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -6,8 +6,8 @@ Laravel Doctrine ACL brings robust, flexible Access Control List (ACL) support t Laravel using Doctrine ORM. It enables you to manage permissions, roles, and organisations in a way that integrates seamlessly with Laravel’s native authorization system. -* Users can belong to organisations. * Users and roles can have permissions. +* Users can belong to organisations. * Flexible permission storage (config or database). diff --git a/docs/organisations.rst b/docs/organisations.rst index d81ac4e..065979b 100644 --- a/docs/organisations.rst +++ b/docs/organisations.rst @@ -16,7 +16,7 @@ you will have to create an entity that implements use Doctrine\ORM\Mapping as ORM; use LaravelDoctrine\ACL\Contracts\Organisation; - use LaravelDoctrine\ACL\Mappings as ACL; + use LaravelDoctrine\ACL\Attribute as ACL; #[ORM\Entity] class Team implements Organisation @@ -35,6 +35,16 @@ you will have to create an entity that implements } } +You can use the Organisation stub as a starting point for your own entity. + +.. code-block:: bash + + php artisan vendor:publish --tag="acl-entity-organisation" + +This command will publish the [`Organisation`](../stubs/Organisation.php) stub for the Organisation entity to the `app/Entities` directory. + +> **Note**: Pay attention that we published a stub for Organisation so you should update `acl.organisation.entity` in the config file. + User can belong to one organisation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -47,7 +57,7 @@ The User class should implement **Note**: Pay attention that we published a stub for Permission so you should update `acl.permission.entity` in the config file. + Getting all permissions ~~~~~~~~~~~~~~~~~~~~~~~ You can get a list of all permissions with the -``LaravelDoctrine\ACL\Permissions\PermissionManager`` +``LaravelDoctrine\ACL\PermissionManager`` .. code:: php @@ -80,7 +90,7 @@ Checking if a User or Role has permission On the User or Role entity ^^^^^^^^^^^^^^^^^^^^^^^^^^ -When adding the ``LaravelDoctrine\ACL\Permissions\HasPermissions`` trait +When adding the ``LaravelDoctrine\ACL\Permissions\WithPermissions`` trait you will get a ``hasPermissionTo`` method. First the ``User`` entity will check if it has the right permission itself. If not it will search in its roles. If none of them has permission, it will return false. diff --git a/docs/roles.rst b/docs/roles.rst index 8697a4e..ca70a31 100644 --- a/docs/roles.rst +++ b/docs/roles.rst @@ -37,7 +37,15 @@ set to ``App\Entities\Role``. } } +You can use the Role stub as a starting point for your own entity. +.. code-block:: bash + + php artisan vendor:publish --tag="acl-entity-role" + +This command will publish the [`Role`](../stubs/Role.php) stub for the Role entity to the `app/Entities` directory. + +> **Note**: Pay attention that we published a stub for Role so you should update `acl.role.entity` in the config file. A User has Roles ---------------- @@ -47,7 +55,7 @@ role. The ``User`` entity should implement the ``LaravelDoctrine\ACL\Contracts\HasRoles`` interface. You can use the ``#[ACL\HasRoles]`` attribute to define the relations (instead of defining the ManyToMany manually). Import -``use LaravelDoctrine\ACL\Mappings as ACL;`` in top of the class. +``use LaravelDoctrine\ACL\Attribute as ACL;`` in top of the class. .. code:: php @@ -55,7 +63,7 @@ defining the ManyToMany manually). Import use Doctrine\ORM\Mapping as ORM; use LaravelDoctrine\ACL\Roles\HasRoles; - use LaravelDoctrine\ACL\Mappings as ACL; + use LaravelDoctrine\ACL\Attribute as ACL; use LaravelDoctrine\ACL\Contracts\HasRoles as HasRolesContract; #[ORM\Entity] @@ -93,7 +101,7 @@ the user and their roles. This means: 2. If not found, check all permissions assigned to each of the user’s roles. -- This logic is implemented in the ``HasPermissions`` trait (see +- This logic is implemented in the ``WithPermissions`` trait (see source), which first checks the user’s permissions, then iterates over all roles (if any) and checks their permissions recursively. diff --git a/docs/usage.rst b/docs/usage.rst index e9c2df7..feabe5b 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -71,7 +71,7 @@ Use the `PermissionManager` to retrieve all permissions: .. code-block:: php - $manager = app(LaravelDoctrine\ACL\Permissions\PermissionManager::class); + $manager = app(LaravelDoctrine\ACL\PermissionManager::class); $manager->getAllPermissions(); diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..76b6d6f --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,19 @@ + + + + + + + + + + + + + src + tests + workbench/app + + + + \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..d0d0224 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 0 + paths: + - src \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 0b5c4a4..0aedd31 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,17 +1,17 @@ - +> ./tests/ + + + ./src + + diff --git a/src/AclServiceProvider.php b/src/AclServiceProvider.php index 8d1aa9b..8fa270d 100644 --- a/src/AclServiceProvider.php +++ b/src/AclServiceProvider.php @@ -1,107 +1,108 @@ isLumen()) { - $this->publishes([ - $this->getConfigPath() => config_path('acl.php'), - ], 'config'); - } - - $this->app->make(DoctrineManager::class)->onResolve(function () { - $this->definePermissions( - app(Gate::class), - app(PermissionManager::class) - ); - }); + $this->publishConfig(); + $this->publishEntities(); } - /** - * Register the service provider. - * @return void - */ - public function register() + public function register(): void { $this->mergeConfig(); - if (method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { - AnnotationRegistry::registerUniqueLoader('class_exists'); - } + $this->registerPaths(); + $this->registerGatePermissions(); + $this->registerDoctrineMappings(); + } + + protected function registerDoctrineMappings(): void + { $manager = $this->app->make(DoctrineManager::class); $manager->extendAll(RegisterMappedEventSubscribers::class); - - $this->registerPaths($manager); } - /** - * @param Gate $gate - * @param PermissionManager $manager - */ - protected function definePermissions(Gate $gate, PermissionManager $manager) + protected function registerPaths(): void { - foreach ($manager->getPermissionsWithDotNotation() as $permission) { - $gate->define($permission, function (HasPermissions $user) use ($permission) { - return $user->hasPermissionTo($permission); - }); + $permissionManager = $this->app->make(PermissionManager::class); + + if (! $permissionManager->useDefaultPermissionEntity()) { + return; } + + $manager = $this->app->make(DoctrineManager::class); + $manager->addPaths([ + __DIR__ . DIRECTORY_SEPARATOR . 'Permissions', + ]); } - /** - * Merge config. - */ - protected function mergeConfig() + protected function registerGatePermissions(): void { - $this->mergeConfigFrom( - $this->getConfigPath(), 'acl' - ); + $this->app->afterResolving(Gate::class, function (Gate $gate): void { + $manager = $this->app->make(PermissionManager::class); - if ($this->isLumen()) { - $this->app->configure('acl'); - } + foreach ($manager->getPermissionsWithDotNotation() as $permission) { + $gate->define($permission, static function (HasPermissions $user) use ($permission) { + return $user->hasPermissionTo($permission); + }); + } + }); } - /** - * @return string - */ - protected function getConfigPath() + protected function publishConfig(): void { - return __DIR__ . '/../config/acl.php'; + $this->publishes([ + $this->getConfigPath() => config_path('acl.php'), + ], 'config'); } - /** - * @return bool - */ - protected function isLumen() + protected function mergeConfig(): void + { + $this->mergeConfigFrom( + $this->getConfigPath(), + 'acl', + ); + } + + protected function getConfigPath(): string { - return Str::contains($this->app->version(), 'Lumen'); + return __DIR__ . '/../config/acl.php'; } /** - * @param $manager + * Publish default entity stubs separately with specific tags/groups. */ - private function registerPaths($manager) + protected function publishEntities(): void { - $permissionManager = $this->app->make(PermissionManager::class); + // Permission entity + $this->publishes([ + __DIR__ . '/../stubs/Permission.php' => app_path('Entities/Permission.php'), + ], ['acl-entities', 'acl-entity-permission']); - if ($permissionManager->useDefaultPermissionEntity()) { - $manager->addPaths([ - __DIR__ . DIRECTORY_SEPARATOR . 'Permissions', - ]); - } + // Role entity + $this->publishes([ + __DIR__ . '/../stubs/Role.php' => app_path('Entities/Role.php'), + ], ['acl-entities', 'acl-entity-role']); + + // Organisation entity + $this->publishes([ + __DIR__ . '/../stubs/Organisation.php' => app_path('Entities/Organisation.php'), + ], ['acl-entities', 'acl-entity-organisation']); } } diff --git a/src/Attribute/BelongsToOrganisation.php b/src/Attribute/BelongsToOrganisation.php new file mode 100644 index 0000000..0c14f9a --- /dev/null +++ b/src/Attribute/BelongsToOrganisation.php @@ -0,0 +1,32 @@ +targetEntity = $targetEntity; + $this->cascade = $cascade; + $this->fetch = $fetch; + $this->orphanRemoval = $orphanRemoval; + $this->indexBy = $indexBy; + } + + public function getTargetEntity(Config $config): string|null + { + return $this->targetEntity ?: $config->get('acl.organisations.entity', 'Organisation'); + } +} diff --git a/src/Attribute/BelongsToOrganisations.php b/src/Attribute/BelongsToOrganisations.php new file mode 100644 index 0000000..c35e6b4 --- /dev/null +++ b/src/Attribute/BelongsToOrganisations.php @@ -0,0 +1,34 @@ +targetEntity = $targetEntity; + $this->mappedBy = $mappedBy; + $this->cascade = $cascade; + $this->fetch = $fetch; + $this->orphanRemoval = $orphanRemoval; + $this->indexBy = $indexBy; + } + + public function getTargetEntity(Config $config): string|null + { + return $this->targetEntity ?: $config->get('acl.organisations.entity', 'Organisation'); + } +} diff --git a/src/Attribute/HasPermissions.php b/src/Attribute/HasPermissions.php new file mode 100644 index 0000000..ddd92a4 --- /dev/null +++ b/src/Attribute/HasPermissions.php @@ -0,0 +1,39 @@ +targetEntity = $targetEntity; + $this->mappedBy = $mappedBy; + $this->cascade = $cascade; + $this->fetch = $fetch; + $this->orphanRemoval = $orphanRemoval; + $this->indexBy = $indexBy; + } + + public function getTargetEntity(Config $config): string|null + { + // Config driver has no target entity + if ($config->get('acl.permissions.driver', 'config') === 'config') { + return null; + } + + return $this->targetEntity ?: $config->get('acl.permissions.entity', 'Permission'); + } +} diff --git a/src/Attribute/HasRoles.php b/src/Attribute/HasRoles.php new file mode 100644 index 0000000..b04b7b9 --- /dev/null +++ b/src/Attribute/HasRoles.php @@ -0,0 +1,34 @@ +targetEntity = $targetEntity; + $this->mappedBy = $mappedBy; + $this->cascade = $cascade; + $this->fetch = $fetch; + $this->orphanRemoval = $orphanRemoval; + $this->indexBy = $indexBy; + } + + public function getTargetEntity(Config $config): string|null + { + return $this->targetEntity ?: $config->get('acl.roles.entity', 'Role'); + } +} diff --git a/src/Attribute/MappingAttribute.php b/src/Attribute/MappingAttribute.php new file mode 100644 index 0000000..c8954d7 --- /dev/null +++ b/src/Attribute/MappingAttribute.php @@ -0,0 +1,13 @@ +config->get('acl.permissions.list', [])); + } +} diff --git a/src/Configurations/DoctrinePermissionsProvider.php b/src/Configurations/DoctrinePermissionsProvider.php new file mode 100644 index 0000000..88ca8e6 --- /dev/null +++ b/src/Configurations/DoctrinePermissionsProvider.php @@ -0,0 +1,56 @@ +getEntityManager(); + $metadata = $em->getClassMetadata($this->getPermissionClass()); + + return new Doctrine(new EntityRepository($em, $metadata)); + } + + protected function getPermissionClass(): string + { + $class = $this->config->get('acl.permissions.entity'); + + if (! $class) { + throw new RunTimeException( + 'Failed to configure doctrine permissions. No entity class provided.', + ); + } + + return $class; + } + + protected function getEntityManager(): EntityManagerInterface + { + $em = $this->registry->getManagerForClass($this->getPermissionClass()); + + if (! $em) { + throw new RunTimeException( + 'Failed to configure doctrine permissions.' + . ' No entity manager found for entity: ' . $this->getPermissionClass() . '.', + ); + } + + return $em; + } +} diff --git a/src/Configurations/PermissionsProvider.php b/src/Configurations/PermissionsProvider.php new file mode 100644 index 0000000..28d3a25 --- /dev/null +++ b/src/Configurations/PermissionsProvider.php @@ -0,0 +1,14 @@ +|Organisation[] */ + public function getOrganisations(): Collection|array; } diff --git a/src/Contracts/HasPermissions.php b/src/Contracts/HasPermissions.php index 78e4396..300956f 100644 --- a/src/Contracts/HasPermissions.php +++ b/src/Contracts/HasPermissions.php @@ -1,20 +1,15 @@ |Permission[]|Collection|string[] */ + public function getPermissions(): Collection|array; } diff --git a/src/Contracts/HasRoles.php b/src/Contracts/HasRoles.php index 1f60633..a9cff1c 100644 --- a/src/Contracts/HasRoles.php +++ b/src/Contracts/HasRoles.php @@ -1,13 +1,13 @@ |Role[] */ + public function getRoles(): Collection|array; } diff --git a/src/Contracts/Organisation.php b/src/Contracts/Organisation.php index 6e627b6..0162d90 100644 --- a/src/Contracts/Organisation.php +++ b/src/Contracts/Organisation.php @@ -1,11 +1,10 @@ getNamespace() . '\\' . Str::studly($driver) . $this->getClassSuffix(); - - // We'll check to see if a creator method exists for the given driver. If not we - // will check for a custom driver creator, which allows developers to create - // drivers using their own customized driver creator Closure to create it. - if (isset($this->customCreators[$driver])) { - return $this->callCustomCreator($driver); - } elseif (class_exists($class)) { - return $this->container->make($class); - } - - throw new DriverNotFound("Driver [$driver] not supported."); - } -} diff --git a/src/Mappings/BelongsToOrganisation.php b/src/Mappings/BelongsToOrganisation.php deleted file mode 100644 index d61c341..0000000 --- a/src/Mappings/BelongsToOrganisation.php +++ /dev/null @@ -1,28 +0,0 @@ -targetEntity ?: $config->get('acl.organisations.entity', 'Organisation'); - } -} diff --git a/src/Mappings/BelongsToOrganisations.php b/src/Mappings/BelongsToOrganisations.php deleted file mode 100644 index 20e114e..0000000 --- a/src/Mappings/BelongsToOrganisations.php +++ /dev/null @@ -1,28 +0,0 @@ -targetEntity ?: $config->get('acl.organisations.entity', 'Organisation'); - } -} diff --git a/src/Mappings/Builders/Builder.php b/src/Mappings/Builders/Builder.php index 14cc339..446cccf 100644 --- a/src/Mappings/Builders/Builder.php +++ b/src/Mappings/Builders/Builder.php @@ -1,17 +1,14 @@ $property->getName(), - 'type' => Type::JSON_ARRAY, - ] + 'type' => Types::JSON, + ], ); $builder->build(); diff --git a/src/Mappings/Builders/ManyToManyBuilder.php b/src/Mappings/Builders/ManyToManyBuilder.php index 8b134c7..423abf0 100644 --- a/src/Mappings/Builders/ManyToManyBuilder.php +++ b/src/Mappings/Builders/ManyToManyBuilder.php @@ -1,51 +1,40 @@ config = $config; } - /** - * @param ClassMetadata $metadata - * @param ReflectionProperty $property - * @param ConfigAnnotation $annotation - */ - public function build(ClassMetadata $metadata, ReflectionProperty $property, ConfigAnnotation $annotation) + public function build(ClassMetadata $metadata, ReflectionProperty $property, MappingAttribute $attribute): void { $builder = new ManyToManyAssociationBuilder( new ClassMetadataBuilder($metadata), [ 'fieldName' => $property->getName(), - 'targetEntity' => $annotation->getTargetEntity($this->config), + 'targetEntity' => $attribute->getTargetEntity($this->config), ], - ClassMetadata::MANY_TO_MANY + OrmClassMetadata::MANY_TO_MANY, ); - if (isset($annotation->inversedBy) && $annotation->inversedBy) { - $builder->inversedBy($annotation->inversedBy); + if (isset($attribute->inversedBy) && $attribute->inversedBy) { + $builder->inversedBy($attribute->inversedBy); } - if (isset($annotation->mappedBy) && $annotation->mappedBy) { - $builder->mappedBy($annotation->mappedBy); + if (isset($attribute->mappedBy) && $attribute->mappedBy) { + $builder->mappedBy($attribute->mappedBy); } $builder->build(); diff --git a/src/Mappings/Builders/ManyToOneBuilder.php b/src/Mappings/Builders/ManyToOneBuilder.php index 1154be2..82823c6 100644 --- a/src/Mappings/Builders/ManyToOneBuilder.php +++ b/src/Mappings/Builders/ManyToOneBuilder.php @@ -1,47 +1,36 @@ config = $config; } - /** - * @param ClassMetadata $metadata - * @param ReflectionProperty $property - * @param ConfigAnnotation $annotation - */ - public function build(ClassMetadata $metadata, ReflectionProperty $property, ConfigAnnotation $annotation) + public function build(ClassMetadata $metadata, ReflectionProperty $property, MappingAttribute $attribute): void { $builder = new AssociationBuilder( new ClassMetadataBuilder($metadata), [ 'fieldName' => $property->getName(), - 'targetEntity' => $annotation->getTargetEntity($this->config), + 'targetEntity' => $attribute->getTargetEntity($this->config), ], - ClassMetadata::MANY_TO_ONE + OrmClassMetadata::MANY_TO_ONE, ); - if (isset($annotation->inversedBy) && $annotation->inversedBy) { - $builder->inversedBy($annotation->inversedBy); + if (isset($attribute->inversedBy) && $attribute->inversedBy) { + $builder->inversedBy($attribute->inversedBy); } $builder->build(); diff --git a/src/Mappings/ConfigAnnotation.php b/src/Mappings/ConfigAnnotation.php deleted file mode 100644 index 2c08c30..0000000 --- a/src/Mappings/ConfigAnnotation.php +++ /dev/null @@ -1,15 +0,0 @@ -get('acl.permissions.driver', 'config') === 'config') { - return false; - } - - return $this->targetEntity ?: $config->get('acl.permissions.entity', 'Permission'); - } -} diff --git a/src/Mappings/HasRoles.php b/src/Mappings/HasRoles.php deleted file mode 100644 index 1120cd8..0000000 --- a/src/Mappings/HasRoles.php +++ /dev/null @@ -1,28 +0,0 @@ -targetEntity ?: $config->get('acl.roles.entity', 'Role'); - } -} diff --git a/src/RegisterMappedEventSubscribers.php b/src/Mappings/RegisterMappedEventSubscribers.php similarity index 62% rename from src/RegisterMappedEventSubscribers.php rename to src/Mappings/RegisterMappedEventSubscribers.php index 5c76dc2..f7fe0e4 100644 --- a/src/RegisterMappedEventSubscribers.php +++ b/src/Mappings/RegisterMappedEventSubscribers.php @@ -1,42 +1,37 @@ > $subscribers */ + protected array $subscribers = [ BelongsToOrganisationsSubscriber::class, BelongsToOrganisationSubscriber::class, HasRolesSubscriber::class, HasPermissionsSubscriber::class, ]; - /** - * @param Configuration $configuration - * @param Connection $connection - * @param EventManager $eventManager - */ - public function extend(Configuration $configuration, Connection $connection, EventManager $eventManager) + public function extend(Configuration $configuration, Connection $connection, EventManager $eventManager): void { + $config = app(Config::class); foreach ($this->subscribers as $subscriber) { - $eventManager->addEventSubscriber( - new $subscriber( - $configuration->getMetadataDriverImpl()->getReader(), - app('config') - ) - ); + $eventManager->addEventSubscriber(new $subscriber($config)); } } } diff --git a/src/Mappings/RelationAnnotation.php b/src/Mappings/RelationAnnotation.php deleted file mode 100644 index 6482399..0000000 --- a/src/Mappings/RelationAnnotation.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ - public $cascade; - - /** - * The fetching strategy to use for the association. - * - * @var string - * - * @Enum({"LAZY", "EAGER", "EXTRA_LAZY"}) - */ - public $fetch = 'LAZY'; - - /** - * @var bool - */ - public $orphanRemoval = false; - - /** - * @var string - */ - public $indexBy; -} diff --git a/src/Mappings/Subscribers/BelongsToOrganisationSubscriber.php b/src/Mappings/Subscribers/BelongsToOrganisationSubscriber.php index 1963daa..4e75512 100644 --- a/src/Mappings/Subscribers/BelongsToOrganisationSubscriber.php +++ b/src/Mappings/Subscribers/BelongsToOrganisationSubscriber.php @@ -1,40 +1,30 @@ getInstance($metadata) instanceof BelongsToOrganisationContract; + return BelongsToOrganisation::class; } - /** - * @return string - */ - public function getAnnotationClass() + protected function shouldBeMapped(ClassMetadata $metadata): bool { - return BelongsToOrganisation::class; + return $this->getInstance($metadata) instanceof BelongsToOrganisationContract; } - /** - * @param ConfigAnnotation $annotation - * - * @return string - */ - protected function getBuilder(ConfigAnnotation $annotation) + protected function getBuilder(MappingAttribute $attribute): Builder { - return ManyToOneBuilder::class; + return new ManyToOneBuilder($this->config); } } diff --git a/src/Mappings/Subscribers/BelongsToOrganisationsSubscriber.php b/src/Mappings/Subscribers/BelongsToOrganisationsSubscriber.php index aec2857..922e458 100644 --- a/src/Mappings/Subscribers/BelongsToOrganisationsSubscriber.php +++ b/src/Mappings/Subscribers/BelongsToOrganisationsSubscriber.php @@ -1,40 +1,30 @@ getInstance($metadata) instanceof BelongsToOrganisationsContract; + return BelongsToOrganisations::class; } - /** - * @return string - */ - public function getAnnotationClass() + protected function shouldBeMapped(ClassMetadata $metadata): bool { - return BelongsToOrganisations::class; + return $this->getInstance($metadata) instanceof BelongsToOrganisationsContract; } - /** - * @param ConfigAnnotation $annotation - * - * @return string - */ - protected function getBuilder(ConfigAnnotation $annotation) + protected function getBuilder(MappingAttribute $attribute): Builder { - return ManyToManyBuilder::class; + return new ManyToManyBuilder($this->config); } } diff --git a/src/Mappings/Subscribers/HasPermissionsSubscriber.php b/src/Mappings/Subscribers/HasPermissionsSubscriber.php index 5248105..fe37c2e 100644 --- a/src/Mappings/Subscribers/HasPermissionsSubscriber.php +++ b/src/Mappings/Subscribers/HasPermissionsSubscriber.php @@ -1,47 +1,37 @@ getInstance($metadata) instanceof HasPermissionsContract; } - /** - * @return string - */ - public function getAnnotationClass() + public function getAttributeClass(): string { return HasPermissions::class; } - /** - * @param ConfigAnnotation $annotation - * - * @return string - */ - protected function getBuilder(ConfigAnnotation $annotation) + protected function getBuilder(MappingAttribute $attribute): Builder { // If there's a target entity, create pivot table - if ($annotation->getTargetEntity($this->config)) { - return ManyToManyBuilder::class; + if ($attribute->getTargetEntity($this->config)) { + return new ManyToManyBuilder($this->config); } // Else save the permissions inside the table as json - return JsonArrayBuilder::class; + return new JsonArrayBuilder($this->config); } } diff --git a/src/Mappings/Subscribers/HasRolesSubscriber.php b/src/Mappings/Subscribers/HasRolesSubscriber.php index f57da3a..b084e88 100644 --- a/src/Mappings/Subscribers/HasRolesSubscriber.php +++ b/src/Mappings/Subscribers/HasRolesSubscriber.php @@ -1,40 +1,30 @@ getInstance($metadata) instanceof HasRolesContract; + return HasRoles::class; } - /** - * @return string - */ - public function getAnnotationClass() + protected function shouldBeMapped(ClassMetadata $metadata): bool { - return HasRoles::class; + return $this->getInstance($metadata) instanceof HasRolesContract; } - /** - * @param ConfigAnnotation $annotation - * - * @return string - */ - protected function getBuilder(ConfigAnnotation $annotation) + protected function getBuilder(MappingAttribute $attribute): Builder { - return ManyToManyBuilder::class; + return new ManyToManyBuilder($this->config); } } diff --git a/src/Mappings/Subscribers/MappedEventSubscriber.php b/src/Mappings/Subscribers/MappedEventSubscriber.php index 40ea1d1..3532d75 100644 --- a/src/Mappings/Subscribers/MappedEventSubscriber.php +++ b/src/Mappings/Subscribers/MappedEventSubscriber.php @@ -1,130 +1,69 @@ */ + abstract public function getAttributeClass(): string; + + abstract protected function shouldBeMapped(ClassMetadata $metadata): bool; - /** - * @var Repository - */ - protected $config; + abstract protected function getBuilder(MappingAttribute $attribute): Builder; - /** - * @param Reader|null $reader - * @param Repository $config - */ - public function __construct(?Reader $reader, Repository $config) + public function __construct(protected Config $config) { - $this->reader = $reader; - $this->config = $config; } - /** - * {@inheritdoc} - */ - public function getSubscribedEvents() + /** @return array */ + public function getSubscribedEvents(): array { return [ Events::loadClassMetadata, ]; } - /** - * @param LoadClassMetadataEventArgs $eventArgs - */ - public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs) + public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void { $metadata = $eventArgs->getClassMetadata(); - if (! $this->reader) { + if (! $this->isInstantiable($metadata) || ! $this->shouldBeMapped($metadata)) { return; } - if ($this->isInstantiable($metadata) && $this->shouldBeMapped($metadata)) { - foreach ($metadata->getReflectionClass()->getProperties() as $property) { - if ($annotation = $this->findMapping($property)) { - $builder = $this->getBuilder($annotation); - $builder = new $builder($this->config); - $builder->build($metadata, $property, $annotation); - } + foreach ($metadata->getReflectionClass()->getProperties() as $property) { + foreach ($property->getAttributes($this->getAttributeClass()) as $refAttr) { + $attribute = $refAttr->newInstance(); + $builder = $this->getBuilder($attribute); + $builder->build($metadata, $property, $attribute); } } } - /** - * @param ClassMetadata $metadata - * - * @return bool - */ - abstract protected function shouldBeMapped(ClassMetadata $metadata); - - /** - * @return string - */ - abstract public function getAnnotationClass(); - - /** - * @param $property - * - * @return ConfigAnnotation - */ - protected function findMapping(ReflectionProperty $property) - { - return $this->reader->getPropertyAnnotation($property, $this->getAnnotationClass()); - } - - /** - * @param ClassMetadata $metadata - * - * @return object - */ - protected function getInstance(ClassMetadata $metadata) + protected function getInstance(ClassMetadata $metadata): object { $reflection = new ReflectionClass($metadata->getName()); - $instance = $reflection->newInstanceWithoutConstructor(); - return $instance; + return $reflection->newInstanceWithoutConstructor(); } - /** - * @param ConfigAnnotation $annotation - * - * @return string - */ - abstract protected function getBuilder(ConfigAnnotation $annotation); - - /** - * A MappedSuperClass or Abstract class cannot be instantiated. - * - * @param ClassMetadata $metadata - * - * @return bool - */ - protected function isInstantiable(ClassMetadata $metadata) + protected function isInstantiable(ClassMetadata $metadata): bool { if ($metadata->isMappedSuperclass) { return false; } - if (!$metadata->getReflectionClass() || $metadata->getReflectionClass()->isAbstract()) { - return false; - } - - return true; + return $metadata->getReflectionClass() && ! $metadata->getReflectionClass()->isAbstract(); } } diff --git a/src/Organisations/BelongsToOrganisation.php b/src/Organisations/BelongsToOrganisation.php index b19566e..c97ff59 100644 --- a/src/Organisations/BelongsToOrganisation.php +++ b/src/Organisations/BelongsToOrganisation.php @@ -1,63 +1,54 @@ belongsToOrganisation($o); - if ($hasOrganisation && !$requireAll) { + if ($hasOrganisation && ! $requireAll) { return true; - } elseif (!$hasOrganisation && $requireAll) { + } + + if (! $hasOrganisation && $requireAll) { return false; } } return $requireAll; - } else { - if ($this instanceof BelongsToOrganisationContract) { - if (!is_null($this->getOrganisation()) && $this->getOrganisationName($org) === $this->getOrganisation()->getName()) { - return true; - } + } + + if ($this instanceof BelongsToOrganisationContract) { + if ($this->getOrganisation() && $this->getOrganisationName($org) === $this->getOrganisation()->getName()) { + return true; } - if ($this instanceof BelongsToOrganisationsContract) { - foreach ($this->getOrganisations() as $o) { - if ($this->getOrganisationName($org) === $o->getName()) { - return true; - } + } + + if ($this instanceof BelongsToOrganisations) { + foreach ($this->getOrganisations() as $o) { + if ($this->getOrganisationName($org) === $o->getName()) { + return true; } } - - return false; } + + return false; } - /** - * @param OrganisationContract|string $org - * - * @return string - */ - protected function getOrganisationName($org) + protected function getOrganisationName(Organisation|string $org): string { - return $org instanceof OrganisationContract ? $org->getName() : $org; + return $org instanceof Organisation ? $org->getName() : $org; } } diff --git a/src/Permissions/PermissionManager.php b/src/PermissionManager.php similarity index 50% rename from src/Permissions/PermissionManager.php rename to src/PermissionManager.php index e7d9ba5..f488619 100644 --- a/src/Permissions/PermissionManager.php +++ b/src/PermissionManager.php @@ -1,42 +1,44 @@ */ + public function getPermissionsWithDotNotation(): array { $permissions = $this->driver()->getAllPermissions(); $list = $this->convertToDotArray( - $permissions->toArray() + $permissions->toArray(), ); return Arr::flatten($list); } /** - * @param array|string $permissions - * @param string $prepend + * @param array|string $permissions * - * @return array + * @return array */ - protected function convertToDotArray($permissions, $prepend = '') + protected function convertToDotArray(array|string $permissions, string $prepend = ''): array { $list = []; if (is_array($permissions)) { foreach ($permissions as $key => $permission) { - $list[] = $this->convertToDotArray($permission, (!is_numeric($key)) ? $prepend . $key . '.' : $prepend); + $list[] = $this->convertToDotArray($permission, ! is_numeric($key) ? $prepend . $key . '.' : $prepend); } } else { $list[] = $prepend . $permissions; @@ -47,48 +49,35 @@ protected function convertToDotArray($permissions, $prepend = '') /** * Get the default driver name. - * @return string */ - public function getDefaultDriver() + public function getDefaultDriver(): string { return $this->container->make('config')->get('acl.permissions.driver', 'config'); } - /** - * @return string - */ - public function getNamespace() + public function getNamespace(): string { - return __NAMESPACE__; + return __NAMESPACE__ . '\\Configurations'; } - /** - * @return string - */ - public function getClassSuffix() + public function getClassSuffix(): string { - return 'PermissionDriver'; + return 'PermissionsProvider'; } - /** - * @return bool - */ - public function useDefaultPermissionEntity() + public function useDefaultPermissionEntity(): bool { - if (!$this->needsDoctrine()) { + if (! $this->needsDoctrine()) { return false; } $entityFqn = $this->container->make('config')->get('acl.permissions.entity', ''); - $entityFqn = ltrim($entityFqn, "\\"); + $entityFqn = ltrim($entityFqn, '\\'); return $entityFqn === Permission::class; } - /** - * @return bool - */ - public function needsDoctrine() + public function needsDoctrine(): bool { return $this->getDefaultDriver() === 'doctrine'; } diff --git a/src/Permissions/ConfigPermissionDriver.php b/src/Permissions/ConfigPermissionDriver.php deleted file mode 100644 index c0c0fdf..0000000 --- a/src/Permissions/ConfigPermissionDriver.php +++ /dev/null @@ -1,32 +0,0 @@ -repository = $repository; - } - - /** - * @return Collection - */ - public function getAllPermissions() - { - return new Collection( - $this->repository->get('acl.permissions.list', []) - ); - } -} diff --git a/src/Permissions/DoctrinePermissionDriver.php b/src/Permissions/DoctrinePermissionDriver.php deleted file mode 100644 index b663691..0000000 --- a/src/Permissions/DoctrinePermissionDriver.php +++ /dev/null @@ -1,101 +0,0 @@ -registry = $registry; - $this->config = $config; - } - - /** - * @return Collection - */ - public function getAllPermissions() - { - if ($this->getRepository()) { - try { - $permissions = $this->getRepository()->findAll(); - } catch (TableNotFoundException $e) { - $permissions = []; - } - - return new Collection( - $this->mapToArrayOfNames($permissions) - ); - } - - return new Collection; - } - - /** - * @param $permissions - * - * @return array - */ - protected function mapToArrayOfNames($permissions) - { - $permissions = array_map(function (Permission $permission) { - return $permission->getName(); - }, $permissions); - - return $permissions; - } - - /** - * @return string - */ - protected function getEntityName() - { - return $this->config->get('acl.permissions.entity'); - } - - /** - * @return EntityManagerInterface|null - */ - protected function getEntityManager() - { - return $this->registry->getManagerForClass( - $this->getEntityName() - ); - } - - /** - * @return EntityRepository - */ - protected function getRepository() - { - if ($this->getEntityManager()) { - $metadata = $this->getEntityManager()->getClassMetadata($this->getEntityName()); - - return new EntityRepository( - $this->getEntityManager(), - $metadata - ); - } - } -} diff --git a/src/Permissions/Driver/Config.php b/src/Permissions/Driver/Config.php new file mode 100644 index 0000000..4945e99 --- /dev/null +++ b/src/Permissions/Driver/Config.php @@ -0,0 +1,23 @@ + */ + public function __construct(array $permissions) + { + $this->collection = new Collection($permissions); + } + + public function getAllPermissions(): Collection + { + return $this->collection; + } +} diff --git a/src/Permissions/Driver/Doctrine.php b/src/Permissions/Driver/Doctrine.php new file mode 100644 index 0000000..446e9b2 --- /dev/null +++ b/src/Permissions/Driver/Doctrine.php @@ -0,0 +1,27 @@ +repository->findAll(); + $permissions = array_map(static fn (Permission $permission) => $permission->getName(), $permissions); + + return new Collection($permissions); + } +} diff --git a/src/Permissions/Driver/PermissionDriver.php b/src/Permissions/Driver/PermissionDriver.php new file mode 100644 index 0000000..c3853dc --- /dev/null +++ b/src/Permissions/Driver/PermissionDriver.php @@ -0,0 +1,12 @@ +hasPermissionTo($n); - - if ($hasPermission && !$requireAll) { - return true; - } elseif (!$hasPermission && $requireAll) { - return false; - } - } - - return $requireAll; - } else { - if ($this instanceof HasPermissionsContract) { - foreach ($this->getPermissions() as $permission) { - if ($this->getPermissionName($permission) === $this->getPermissionName($name)) { - return true; - } - } - } - - if ($this instanceof HasRolesHasRoles) { - foreach ($this->getRoles() as $role) { - if ($role instanceof HasPermissionsContract) { - if ($role->hasPermissionTo($name)) { - return true; - } - } - } - } - - return false; - } - } - - /** - * @param PermissionContract|string $permission - * - * @return string - */ - protected function getPermissionName($permission) - { - return $permission instanceof PermissionContract ? $permission->getName() : $permission; - } -} diff --git a/src/Permissions/Permission.php b/src/Permissions/Permission.php index f1f4b9f..31807a8 100644 --- a/src/Permissions/Permission.php +++ b/src/Permissions/Permission.php @@ -1,56 +1,42 @@ name = $name; } - /** - * @return mixed - */ - public function getId() + public function getId(): int|null { return $this->id; } - /** - * @return string - */ - public function getName() + public function getName(): string { return $this->name; } - /** - * @param mixed $name - */ - public function setName($name) + public function setName(string $name): self { $this->name = $name; + + return $this; } } diff --git a/src/Permissions/PermissionDriver.php b/src/Permissions/PermissionDriver.php deleted file mode 100644 index 3cd60db..0000000 --- a/src/Permissions/PermissionDriver.php +++ /dev/null @@ -1,13 +0,0 @@ -hasPermissionTo($n); + + if ($hasPermission && ! $requireAll) { + return true; + } + + if (! $hasPermission && $requireAll) { + return false; + } + } + + return $requireAll; + } + + if ($this instanceof HasPermissionsContract) { + foreach ($this->getPermissions() as $permission) { + if ($this->getPermissionName($permission) === $this->getPermissionName($name)) { + return true; + } + } + } + + if ($this instanceof HasRoles) { + foreach ($this->getRoles() as $role) { + if (! ($role instanceof HasPermissionsContract)) { + continue; + } + + if ($role->hasPermissionTo($name)) { + return true; + } + } + } + + return false; + } + + protected function getPermissionName(Permission|string $permission): string + { + return $permission instanceof Permission ? $permission->getName() : $permission; + } +} diff --git a/src/Roles/HasRoles.php b/src/Roles/HasRoles.php deleted file mode 100644 index b1e1101..0000000 --- a/src/Roles/HasRoles.php +++ /dev/null @@ -1,73 +0,0 @@ -hasRole($r); - - if ($hasRole && !$requireAll) { - return true; - } elseif (!$hasRole && $requireAll) { - return false; - } - } - - return $requireAll; - } else { - foreach ($this->getRoles() as $ownedRole) { - if ($ownedRole === $role) { - return true; - } - } - } - - return false; - } - - /** - * @param string|array $name - * @param bool $requireAll - * @return bool - */ - public function hasRoleByName($name, $requireAll = false) - { - if (is_array($name)) { - foreach ($name as $n) { - $hasRole = $this->hasRoleByName($n); - - if ($hasRole && !$requireAll) { - return true; - } elseif (!$hasRole && $requireAll) { - return false; - } - } - - return $requireAll; - } else { - foreach ($this->getRoles() as $ownedRole) { - if ($ownedRole->getName() === $name) { - return true; - } - } - } - - return false; - } - - /** - * @return ArrayCollection|Role[] - */ - abstract public function getRoles(); -} diff --git a/src/Roles/WithRoles.php b/src/Roles/WithRoles.php new file mode 100644 index 0000000..87dab1f --- /dev/null +++ b/src/Roles/WithRoles.php @@ -0,0 +1,70 @@ +hasRole($r); + + if ($hasRole && ! $requireAll) { + return true; + } + + if (! $hasRole && $requireAll) { + return false; + } + } + + return $requireAll; + } + + foreach ($this->getRoles() as $ownedRole) { + if ($ownedRole === $role) { + return true; + } + } + + return false; + } + + public function hasRoleByName(string|array $name, bool $requireAll = false): bool + { + if (is_array($name)) { + foreach ($name as $n) { + $hasRole = $this->hasRoleByName($n); + + if ($hasRole && ! $requireAll) { + return true; + } + + if (! $hasRole && $requireAll) { + return false; + } + } + + return $requireAll; + } + + foreach ($this->getRoles() as $ownedRole) { + if ($ownedRole->getName() === $name) { + return true; + } + } + + return false; + } + + /** @return Collection|Role[] */ + abstract public function getRoles(): Collection|array; +} diff --git a/stubs/Organisation.php b/stubs/Organisation.php new file mode 100644 index 0000000..8094dc9 --- /dev/null +++ b/stubs/Organisation.php @@ -0,0 +1,37 @@ +id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } +} diff --git a/stubs/Permission.php b/stubs/Permission.php new file mode 100644 index 0000000..709a47a --- /dev/null +++ b/stubs/Permission.php @@ -0,0 +1,37 @@ +id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } +} + + diff --git a/stubs/Role.php b/stubs/Role.php new file mode 100644 index 0000000..3daf4e8 --- /dev/null +++ b/stubs/Role.php @@ -0,0 +1,71 @@ + */ + #[ACL\HasPermissions] + public Collection $permissions; + + public function __construct() + { + $this->permissions = new ArrayCollection(); + } + + public function getId(): int|null + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + /** @return Collection */ + public function getPermissions(): Collection + { + return $this->permissions; + } + + /** @param Collection|Permission[] $permissions */ + public function setPermissions(Collection|array $permissions): self + { + $this->permissions = is_array($permissions) ? new ArrayCollection($permissions) : $permissions; + + return $this; + } +} + + diff --git a/testbench.yaml b/testbench.yaml new file mode 100644 index 0000000..2fedee6 --- /dev/null +++ b/testbench.yaml @@ -0,0 +1,16 @@ +laravel: ./workbench +workbench: + start: '/' + install: true + health: false + discovers: + web: false + api: false + commands: false + components: false + views: false + build: + - asset-publish + - create-sqlite-db + - db-wipe + sync: [] diff --git a/tests/Configurations/DoctrinePermissionsProviderTest.php b/tests/Configurations/DoctrinePermissionsProviderTest.php new file mode 100644 index 0000000..788caaf --- /dev/null +++ b/tests/Configurations/DoctrinePermissionsProviderTest.php @@ -0,0 +1,56 @@ +shouldReceive('get')->with('acl.permissions.entity')->andReturn(null); + + $provider = new DoctrinePermissionsProvider($registry, $config); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Failed to configure doctrine permissions. No entity class provided.'); + // Call protected method via reflection + $reflection = new ReflectionClass($provider); + $method = $reflection->getMethod('getPermissionClass'); + $method->setAccessible(true); + $method->invoke($provider); + } + + public function testThrowsExceptionWhenNoEntityManagerFound(): void + { + $registry = m::mock(ManagerRegistry::class); + $config = m::mock(Repository::class); + $config->shouldReceive('get')->with('acl.permissions.entity')->andReturn('Some\\Entity\\Class'); + $registry->shouldReceive('getManagerForClass')->with('Some\\Entity\\Class')->andReturn(null); + + $provider = new DoctrinePermissionsProvider($registry, $config); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Failed to configure doctrine permissions. No entity manager found for entity: Some\\Entity\\Class.'); + // Call protected method via reflection + $reflection = new ReflectionClass($provider); + $method = $reflection->getMethod('getEntityManager'); + $method->setAccessible(true); + $method->invoke($provider); + } +} diff --git a/tests/Configurations/PermissionManagerTest.php b/tests/Configurations/PermissionManagerTest.php new file mode 100644 index 0000000..1735d9c --- /dev/null +++ b/tests/Configurations/PermissionManagerTest.php @@ -0,0 +1,177 @@ +driver = m::mock(PermissionDriver::class); + + $this->container = m::mock(Container::class); + + $this->manager = new PermissionManager($this->container); + $this->manager->extend('config', function () { + return $this->driver; + }); + } + + protected function tearDown(): void + { + m::close(); + } + + public function testCanDotNotatedArrayOfPermissions(): void + { + $this->driver->shouldReceive('getAllPermissions')->once()->andReturn(new Collection([ + 'permissionKey2' => [ + 'permissionValue1', + 'permissionValue2', + ], + 'permissionKey3' => [ + 'permissionKey4' => [ + 'permissionValue3', + 'permissionValue4', + ], + ], + ])); + + $config = m::mock(Repository::class); + + $this->container->shouldReceive('make')->with('config')->andReturn($config); + + $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); + + $this->assertEquals([ + 'permissionKey2.permissionValue1', + 'permissionKey2.permissionValue2', + 'permissionKey3.permissionKey4.permissionValue3', + 'permissionKey3.permissionKey4.permissionValue4', + ], $this->manager->getPermissionsWithDotNotation()); + } + + public function testWhenShouldUseDefaultPermissionEntity(): void + { + $config = m::mock(Repository::class); + + $this->container->shouldReceive('make')->with('config')->andReturn($config); + + $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('doctrine'); + + // Tests for leading slashes in case someone is providing a manually written FQN + $config->shouldReceive('get')->with('acl.permissions.entity', null)->andReturn('\\' . Permission::class); + + $this->assertTrue($this->manager->useDefaultPermissionEntity()); + } + + public function testWhenShouldNotUseDefaultPermissionEntityBecauseDriverIsNotDoctrine(): void + { + $config = m::mock(Repository::class); + + $this->container->shouldReceive('make')->with('config')->andReturn($config); + + $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); + $config->shouldReceive('get')->with('acl.permissions.entity', null)->andReturn(Permission::class); + + $this->assertFalse($this->manager->useDefaultPermissionEntity()); + } + + public function testWhenShouldNotUseDefaultPermissionEntityBecauseEntityIsDifferent(): void + { + $config = m::mock(Repository::class); + + $this->container->shouldReceive('make')->with('config')->andReturn($config); + + $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); + $config->shouldReceive('get')->with('acl.permissions.entity', null)->andReturn('Namespace\Class'); + + $this->assertFalse($this->manager->useDefaultPermissionEntity()); + } + + public function testNeedsDoctrine(): void + { + $config = m::mock(Repository::class); + + $this->container->shouldReceive('make')->with('config')->andReturn($config); + + $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('doctrine'); + + $this->assertTrue($this->manager->needsDoctrine()); + } + + public function testDoesNotNeedDoctrine(): void + { + $config = m::mock(Repository::class); + + $this->container->shouldReceive('make')->with('config')->andReturn($config); + + $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); + + $this->assertFalse($this->manager->needsDoctrine()); + } + + public function testThrowsDriverNotFoundException(): void + { + $this->expectException(DriverNotFound::class); + $manager = new PermissionManager($this->container); + // Do not extend with any driver, so the requested driver does not exist + $manager->driver('nonexistent'); + } + + public function testDoctrinePermissionsProviderThrowsExceptionWhenNoEntityClassProvided(): void + { + $registry = m::mock(ManagerRegistry::class); + $config = m::mock(Repository::class); + $config->shouldReceive('get')->with('acl.permissions.entity')->andReturn(null); + + $provider = new DoctrinePermissionsProvider($registry, $config); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Failed to configure doctrine permissions. No entity class provided.'); + // Call protected method via reflection + $reflection = new ReflectionClass($provider); + $method = $reflection->getMethod('getPermissionClass'); + $method->setAccessible(true); + $method->invoke($provider); + } + + public function testDoctrinePermissionsProviderThrowsExceptionWhenNoEntityManagerFound(): void + { + $registry = m::mock(ManagerRegistry::class); + $config = m::mock(Repository::class); + $config->shouldReceive('get')->with('acl.permissions.entity')->andReturn('Some\\Entity\\Class'); + $registry->shouldReceive('getManagerForClass')->with('Some\\Entity\\Class')->andReturn(null); + + $provider = new DoctrinePermissionsProvider($registry, $config); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Failed to configure doctrine permissions. No entity manager found for entity: Some\\Entity\\Class.'); + // Call protected method via reflection + $reflection = new ReflectionClass($provider); + $method = $reflection->getMethod('getEntityManager'); + $method->setAccessible(true); + $method->invoke($provider); + } +} diff --git a/tests/Integration/AclServiceProviderTest.php b/tests/Integration/AclServiceProviderTest.php new file mode 100644 index 0000000..200cb30 --- /dev/null +++ b/tests/Integration/AclServiceProviderTest.php @@ -0,0 +1,74 @@ +createMock(PermissionManager::class); + $manager->method('getPermissionsWithDotNotation')->willReturn(['foo.bar', 'baz.qux']); + $this->app->instance(PermissionManager::class, $manager); + + // Get the Gate + $gate = $this->app->make(Gate::class); + + // Assert: Gate has the permissions defined + $this->assertTrue($gate->has('foo.bar')); + $this->assertTrue($gate->has('baz.qux')); + + $user = entity(User::class)->create(); + $user->setPermissions(['foo.bar']); + $this->actingAs($user); + + $this->assertTrue($gate->allows('foo.bar')); + $this->assertFalse($gate->allows('baz.quxdkdkd')); + } + + public function testNoPermissionsDefinedWhenManagerReturnsEmpty(): void + { + // $manager = $this->createMock(PermissionManager::class); + // $manager->method('getPermissionsWithDotNotation')->willReturn([]); + // $this->app->instance(PermissionManager::class, $manager); + + $gate = $this->app->make(Gate::class); + + $this->assertFalse($gate->has('any.permission')); + } + + public function testRegisterPathsSkipsWhenNotUsingDefaultPermissionEntity(): void + { + // Arrange: Mock PermissionManager + $manager = Mockery::mock(PermissionManager::class); + $manager->shouldReceive('useDefaultPermissionEntity')->once()->andReturn(false); + + // We expect that DoctrineManager::addPaths should NOT be called + $doctrineManager = Mockery::mock(DoctrineManager::class); + $doctrineManager->shouldNotReceive('addPaths'); + + $this->app->instance(PermissionManager::class, $manager); + $this->app->instance(DoctrineManager::class, $doctrineManager); + + // Act: Call registerPaths via reflection + $provider = $this->app->getProvider(AclServiceProvider::class); + $reflection = new ReflectionClass($provider); + $method = $reflection->getMethod('registerPaths'); + $method->setAccessible(true); + + $this->assertNull($method->invoke($provider)); + } +} diff --git a/tests/Integration/ConfigPermissionDriverTest.php b/tests/Integration/ConfigPermissionDriverTest.php new file mode 100644 index 0000000..62463c7 --- /dev/null +++ b/tests/Integration/ConfigPermissionDriverTest.php @@ -0,0 +1,44 @@ +set('acl.permissions.driver', 'config'); + } + + public function testPermissionsAreLoadedFromConfig(): void + { + $em = $this->app->make(EntityManager::class); + + $user = entity(UserJsonPermissions::class)->create(); + $user->setPermissions(['role.attach']); + + $em->persist($user); + $em->flush(); + + $this->actingAs($user); + + $gate = $this->app->make(Gate::class); + + $this->assertTrue($gate->allows('role.attach')); + $this->assertFalse($gate->allows('no_permission')); + } +} diff --git a/tests/Integration/DoctrinePermissionPersistenceTest.php b/tests/Integration/DoctrinePermissionPersistenceTest.php new file mode 100644 index 0000000..bc18e77 --- /dev/null +++ b/tests/Integration/DoctrinePermissionPersistenceTest.php @@ -0,0 +1,103 @@ +getRepository(User::class); + + // Create user, role, organisation, and permission + $user = entity(User::class)->create(); + $role = new Role('test-role'); + $organisation = new Organisation('test-org'); + $permission = new Permission('persisted.permission'); + $permission->setName('persisted.permission'); // Just for test coverage + $role->getPermissions()->add($permission); + $user->getRoles()->add($role); + $user->getOrganisations()->add($organisation); + $user->getPermissions()->add($permission); + + // Persist all entities + $em->persist($permission); + $em->persist($role); + $em->persist($organisation); + $em->persist($user); + $em->flush(); + $em->clear(); + + $reloaded = $repo->findOneBy(['email' => $user->email]); + assert($reloaded instanceof User); + $this->assertNotNull($reloaded); + $this->assertInstanceOf(User::class, $reloaded); + + // Permissions + $permissions = $reloaded->getPermissions(); + $this->assertInstanceOf(Collection::class, $permissions); + $this->assertTrue($permissions->exists(static fn ($key, $perm) => $perm->getName() === 'persisted.permission')); + + $reloadedPermission = $permissions->filter(static fn ($perm) => $perm->getName() === 'persisted.permission')->first(); + assert($reloadedPermission instanceof Permission); + $this->assertNotNull($reloadedPermission); + $this->assertInstanceOf(Permission::class, $reloadedPermission); + $this->assertIsNumeric($reloadedPermission->getId()); + + // Roles + $roles = $reloaded->getRoles(); + $this->assertInstanceOf(Collection::class, $roles); + $this->assertTrue($roles->exists(static fn ($key, $role) => $role->getName() === 'test-role')); + $reloadedRole = $roles->filter(static fn ($role) => $role->getName() === 'test-role')->first(); + $this->assertNotNull($reloadedRole); + $this->assertTrue($reloadedRole->getPermissions()->exists(static fn ($key, $perm) => $perm->getName() === 'persisted.permission')); + + // Organisations + $organisations = $reloaded->getOrganisations(); + $this->assertInstanceOf(Collection::class, $organisations); + $this->assertTrue($organisations->exists(static fn ($key, $org) => $org->getName() === 'test-org')); + } + + public function testUserSingleOrgPermissionsArrayPersistence(): void + { + $em = app(EntityManagerInterface::class); + assert($em instanceof EntityManagerInterface); + $repo = $em->getRepository(UserSingleOrg::class); + + // Create user and permission + $user = entity(UserSingleOrg::class)->create(); + $user->setPermissions(['array.permission']); + + // Persist entities + $em->persist($user); + $em->flush(); + $em->clear(); + + $reloaded = $repo->findOneBy(['email' => $user->email]); + assert($reloaded instanceof UserSingleOrg); + $this->assertNotNull($reloaded); + $this->assertInstanceOf(UserSingleOrg::class, $reloaded); + + // Permissions should be loaded as an array + $permissions = $reloaded->getPermissions(); + $this->assertIsArray($permissions); + $this->assertNotEmpty($permissions); + $this->assertEquals(['array.permission'], $permissions); + } +} diff --git a/tests/LaravelSetupTest.php b/tests/LaravelSetupTest.php new file mode 100644 index 0000000..69ca6ec --- /dev/null +++ b/tests/LaravelSetupTest.php @@ -0,0 +1,24 @@ +assertNotNull($this->app); + $this->assertInstanceOf(Application::class, $this->app); + $this->assertEquals('testing', $this->app->environment()); + + $user = entity(User::class)->create(); + + $this->actingAs($user); + } +} diff --git a/tests/Organisations/BelongsToOrganisationTest.php b/tests/Organisations/BelongsToOrganisationTest.php index 12b8dca..da9719d 100644 --- a/tests/Organisations/BelongsToOrganisationTest.php +++ b/tests/Organisations/BelongsToOrganisationTest.php @@ -1,207 +1,73 @@ user = new UserMock3; - $this->userSingle = new UserMock4; - $this->orgMock1 = new OrgMock('org1'); - $this->orgMock2 = new OrgMock('org2'); - $this->orgMock3 = new OrgMock('org3'); + parent::setUp(); + + $this->user = entity(User::class)->create(); + $this->userSingle = entity(UserSingleOrg::class)->create(); + $this->orgMock1 = entity(Organisation::class)->create(['name' => 'org1']); + $this->orgMock2 = entity(Organisation::class)->create(['name' => 'org2']); + $this->orgMock3 = entity(Organisation::class)->create(['name' => 'org3']); } - public function test_doesnt_have_organisation_when_no_organisations_assigned_single(): void + public function testBelongsToOrganisationVariousCases(): void { + // No organisations assigned (single org user) $this->assertFalse($this->userSingle->belongsToOrganisation($this->orgMock1)); - } - public function test_doesnt_have_organisation_when_no_organisations_assigned(): void - { - $this->assertFalse($this->user->belongsToOrganisation($this->orgMock1)); - } + // Assign an organisation to userSingle and check positive/negative cases + $this->userSingle->setOrganisation($this->orgMock1); + $this->assertTrue($this->userSingle->belongsToOrganisation($this->orgMock1)); + $this->assertTrue($this->userSingle->belongsToOrganisation('org1')); + $this->assertFalse($this->userSingle->belongsToOrganisation($this->orgMock2)); + $this->assertFalse($this->userSingle->belongsToOrganisation('org2')); - public function test_doesnt_have_role_by_name_when_no_roles_assigned(): void - { + // No organisations assigned (multi org user) + $this->assertFalse($this->user->belongsToOrganisation($this->orgMock1)); $this->assertFalse($this->user->belongsToOrganisation('org1')); - } - public function test_doesnt_have_organisation_when_when_other_orgiansation_assigned(): void - { - $this->user->setOrganisations([ - new OrgMock('org4'), - ]); + // Other organisation assigned + $this->user->setOrganisations([entity(Organisation::class)->create(['name' => 'org4'])]); $this->assertFalse($this->user->belongsToOrganisation($this->orgMock1)); - } - public function test_doesnt_have_any_organisations_when_organisation_assigned(): void - { - $this->user->setOrganisations([ - $this->orgMock1 - ]); + // Organisation assigned, check any/all/none by object and name + $this->user->setOrganisations([$this->orgMock1]); $this->assertFalse($this->user->belongsToOrganisation([$this->orgMock2, $this->orgMock3])); - } - - public function test_doesnt_have_any_organisation_by_name_when_organisation_assigned(): void - { - $this->user->setOrganisations([ - $this->orgMock1 - ]); $this->assertFalse($this->user->belongsToOrganisation(['org2', 'org3'])); - } + $this->assertTrue($this->user->belongsToOrganisation($this->orgMock1)); + $this->assertTrue($this->user->belongsToOrganisation('org1')); - public function test_doesnt_have_all_organisations_when_organisations_assigned(): void - { - $this->user->setOrganisations([ - $this->orgMock1, - $this->orgMock2 - ]); + // Two organisations assigned + $this->user->setOrganisations([$this->orgMock1, $this->orgMock2]); $this->assertFalse($this->user->belongsToOrganisation([$this->orgMock1, $this->orgMock2, $this->orgMock3], true)); - } - - public function test_doesnt_have_all_organisations_by_name_when_organisations_assigned(): void - { - $this->user->setOrganisations([ - $this->orgMock1, - $this->orgMock2 - ]); $this->assertFalse($this->user->belongsToOrganisation(['org1', 'org2', 'org3'], true)); - } - - public function test_has_organisation_when_when_organisation_assigned(): void - { - $this->user->setOrganisations([ - $this->orgMock1, - ]); - $this->assertTrue($this->user->belongsToOrganisation($this->orgMock1)); - } - public function test_has_organisation_by_name_when_when_organisation_assigned(): void - { - $this->user->setOrganisations([ - $this->orgMock1, - ]); - $this->assertTrue($this->user->belongsToOrganisation('org1')); - } - - public function test_has_any_organisation_when_organisation_assigned(): void - { - $this->user->setOrganisations([ - $this->orgMock1, - $this->orgMock2, - $this->orgMock3 - ]); + // Three organisations assigned + $this->user->setOrganisations([$this->orgMock1, $this->orgMock2, $this->orgMock3]); $this->assertTrue($this->user->belongsToOrganisation([$this->orgMock1, $this->orgMock2])); - } - - public function test_has_all_organisations_when_organisations_assigned(): void - { - $this->user->setOrganisations([ - $this->orgMock1, - $this->orgMock2, - $this->orgMock3 - ]); $this->assertTrue($this->user->belongsToOrganisation([$this->orgMock1, $this->orgMock2, $this->orgMock3], true)); - } - - public function test_has_any_organisation_by_name_when_organisation_assigned(): void - { - $this->user->setOrganisations([ - $this->orgMock1, - $this->orgMock2, - $this->orgMock3 - ]); $this->assertTrue($this->user->belongsToOrganisation(['org1', 'org4'])); - } - - public function test_has_all_organisations_by_name_when_organisations_assigned(): void - { - $this->user->setOrganisations([ - $this->orgMock1, - $this->orgMock2, - $this->orgMock3 - ]); $this->assertTrue($this->user->belongsToOrganisation(['org1', 'org2', 'org3'], true)); } } - -class UserMock3 implements \LaravelDoctrine\ACL\Contracts\BelongsToOrganisations -{ - use \LaravelDoctrine\ACL\Organisations\BelongsToOrganisation; - - protected $organisations = []; - - public function getOrganisations() - { - return $this->organisations; - } - - public function setOrganisations($orgs): void - { - $this->organisations = $orgs; - } -} - -class UserMock4 implements \LaravelDoctrine\ACL\Contracts\BelongsToOrganisation -{ - use \LaravelDoctrine\ACL\Organisations\BelongsToOrganisation; - - protected $organisation; - - public function getOrganisation() - { - return $this->organisation; - } - - public function setOrganisation($org) - { - $this->organisation = $org; - } -} - -class OrgMock implements \LaravelDoctrine\ACL\Contracts\Organisation -{ - /** - * @var string - */ - protected $name; - - /** - * @param $name - */ - public function __construct($name) - { - $this->name = $name; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } -} diff --git a/tests/Permissions/ConfigPermissionDriverTest.php b/tests/Permissions/ConfigPermissionDriverTest.php deleted file mode 100644 index 258cd32..0000000 --- a/tests/Permissions/ConfigPermissionDriverTest.php +++ /dev/null @@ -1,34 +0,0 @@ -config = m::mock(Repository::class); - $this->driver = new ConfigPermissionDriver($this->config); - } - - public function test_can_get_all_permissions(): void - { - $this->config->shouldReceive('get')->with('acl.permissions.list', [])->once()->andReturn(['mocked']); - - $permissions = $this->driver->getAllPermissions(); - $this->assertInstanceOf(Collection::class, $permissions); - $this->assertTrue($permissions->contains('mocked')); - } -} diff --git a/tests/Permissions/DoctrinePermissionDriverTest.php b/tests/Permissions/DoctrinePermissionDriverTest.php deleted file mode 100644 index e0b59f6..0000000 --- a/tests/Permissions/DoctrinePermissionDriverTest.php +++ /dev/null @@ -1,103 +0,0 @@ -config = m::mock(Repository::class); - $this->registry = m::mock(ManagerRegistry::class); - $this->em = m::mock(EntityManagerInterface::class); - $this->driver = new DoctrinePermissionDriver($this->registry, $this->config); - } - - public function test_can_get_all_permissions(): void - { - $this->config->shouldReceive('get')->with('acl.permissions.entity')->once()->andReturn(Permission::class); - - $this->registry->shouldReceive('getManagerForClass')->with(Permission::class)->once()->andReturn($this->em); - - $this->em->shouldReceive('getUnitOfWork')->once()->andReturn($this->em); - $this->em->shouldReceive('getEntityPersister')->with(Permission::class)->once()->andReturn($this->em); - $this->em->shouldReceive('loadAll')->once()->andReturn([ - new Permission('mocked'), - ]); - - $meta = new ClassMetadata(Permission::class); - $meta->table = [ - 'name' => 'permissions', - ]; - $this->em->shouldReceive('getClassMetadata')->once()->andReturn($meta); - - $permissions = $this->driver->getAllPermissions(); - $this->assertInstanceOf(Collection::class, $permissions); - $this->assertTrue($permissions->contains('mocked')); - } - - public function test_should_not_fail_when_table_does_not_exist(): void - { - $this->config->shouldReceive('get')->with('acl.permissions.entity')->once()->andReturn(Permission::class); - - $this->registry->shouldReceive('getManagerForClass')->with(Permission::class)->once()->andReturn($this->em); - - $this->em->shouldReceive('getUnitOfWork')->once()->andReturn($this->em); - $this->em->shouldReceive('getEntityPersister')->with(Permission::class)->once()->andReturn($this->em); - - - - if (class_exists(MysqliException::class)) { - $driver = new Driver(); - $exception = new MysqliException('Base table or view not found: 1146 Table \'permissions\' doesn\'t exist', 1146, 1146); - $tableNotFoundException = DBALException::driverExceptionDuringQuery($driver, $exception, 'SELECT t0.id AS id_1, t0.name AS name_2, t0.modules AS modules_3 FROM permissions t0'); - - $this->em->shouldReceive('loadAll')->once()->andThrow($tableNotFoundException); - } else { - // DBAL 3 removed MysqliException - $this->em->shouldReceive('loadAll')->once()->andThrow(new \Doctrine\DBAL\Exception\TableNotFoundException( - new \Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionFailed('Table not found'), null) - ); - } - - $meta = new ClassMetadata(Permission::class); - $meta->table = [ - 'name' => 'permissions', - ]; - $this->em->shouldReceive('getClassMetadata')->once()->andReturn($meta); - - $permissions = $this->driver->getAllPermissions(); - $this->assertInstanceOf(Collection::class, $permissions); - $this->assertTrue($permissions->isEmpty()); - } -} diff --git a/tests/Permissions/Driver/ConfigPermissionDriverTest.php b/tests/Permissions/Driver/ConfigPermissionDriverTest.php new file mode 100644 index 0000000..d750c06 --- /dev/null +++ b/tests/Permissions/Driver/ConfigPermissionDriverTest.php @@ -0,0 +1,20 @@ +assertTrue($emptyConfig->getAllPermissions()->isEmpty()); + + $config = new Config(['mocked']); + $this->assertTrue($config->getAllPermissions()->contains('mocked')); + } +} diff --git a/tests/Permissions/Driver/DoctrinePermissionDriverTest.php b/tests/Permissions/Driver/DoctrinePermissionDriverTest.php new file mode 100644 index 0000000..be2f6dd --- /dev/null +++ b/tests/Permissions/Driver/DoctrinePermissionDriverTest.php @@ -0,0 +1,27 @@ +shouldReceive('findAll')->andReturn([ + new Permission('mocked'), + ]); + + $driver = new Doctrine($repository); + + $permissions = $driver->getAllPermissions(); + $this->assertTrue($permissions->contains('mocked')); + } +} diff --git a/tests/Permissions/HasPermissionsTest.php b/tests/Permissions/HasPermissionsTest.php index 51fefa5..1baf792 100644 --- a/tests/Permissions/HasPermissionsTest.php +++ b/tests/Permissions/HasPermissionsTest.php @@ -1,273 +1,169 @@ user = new UserMock; - $this->userWithRoles = new UserMockWithRoles; + parent::setUp(); + + $this->user = entity(User::class)->create(); } - public function test_doesnt_have_permission_when_no_roles_and_no_permissions(): void + public function testDoesntHavePermissionWhenNoRolesAndNoPermissions(): void { $this->assertFalse($this->user->hasPermissionTo('create.post')); } - public function test_doesnt_have_permission_when_no_roles_with_other_permissions(): void + public function testDoesntHavePermissionWhenNoRolesWithOtherPermissions(): void { - $this->user->setPermissions([ - 'create.page', - ]); + $this->user->setPermissions(['create.page']); $this->assertFalse($this->user->hasPermissionTo('create.post')); } - public function test_doesnt_have_permission_with_roles_and_other_permissions(): void + public function testDoesntHavePermissionWithRolesAndOtherPermissions(): void { - $this->userWithRoles->setRoles([ - new RoleMock, + $this->user->setRoles([ + entity(Role::class)->make(), ]); - $this->userWithRoles->setPermissions([ - 'create.page', - ]); + $this->user->setPermissions(['create.page']); - $this->assertFalse($this->userWithRoles->hasPermissionTo('create.post')); + $this->assertFalse($this->user->hasPermissionTo('create.post')); } - public function test_doesnt_have_permission_with_roles_with_other_permissions_and_other_permissions(): void + public function testDoesntHavePermissionWithRolesWithOtherPermissionsAndOtherPermissions(): void { - $role = new RoleMock; - $role->setPermissions([ - 'create.page', - ]); + $role = entity(Role::class)->make(); + $role->setPermissions(['create.page']); - $this->userWithRoles->setRoles([ - $role, - ]); + $this->user->setRoles([$role]); - $this->userWithRoles->setPermissions([ - 'create.page', - ]); + $this->user->setPermissions(['create.page']); - $this->assertFalse($this->userWithRoles->hasPermissionTo('create.post')); + $this->assertFalse($this->user->hasPermissionTo('create.post')); } - public function test_doesnt_have_permission_with_permission_but_no_other_permissions(): void + public function testDoesntHavePermissionWithPermissionButNoOtherPermissions(): void { - $this->user->setPermissions([ - 'create.page', - ]); + $this->user->setPermissions(['create.page']); $this->assertFalse($this->user->hasPermissionTo(['create.post', 'create.comment'])); } - public function test_doesnt_have_permission_with_permission_but_not_all_other_permissions(): void + public function testDoesntHavePermissionWithPermissionButNotAllOtherPermissions(): void { $this->user->setPermissions([ 'create.page', - 'create.post' + 'create.post', ]); $this->assertFalse($this->user->hasPermissionTo(['create.post', 'create.page', 'create.comment'], true)); } - public function test_user_has_permission_when_no_roles_but_has_the_permission(): void + public function testUserHasPermissionWhenNoRolesButHasThePermission(): void { - $this->user->setPermissions([ - 'create.post', - ]); + $this->user->setPermissions(['create.post']); $this->assertTrue($this->user->hasPermissionTo('create.post')); } - public function test_user_has_permission_when_with_roles_but_has_the_permission(): void + public function testUserHasPermissionWhenWithRolesButHasThePermission(): void { - $this->userWithRoles->setRoles([ - new RoleMock, + $this->user->setRoles([ + entity(Role::class)->make(), ]); - $this->userWithRoles->setPermissions([ - 'create.post', - ]); + $this->user->setPermissions(['create.post']); - $this->assertTrue($this->userWithRoles->hasPermissionTo('create.post')); + $this->assertTrue($this->user->hasPermissionTo('create.post')); } - public function test_user_has_permission_when_role_has_permission(): void + public function testUserHasPermissionWhenRoleHasPermission(): void { - $role = new RoleMock; - $role->setPermissions([ - 'create.post', - ]); + $role = entity(Role::class)->make(); + $role->setPermissions(['create.post']); - $this->userWithRoles->setRoles([ - $role, - ]); + $this->user->setRoles([$role]); - $this->assertTrue($this->userWithRoles->hasPermissionTo('create.post')); + $this->assertTrue($this->user->hasPermissionTo('create.post')); } - public function test_user_has_permission_when_one_role_has_permission(): void + public function testUserHasPermissionWhenOneRoleHasPermission(): void { - $role = new RoleMock; - $role->setPermissions([ - 'create.post', - ]); + $role = entity(Role::class)->make(); + $role->setPermissions(['create.post']); - $this->userWithRoles->setRoles([ - new RoleMock, + $this->user->setRoles([ + entity(Role::class)->make(), $role, ]); - $this->assertTrue($this->userWithRoles->hasPermissionTo('create.post')); + $this->assertTrue($this->user->hasPermissionTo('create.post')); } - public function test_can_check_if_has_permission_with_permission_objects(): void + public function testCanCheckIfHasPermissionWithPermissionObjects(): void { $this->user->setPermissions([ - new \LaravelDoctrine\ACL\Permissions\Permission('create.post'), + new Permission('create.post'), ]); $this->assertTrue($this->user->hasPermissionTo('create.post')); } - public function test_user_has_permission_when_role_has_permission_with_object(): void + public function testUserHasPermissionWhenRoleHasPermissionWithObject(): void { - $role = new RoleMock; + $role = entity(Role::class)->create(); $role->setPermissions([ - new \LaravelDoctrine\ACL\Permissions\Permission('create.post'), + new Permission('create.post'), ]); - $this->userWithRoles->setRoles([ - $role, - ]); + $this->user->setRoles([$role]); - $this->assertTrue($this->userWithRoles->hasPermissionTo('create.post')); + $this->assertTrue($this->user->hasPermissionTo('create.post')); } - public function test_has_permission_with_permission_but_not_all_other_permissions(): void + public function testHasPermissionWithPermissionButNotAllOtherPermissions(): void { - $this->user->setPermissions([ - 'create.page', - ]); + $this->user->setPermissions(['create.page']); $this->assertTrue($this->user->hasPermissionTo(['create.post', 'create.page', 'create.comment'])); } - public function test_has_permission_and_all_permissions() + public function testHasPermissionAndAllPermissions(): void { $this->user->setPermissions([ 'create.page', - 'create.post' + 'create.post', ]); $this->assertTrue($this->user->hasPermissionTo(['create.post', 'create.page'], true)); } - public function test_user_has_permission_by_object() + public function testUserHasPermissionByObject(): void { $this->user->setPermissions(['test.test']); - $this->assertTrue($this->user->hasPermissionTo(new \LaravelDoctrine\ACL\Permissions\Permission('test.test'))); - } - - public function test_user_has_object_permission_by_object(): void - { - $this->user->setPermissions([new \LaravelDoctrine\ACL\Permissions\Permission('test.test')]); - - $this->assertTrue($this->user->hasPermissionTo(new \LaravelDoctrine\ACL\Permissions\Permission('test.test'))); - } -} - -class UserMock implements HasPermissionsContract -{ - use HasPermissions; - - protected $permissions = []; - - public function getPermissions() - { - return $this->permissions; + $this->assertTrue($this->user->hasPermissionTo(new Permission('test.test'))); } - public function setPermissions($permissions) + public function testUserHasObjectPermissionByObject(): void { - $this->permissions = $permissions; - } -} - -class UserMockWithRoles implements HasPermissionsContract, HasRolesContract -{ - use HasPermissions, HasRoles; - - protected $permissions = []; - - protected $roles = []; - - public function getPermissions() - { - return $this->permissions; - } + $this->user->setPermissions([new Permission('test.test')]); - public function setPermissions($permissions) - { - $this->permissions = $permissions; - } - - public function getRoles() - { - return $this->roles; - } - - /** - * @param array $roles - */ - public function setRoles($roles) - { - $this->roles = $roles; - } -} - -class RoleMock implements Role -{ - use HasPermissions; - - protected $permissions = []; - - protected $roles = []; - - public function getPermissions() - { - return $this->permissions; - } - - public function setPermissions($permissions) - { - $this->permissions = $permissions; - } - - /** - * @return string - */ - public function getName() - { - return 'Admin'; + $this->assertTrue($this->user->hasPermissionTo(new Permission('test.test'))); } } diff --git a/tests/Permissions/PermissionManagerTest.php b/tests/Permissions/PermissionManagerTest.php deleted file mode 100644 index 627e1da..0000000 --- a/tests/Permissions/PermissionManagerTest.php +++ /dev/null @@ -1,129 +0,0 @@ -driver = m::mock(PermissionDriver::class); - - $this->container = m::mock(Container::class); - - $this->manager = new PermissionManager($this->container); - $this->manager->extend('config', function () { - return $this->driver; - }); - } - - public function test_can_dot_notated_array_of_permissions(): void - { - $this->driver->shouldReceive('getAllPermissions')->once()->andReturn(new Collection([ - 'permission1', - 'permissionKey2' => [ - 'permissionValue1', - 'permissionValue2' - ], - 'permissionKey3' => [ - 'permissionKey4' => [ - 'permissionValue3', - 'permissionValue4' - ] - ] - ])); - - $config = m::mock(Config::class); - - $this->container->shouldReceive('make')->with('config')->andReturn($config); - - $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); - - $this->assertEquals([ - 'permission1', - 'permissionKey2.permissionValue1', - 'permissionKey2.permissionValue2', - 'permissionKey3.permissionKey4.permissionValue3', - 'permissionKey3.permissionKey4.permissionValue4' - ], $this->manager->getPermissionsWithDotNotation()); - } - - public function test_when_should_use_default_permission_entity(): void - { - $config = m::mock(Config::class); - - $this->container->shouldReceive('make')->with('config')->andReturn($config); - - $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('doctrine'); - - // Tests for leading slashes in case someone is providing a manually written FQN - $config->shouldReceive('get')->with('acl.permissions.entity', null)->andReturn("\\" . Permission::class); - - $this->assertTrue($this->manager->useDefaultPermissionEntity()); - } - - public function test_when_should_not_use_default_permission_entity_because_driver_is_not_doctrine(): void - { - $config = m::mock(Config::class); - - $this->container->shouldReceive('make')->with('config')->andReturn($config); - - $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); - $config->shouldReceive('get')->with('acl.permissions.entity', null)->andReturn(Permission::class); - - $this->assertFalse($this->manager->useDefaultPermissionEntity()); - } - - public function test_when_should_not_use_default_permission_entity_because_entity_is_different(): void - { - $config = m::mock(Config::class); - - $this->container->shouldReceive('make')->with('config')->andReturn($config); - - $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); - $config->shouldReceive('get')->with('acl.permissions.entity', null)->andReturn('Namespace\Class'); - - $this->assertFalse($this->manager->useDefaultPermissionEntity()); - } - - public function test_needs_doctrine(): void - { - $config = m::mock(Config::class); - - $this->container->shouldReceive('make')->with('config')->andReturn($config); - - $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('doctrine'); - - $this->assertTrue($this->manager->needsDoctrine()); - } - - public function test_does_not_need_doctrine(): void - { - $config = m::mock(Config::class); - - $this->container->shouldReceive('make')->with('config')->andReturn($config); - - $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); - - $this->assertFalse($this->manager->needsDoctrine()); - } -} diff --git a/tests/Roles/HasRolesTest.php b/tests/Roles/HasRolesTest.php index c9c8e50..bb733a7 100644 --- a/tests/Roles/HasRolesTest.php +++ b/tests/Roles/HasRolesTest.php @@ -1,211 +1,89 @@ user = new UserMock2; - $this->admin = new RoleMock2('admin'); - $this->extraRole1 = new RoleMock2('extraRole1'); - $this->extraRole2 = new RoleMock2('extraRole2'); - } +use function entity; - public function test_doesnt_have_role_when_no_roles_assigned(): void +class HasRolesTest extends TestCase +{ + protected User|null $user; + protected Role|null $admin; + protected Role|null $extraRole1; + protected Role|null $extraRole2; + + public function setUp(): void { - $this->assertFalse($this->user->hasRole($this->admin)); + parent::setUp(); + + $this->user = entity(User::class)->create(); + $this->admin = entity(Role::class, 'admin')->create(); + $this->extraRole1 = entity(Role::class)->create(['name' => 'extraRole1']); + $this->extraRole2 = entity(Role::class)->create(['name' => 'extraRole2']); } - public function test_doesnt_have_role_by_name_when_no_roles_assigned(): void + public function testRolesAndRoleNamesCombinations(): void { + // Initial state: user has no roles + $this->assertFalse($this->user->hasRole($this->admin)); $this->assertFalse($this->user->hasRoleByName('admin')); - } - public function test_doesnt_have_role_when_when_other_role_assigned(): void - { - $this->user->setRoles([ - new RoleMock2('user'), - ]); + // User has a different role (not admin) + $this->user->setRoles([entity(Role::class, 'user')->create()]); $this->assertFalse($this->user->hasRole($this->admin)); - } + $this->assertFalse($this->user->hasRoleByName('admin')); - public function test_doesnt_have_any_role_when_role_assigned(): void - { - $this->user->setRoles([ - $this->admin - ]); + // User has only admin + $this->user->setRoles([$this->admin]); $this->assertFalse($this->user->hasRole([$this->extraRole1, $this->extraRole2])); - } - - public function test_doesnt_have_any_role_by_name_when_role_assigned(): void - { - $this->user->setRoles([ - $this->admin - ]); $this->assertFalse($this->user->hasRoleByName(['extraRole1', 'extraRole2'])); - } - - public function test_doesnt_have_all_roles_when_role_assigned(): void - { - $this->user->setRoles([ - $this->admin, - $this->extraRole1 - ]); - $this->assertFalse($this->user->hasRole([$this->admin, $this->extraRole1, $this->extraRole2], true)); - } - - public function test_doesnt_have_all_roles_by_name_when_role_assigned(): void - { - $this->user->setRoles([ - $this->admin, - $this->extraRole1 - ]); - $this->assertFalse($this->user->hasRoleByName(['admin', 'extraRole1', 'extraRole2'], true)); - } - - public function test_doesnt_have_role_by_name_when_when_other_role_assigned(): void - { - $this->user->setRoles([ - new RoleMock2('user'), - ]); - $this->assertFalse($this->user->hasRoleByName('admin')); - } - - public function test_has_role_when_when_role_assigned(): void - { - $this->user->setRoles([ - $this->admin, - ]); $this->assertTrue($this->user->hasRole($this->admin)); - } - - public function test_has_role_by_name_when_when_role_assigned(): void - { - $this->user->setRoles([ - $this->admin, - ]); $this->assertTrue($this->user->hasRoleByName('admin')); - } - public function test_has_any_role_when_role_assigned(): void - { + // User has admin and extraRole1 $this->user->setRoles([ $this->admin, $this->extraRole1, - $this->extraRole2 ]); - $this->assertTrue($this->user->hasRole([$this->admin, $this->extraRole1])); - } - - public function test_has_all_role_when_role_assigned(): void - { - $this->user->setRoles([ + $this->assertFalse($this->user->hasRole([ $this->admin, $this->extraRole1, - $this->extraRole2 - ]); - $this->assertTrue($this->user->hasRole([$this->admin, $this->extraRole1, $this->extraRole2], true)); - } - - public function test_has_any_role_by_name_when_role_assigned(): void - { + $this->extraRole2, + ], true)); + $this->assertFalse($this->user->hasRoleByName([ + 'admin', + 'extraRole1', + 'extraRole2', + ], true)); + + // User has admin, extraRole1, extraRole2 $this->user->setRoles([ $this->admin, $this->extraRole1, - $this->extraRole2 + $this->extraRole2, ]); - $this->assertTrue($this->user->hasRoleByName(['admin', 'extraRole1'])); - } - - public function test_has_all_role_by_name_when_role_assigned(): void - { - $this->user->setRoles([ + $this->assertTrue($this->user->hasRole([ $this->admin, $this->extraRole1, - $this->extraRole2 - ]); - $this->assertTrue($this->user->hasRoleByName(['admin', 'extraRole1', 'extraRole2'], true)); - } -} - -class UserMock2 implements HasRolesContract -{ - use HasRoles; - - protected $roles = []; - - public function getRoles() - { - return $this->roles; - } - - /** - * @param array $roles - */ - public function setRoles($roles) - { - $this->roles = $roles; - } -} - -class RoleMock2 implements Role -{ - use HasPermissions; - - protected $permissions = []; - - protected $roles = []; - - /** - * @var string - */ - protected $name; - - /** - * @param $name - */ - public function __construct($name) - { - $this->name = $name; - } - - public function getPermissions() - { - return $this->permissions; - } - - public function setPermissions($permissions) - { - $this->permissions = $permissions; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; + ])); + $this->assertTrue($this->user->hasRole([ + $this->admin, + $this->extraRole1, + $this->extraRole2, + ], true)); + $this->assertTrue($this->user->hasRoleByName([ + 'admin', + 'extraRole1', + ])); + $this->assertTrue($this->user->hasRoleByName([ + 'admin', + 'extraRole1', + 'extraRole2', + ], true)); } } diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..a3a955f --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,30 @@ +artisan('doctrine:schema:create'); + } + + public function tearDown(): void + { + parent::tearDown(); + } + + protected function em(): EntityManager + { + return $this->app->make(EntityManager::class); + } +} diff --git a/workbench/app/Entities/Organisation.php b/workbench/app/Entities/Organisation.php new file mode 100644 index 0000000..b809430 --- /dev/null +++ b/workbench/app/Entities/Organisation.php @@ -0,0 +1,42 @@ +name = $name; + } + + public function getId(): int|null + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } +} diff --git a/workbench/app/Entities/Role.php b/workbench/app/Entities/Role.php new file mode 100644 index 0000000..609efa2 --- /dev/null +++ b/workbench/app/Entities/Role.php @@ -0,0 +1,64 @@ + */ + #[ACL\HasPermissions] + public Collection $permissions; + + public function __construct(string $name) + { + $this->name = $name; + $this->permissions = new ArrayCollection(); + } + + public function getName(): string + { + return $this->name; + } + + public function getId(): int|null + { + return $this->id; + } + + /** @return Collection */ + public function getPermissions(): Collection + { + return $this->permissions; + } + + /** @param Collection|Permission[] $permissions */ + public function setPermissions(Collection|array $permissions): self + { + $this->permissions = is_array($permissions) ? new ArrayCollection($permissions) : $permissions; + + return $this; + } +} diff --git a/workbench/app/Entities/User.php b/workbench/app/Entities/User.php new file mode 100644 index 0000000..be993f0 --- /dev/null +++ b/workbench/app/Entities/User.php @@ -0,0 +1,110 @@ + */ + #[ACL\HasRoles] + public Collection $roles; + + /** @var Collection */ + #[ACL\HasPermissions] + public Collection $permissions; + + /** @var Collection */ + #[ACL\BelongsToOrganisations] + public Collection $organisations; + + public function __construct() + { + $this->roles = new ArrayCollection(); + $this->permissions = new ArrayCollection(); + $this->organisations = new ArrayCollection(); + } + + /** @return Collection */ + public function getRoles(): Collection + { + return $this->roles; + } + + /** @param Collection|Role[] $roles */ + public function setRoles(Collection|array $roles): self + { + $this->roles = is_array($roles) ? new ArrayCollection($roles) : $roles; + + return $this; + } + + /** @return Collection */ + public function getPermissions(): Collection + { + return $this->permissions; + } + + /** @param Collection|Permission[] $permissions */ + public function setPermissions(Collection|array $permissions): self + { + $this->permissions = is_array($permissions) ? new ArrayCollection($permissions) : $permissions; + + return $this; + } + + /** @return Collection */ + public function getOrganisations(): Collection + { + return $this->organisations; + } + + /** @param Collection|Organisation[] $organisations */ + public function setOrganisations(Collection|array $organisations): self + { + $this->organisations = is_array($organisations) ? new ArrayCollection($organisations) : $organisations; + + return $this; + } +} diff --git a/workbench/app/Entities/UserJsonPermissions.php b/workbench/app/Entities/UserJsonPermissions.php new file mode 100644 index 0000000..89b831f --- /dev/null +++ b/workbench/app/Entities/UserJsonPermissions.php @@ -0,0 +1,57 @@ + */ + #[ACL\HasPermissions(inversedBy: 'users')] + public array $permissions = []; + + /** @return array */ + public function getPermissions(): array + { + return $this->permissions; + } + + /** @param array $permissions */ + public function setPermissions(array $permissions): self + { + $this->permissions = $permissions; + + return $this; + } +} diff --git a/workbench/app/Entities/UserSingleOrg.php b/workbench/app/Entities/UserSingleOrg.php new file mode 100644 index 0000000..4921eaa --- /dev/null +++ b/workbench/app/Entities/UserSingleOrg.php @@ -0,0 +1,110 @@ + */ + #[ACL\HasRoles()] + public Collection $roles; + + /** @var array */ + #[ORM\Column(type: 'json')] + public array $permissions = []; + + #[ACL\BelongsToOrganisation()] + public Organisation|null $organisation = null; + + public function __construct() + { + $this->roles = new ArrayCollection(); + } + + public function getId(): int|null + { + return $this->id; + } + + /** @return Collection */ + public function getRoles(): Collection + { + return $this->roles; + } + + /** @param Collection|Role[] $roles */ + public function setRoles(Collection|array $roles): self + { + $this->roles = is_array($roles) ? new ArrayCollection($roles) : $roles; + + return $this; + } + + /** @return array */ + public function getPermissions(): array + { + return $this->permissions; + } + + /** @param array $permissions */ + public function setPermissions(array $permissions): self + { + $this->permissions = $permissions; + + return $this; + } + + public function getOrganisation(): Organisation|null + { + return $this->organisation; + } + + public function setOrganisation(Organisation|null $organisation): self + { + $this->organisation = $organisation; + + return $this; + } +} diff --git a/workbench/app/Providers/WorkbenchServiceProvider.php b/workbench/app/Providers/WorkbenchServiceProvider.php new file mode 100644 index 0000000..99148d7 --- /dev/null +++ b/workbench/app/Providers/WorkbenchServiceProvider.php @@ -0,0 +1,24 @@ +withRouting( + web: __DIR__ . '/../routes/web.php', + commands: __DIR__ . '/../routes/console.php', + ) + ->withMiddleware(static function (Middleware $middleware): void { + }) + ->withExceptions(static function (Exceptions $exceptions): void { + })->create(); diff --git a/workbench/bootstrap/cache/.gitkeep b/workbench/bootstrap/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/workbench/bootstrap/providers.php b/workbench/bootstrap/providers.php new file mode 100644 index 0000000..d216fbd --- /dev/null +++ b/workbench/bootstrap/providers.php @@ -0,0 +1,13 @@ + [ + 'driver' => 'doctrine', + 'entity' => Permission::class, + 'list' => [ + 'role.attach', + 'role.detach', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Roles + |-------------------------------------------------------------------------- + */ + 'roles' => [ + 'entity' => Role::class, + ], + + /* + |-------------------------------------------------------------------------- + | Organisations + |-------------------------------------------------------------------------- + */ + 'organisations' => [ + 'entity' => Organisation::class, + ], +]; diff --git a/workbench/config/auth.php b/workbench/config/auth.php new file mode 100644 index 0000000..2c1866e --- /dev/null +++ b/workbench/config/auth.php @@ -0,0 +1,119 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'doctrine', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/workbench/config/database.php b/workbench/config/database.php new file mode 100644 index 0000000..2a6e6bb --- /dev/null +++ b/workbench/config/database.php @@ -0,0 +1,177 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', ':memory:'), + 'prefix' => '', + 'prefix_indexes' => null, + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + ], + + ], + +]; diff --git a/workbench/config/doctrine.php b/workbench/config/doctrine.php new file mode 100644 index 0000000..0e73e5a --- /dev/null +++ b/workbench/config/doctrine.php @@ -0,0 +1,196 @@ + Warning: Proxy auto generation should only be enabled in dev! + | + */ + 'managers' => [ + 'default' => [ + 'dev' => env('APP_DEBUG', false), + 'meta' => env('DOCTRINE_METADATA', 'attributes'), + 'connection' => env('DB_CONNECTION', 'sqlite'), + 'paths' => [app_path('Entities')], + + 'repository' => EntityRepository::class, + + 'proxies' => [ + 'namespace' => 'DoctrineProxies', + 'path' => storage_path('proxies'), + 'auto_generate' => env('DOCTRINE_PROXY_AUTOGENERATE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Doctrine events + |-------------------------------------------------------------------------- + | + | The listener array expects the key to be a Doctrine event + | e.g. Doctrine\ORM\Events::onFlush + | + */ + 'events' => [ + 'listeners' => [], + 'subscribers' => [], + ], + + 'filters' => [], + + /* + |-------------------------------------------------------------------------- + | Doctrine mapping types + |-------------------------------------------------------------------------- + | + | Link a Database Type to a Local Doctrine Type + | + | Using 'enum' => 'string' is the same of: + | $doctrineManager->extendAll(function (\Doctrine\ORM\Configuration $configuration, + | \Doctrine\DBAL\Connection $connection, + | \Doctrine\Common\EventManager $eventManager) { + | $connection->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); + | }); + | + | References: + | https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/custom-mapping-types.html + | https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#custom-mapping-types + | https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/advanced-field-value-conversion-using-custom-mapping-types.html + | https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html + | https://symfony.com/doc/current/doctrine/dbal.html#registering-custom-mapping-types-in-the-schematool + |-------------------------------------------------------------------------- + */ + 'mapping_types' => [], + + /** + * References: + * https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/architecture.html#middlewares + */ + 'middlewares' => [], + ], + ], + /* + |-------------------------------------------------------------------------- + | Doctrine Extensions + |-------------------------------------------------------------------------- + | + | Enable/disable Doctrine Extensions by adding or removing them from the list + | + | If you want to require custom extensions you will have to require + | laravel-doctrine/extensions in your composer.json + | + */ + 'extensions' => [], + /* + |-------------------------------------------------------------------------- + | Doctrine custom types + |-------------------------------------------------------------------------- + | + | Create a custom or override a Doctrine Type + |-------------------------------------------------------------------------- + */ + 'custom_types' => [], + /* + |-------------------------------------------------------------------------- + | DQL custom datetime functions + |-------------------------------------------------------------------------- + */ + 'custom_datetime_functions' => [], + /* + |-------------------------------------------------------------------------- + | DQL custom numeric functions + |-------------------------------------------------------------------------- + */ + 'custom_numeric_functions' => [], + /* + |-------------------------------------------------------------------------- + | DQL custom string functions + |-------------------------------------------------------------------------- + */ + 'custom_string_functions' => [], + /* + |-------------------------------------------------------------------------- + | Register custom hydrators + |-------------------------------------------------------------------------- + */ + 'custom_hydration_modes' => [], + /* + |-------------------------------------------------------------------------- + | Cache + |-------------------------------------------------------------------------- + | + | Configure meta-data, query and result caching here. + | Optionally you can enable second level caching. + | + | Available: apc|array|file|illuminate|memcached|php_file|redis + | + */ + 'cache' => [ + 'second_level' => false, + 'default' => env('DOCTRINE_CACHE', 'array'), + 'namespace' => null, + 'metadata' => [ + 'driver' => env('DOCTRINE_METADATA_CACHE', env('DOCTRINE_CACHE', 'array')), + 'namespace' => 'metadata', + ], + 'query' => [ + 'driver' => env('DOCTRINE_QUERY_CACHE', env('DOCTRINE_CACHE', 'array')), + 'namespace' => 'query', + ], + 'result' => [ + 'driver' => env('DOCTRINE_RESULT_CACHE', env('DOCTRINE_CACHE', 'array')), + 'namespace' => 'result', + ], + ], + /* + |-------------------------------------------------------------------------- + | Gedmo extensions + |-------------------------------------------------------------------------- + | + | Settings for Gedmo extensions + | If you want to use this you will have to require + | laravel-doctrine/extensions in your composer.json + | + */ + 'gedmo' => ['all_mappings' => false], + /* + |-------------------------------------------------------------------------- + | Validation + |-------------------------------------------------------------------------- + | + | Enables the Doctrine Presence Verifier for Validation + | + */ + 'doctrine_presence_verifier' => true, + + /* + |-------------------------------------------------------------------------- + | Notifications + |-------------------------------------------------------------------------- + | + | Doctrine notifications channel + | + */ + 'notifications' => ['channel' => 'database'], +]; diff --git a/workbench/config/migrations.php b/workbench/config/migrations.php new file mode 100644 index 0000000..bd2b2dc --- /dev/null +++ b/workbench/config/migrations.php @@ -0,0 +1,67 @@ + [ + 'table_storage' => [ + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run in the database. + | + */ + 'table_name' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | Schema filter + |-------------------------------------------------------------------------- + | + | Tables which are filtered by Regular Expression. You optionally + | exclude or limit to certain tables. The default will + | filter all tables. + | + */ + 'schema_filter' => '/^(?!password_resets|failed_jobs).*$/', + ], + + 'migrations_paths' => [ + 'Database\\Migrations' => database_path('migrations'), + ], + + /* + |-------------------------------------------------------------------------- + | Migration Organize Directory + |-------------------------------------------------------------------------- + | + | Organize migrations file by directory. + | Possible values: "year", "year_and_month" and "none" + | + | none: + | directory/ + | "year": + | directory/2020/ + | "year_and_month": + | directory/2020/01/ + | + */ + 'organize_migrations' => 'none', + ], +]; diff --git a/workbench/config/session.php b/workbench/config/session.php new file mode 100644 index 0000000..1917915 --- /dev/null +++ b/workbench/config/session.php @@ -0,0 +1,219 @@ + env('SESSION_DRIVER', 'array'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "apc", "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel'), '_') . '_session', + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain and all subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/workbench/config/view.php b/workbench/config/view.php new file mode 100644 index 0000000..528cbf4 --- /dev/null +++ b/workbench/config/view.php @@ -0,0 +1,38 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => env( + 'VIEW_COMPILED_PATH', + realpath(storage_path('framework/views')), + ), + +]; diff --git a/workbench/database/README.md b/workbench/database/README.md new file mode 100644 index 0000000..4a57e5b --- /dev/null +++ b/workbench/database/README.md @@ -0,0 +1,79 @@ +# Laravel Doctrine ORM Entity Factories + +This guide explains how to create and use entity factories for Doctrine entities in the workbench. Factories are essential for generating test data and seeding your database in a consistent, maintainable way. + +## Defining an Entity Factory + +To define a factory for an entity, use the `$factory->define()` method in a factory file (e.g., `UserEntityFactory.php`). + +```php +$factory->define(App\Entities\User::class, function(Faker\Generator $faker) { + return [ + 'name' => $faker->name, + 'emailAddress' => $faker->email + ]; +}); +``` +- Use Doctrine entity property names (not database column names). +- You can define multiple types for the same entity using `defineAs`: + +```php +$factory->defineAs(App\Entities\User::class, 'admin', function(Faker\Generator $faker) { + return [ + 'name' => $faker->name, + 'emailAddress' => $faker->email, + 'isAdmin' => true + ]; +}); +``` + +## Using Factories in Seeds and Tests + +After defining factories, you can generate entities for tests or seeds using the `entity()` helper or the factory directly. + +- **Create (persist) a single entity:** + ```php + entity(App\Entities\User::class)->create(); + // or + $factory->of(App\Entities\User::class)->create(); + ``` + +- **Make (do not persist) a single entity:** + ```php + entity(App\Entities\User::class)->make(); + ``` + +- **Create multiple entities:** + ```php + entity(App\Entities\User::class, 3)->create(); + // or + $factory->of(App\Entities\User::class)->times(3)->create(); + ``` + +- **Create a specific type:** + ```php + entity(App\Entities\User::class, 'admin')->create(); + ``` + +## Passing Extra Attributes + +You can override default attributes by passing an array: + +```php +$factory->define(App\Entities\User::class, function(Faker\Generator $faker, array $attributes) { + return [ + 'name' => $attributes['name'] ?? $faker->name, + 'emailAddress' => $faker->email + ]; +}); + +$user = entity(App\Entities\User::class)->make(['name' => 'Taylor']); +``` + +## Notes +- The `entity()` helper returns an `Illuminate\Support\Collection` if you request multiple entities. +- Use `->make()` to get an instance without saving, or `->create()` to persist to the database. +- Always use property names as defined in your Doctrine entity. + +## References +- [Official Docs: Testing - Entity Factories](https://laravel-doctrine-orm-official.readthedocs.io/en/latest/testing.html) diff --git a/workbench/database/factories/.gitkeep b/workbench/database/factories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/workbench/database/factories/OrganisationFactory.php b/workbench/database/factories/OrganisationFactory.php new file mode 100644 index 0000000..04ed19b --- /dev/null +++ b/workbench/database/factories/OrganisationFactory.php @@ -0,0 +1,15 @@ +define(Organisation::class, static function (Generator $faker, array $attributes) { + return [ + 'name' => $attributes['name'] ?? $faker->unique()->company, + ]; +}); diff --git a/workbench/database/factories/PermissionFactory.php b/workbench/database/factories/PermissionFactory.php new file mode 100644 index 0000000..c5b4721 --- /dev/null +++ b/workbench/database/factories/PermissionFactory.php @@ -0,0 +1,30 @@ +define(Permission::class, static function (Generator $faker, array $attributes = []) { + return [ + 'name' => $attributes['name'] ?? $faker->unique()->word . '-' . $faker->unique()->word, + ]; +}); + +$factory->defineAs(Permission::class, 'view', static function () { + return ['name' => 'view']; +}); + +$factory->defineAs(Permission::class, 'edit', static function () { + return ['name' => 'edit']; +}); + +$factory->defineAs(Permission::class, 'delete', static function () { + return ['name' => 'delete']; +}); diff --git a/workbench/database/factories/RoleFactory.php b/workbench/database/factories/RoleFactory.php new file mode 100644 index 0000000..4c9eeae --- /dev/null +++ b/workbench/database/factories/RoleFactory.php @@ -0,0 +1,26 @@ +define(Role::class, static function (Generator $faker, array $attributes = []) { + return [ + 'name' => $attributes['name'] ?? $faker->unique()->word . '-' . $faker->unique()->word, + ]; +}); + +$factory->defineAs(Role::class, 'admin', static function () { + return ['name' => 'admin']; +}); + +$factory->defineAs(Role::class, 'user', static function () { + return ['name' => 'user']; +}); diff --git a/workbench/database/factories/UserFactory.php b/workbench/database/factories/UserFactory.php new file mode 100644 index 0000000..5a67bac --- /dev/null +++ b/workbench/database/factories/UserFactory.php @@ -0,0 +1,28 @@ +define(User::class, static function (Generator $faker, array $attributes = []) { + return [ + 'name' => $faker->name(), + 'email' => $faker->safeEmail, + 'password' => 'password', + ]; +}); + +$factory->defineAs(User::class, 'test', static function (Generator $faker, array $attributes = []) { + return [ + 'name' => 'test', + 'email' => 'test@test.tld', + 'password' => 'password', + ]; +}); diff --git a/workbench/database/factories/UserJsonPermissions.php b/workbench/database/factories/UserJsonPermissions.php new file mode 100644 index 0000000..fc44090 --- /dev/null +++ b/workbench/database/factories/UserJsonPermissions.php @@ -0,0 +1,20 @@ +define(UserJsonPermissions::class, static function (Generator $faker, array $attributes = []) { + return [ + 'name' => $attributes['name'] ?? $faker->name(), + 'email' => $attributes['email'] ?? $faker->safeEmail, + 'password' => 'password', + ]; +}); diff --git a/workbench/database/factories/UserSingleOrgFactory.php b/workbench/database/factories/UserSingleOrgFactory.php new file mode 100644 index 0000000..d825f04 --- /dev/null +++ b/workbench/database/factories/UserSingleOrgFactory.php @@ -0,0 +1,20 @@ +define(UserSingleOrg::class, static function (Generator $faker, array $attributes = []) { + return [ + 'name' => $attributes['name'] ?? $faker->name(), + 'email' => $attributes['email'] ?? $faker->safeEmail, + 'password' => 'password', + ]; +}); diff --git a/workbench/database/migrations/.gitkeep b/workbench/database/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/workbench/database/seeders/DatabaseSeeder.php b/workbench/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..17e1f56 --- /dev/null +++ b/workbench/database/seeders/DatabaseSeeder.php @@ -0,0 +1,25 @@ +create(); + + UserFactory::new()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } +} diff --git a/workbench/resources/views/.gitkeep b/workbench/resources/views/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/workbench/routes/console.php b/workbench/routes/console.php new file mode 100644 index 0000000..c476778 --- /dev/null +++ b/workbench/routes/console.php @@ -0,0 +1,10 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote')->hourly(); diff --git a/workbench/routes/web.php b/workbench/routes/web.php new file mode 100644 index 0000000..f3f93c1 --- /dev/null +++ b/workbench/routes/web.php @@ -0,0 +1,9 @@ +