├── .github └── workflows │ ├── coding-standards.yml │ ├── continuous-integration.yml │ └── static-analysis.yml ├── .gitignore ├── .php_cs ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── composer.json ├── config └── acl.php ├── docs ├── banner.png ├── conf.py ├── configurations.rst ├── core-concepts.rst ├── favicon.ico ├── footer.rst ├── index.rst ├── install.rst ├── introduction.rst ├── organisations.rst ├── permissions.rst ├── requirements.txt ├── roles.rst └── usage.rst ├── phpcs.xml.dist ├── phpstan.neon ├── phpunit.xml ├── src ├── AclServiceProvider.php ├── Attribute │ ├── BelongsToOrganisation.php │ ├── BelongsToOrganisations.php │ ├── HasPermissions.php │ ├── HasRoles.php │ ├── MappingAttribute.php │ └── RelationAttribute.php ├── Configurations │ ├── ConfigPermissionsProvider.php │ ├── DoctrinePermissionsProvider.php │ └── PermissionsProvider.php ├── Contracts │ ├── BelongsToOrganisation.php │ ├── BelongsToOrganisations.php │ ├── HasPermissions.php │ ├── HasRoles.php │ ├── Organisation.php │ ├── Permission.php │ └── Role.php ├── Mappings │ ├── Builders │ │ ├── Builder.php │ │ ├── JsonArrayBuilder.php │ │ ├── ManyToManyBuilder.php │ │ └── ManyToOneBuilder.php │ ├── RegisterMappedEventSubscribers.php │ └── Subscribers │ │ ├── BelongsToOrganisationSubscriber.php │ │ ├── BelongsToOrganisationsSubscriber.php │ │ ├── HasPermissionsSubscriber.php │ │ ├── HasRolesSubscriber.php │ │ └── MappedEventSubscriber.php ├── Organisations │ └── BelongsToOrganisation.php ├── PermissionManager.php ├── Permissions │ ├── Driver │ │ ├── Config.php │ │ ├── Doctrine.php │ │ └── PermissionDriver.php │ ├── Permission.php │ └── WithPermissions.php └── Roles │ └── WithRoles.php ├── stubs ├── Organisation.php ├── Permission.php └── Role.php ├── testbench.yaml ├── tests ├── Configurations │ ├── DoctrinePermissionsProviderTest.php │ └── PermissionManagerTest.php ├── Integration │ ├── AclServiceProviderTest.php │ ├── ConfigPermissionDriverTest.php │ └── DoctrinePermissionPersistenceTest.php ├── LaravelSetupTest.php ├── Organisations │ └── BelongsToOrganisationTest.php ├── Permissions │ ├── Driver │ │ ├── ConfigPermissionDriverTest.php │ │ └── DoctrinePermissionDriverTest.php │ └── HasPermissionsTest.php ├── Roles │ └── HasRolesTest.php └── TestCase.php └── workbench ├── app ├── Entities │ ├── Organisation.php │ ├── Role.php │ ├── User.php │ ├── UserJsonPermissions.php │ └── UserSingleOrg.php └── Providers │ └── WorkbenchServiceProvider.php ├── bootstrap ├── app.php ├── cache │ └── .gitkeep └── providers.php ├── config ├── acl.php ├── auth.php ├── database.php ├── doctrine.php ├── migrations.php ├── session.php └── view.php ├── database ├── README.md ├── factories │ ├── .gitkeep │ ├── OrganisationFactory.php │ ├── PermissionFactory.php │ ├── RoleFactory.php │ ├── UserFactory.php │ ├── UserJsonPermissions.php │ └── UserSingleOrgFactory.php ├── migrations │ └── .gitkeep └── seeders │ └── DatabaseSeeder.php ├── resources └── views │ └── .gitkeep ├── routes ├── console.php └── web.php └── storage └── framework └── views └── .gitkeep /.github/workflows/coding-standards.yml: -------------------------------------------------------------------------------- 1 | name: "Coding Standards" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "*.x" 7 | - "main" 8 | push: 9 | branches: 10 | - "*.x" 11 | - "main" 12 | 13 | jobs: 14 | coding-standards: 15 | name: "Coding Standards" 16 | uses: "doctrine/.github/.github/workflows/coding-standards.yml@5.2.0" 17 | with: 18 | php-version: '8.2' 19 | composer-options: '--prefer-dist --ignore-platform-req=php' 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: "Continuous Integration" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "*.x" 7 | - "main" 8 | push: 9 | branches: 10 | - "*.x" 11 | - "main" 12 | 13 | jobs: 14 | phpunit: 15 | name: "PHPUnit" 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | php-version: 22 | - "8.2" 23 | - "8.3" 24 | - "8.4" 25 | dependencies: 26 | - "highest" 27 | - "lowest" 28 | optional-dependencies: 29 | - true 30 | - false 31 | 32 | steps: 33 | - name: "Checkout" 34 | uses: "actions/checkout@v2" 35 | with: 36 | fetch-depth: 2 37 | 38 | - name: "Install PHP" 39 | uses: "shivammathur/setup-php@v2" 40 | with: 41 | php-version: "${{ matrix.php-version }}" 42 | coverage: "pcov" 43 | ini-values: "zend.assertions=1" 44 | extensions: "pdo_mysql" 45 | 46 | - name: "Install dependencies with Composer" 47 | uses: "ramsey/composer-install@v1" 48 | with: 49 | dependency-versions: "${{ matrix.dependencies }}" 50 | composer-options: "--prefer-dist" 51 | 52 | - name: "Show Composer packages" 53 | run: "composer show" 54 | 55 | - name: "Run PHPUnit" 56 | run: "vendor/bin/phpunit --coverage-clover=coverage.xml" 57 | 58 | - name: "Upload coverage" 59 | uses: "codecov/codecov-action@v5" 60 | with: 61 | token: ${{ secrets.CODECOV_TOKEN }} 62 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Static Analysis" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | static-analysis-phpstan: 9 | name: "Static Analysis with PHPStan" 10 | runs-on: "ubuntu-22.04" 11 | 12 | strategy: 13 | matrix: 14 | php-version: 15 | - "8.2" 16 | - "8.3" 17 | - "8.4" 18 | 19 | steps: 20 | - name: "Checkout code" 21 | uses: "actions/checkout@v3" 22 | 23 | - name: "Install PHP" 24 | uses: "shivammathur/setup-php@v2" 25 | with: 26 | coverage: "none" 27 | php-version: "${{ matrix.php-version }}" 28 | extensions: "pdo_sqlite" 29 | 30 | - name: "Install dependencies with Composer" 31 | uses: "ramsey/composer-install@v2" 32 | with: 33 | dependency-versions: "${{ matrix.dependencies }}" 34 | 35 | - name: "Run a static analysis with phpstan/phpstan" 36 | run: "vendor/bin/phpstan analyse src --level 1" 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /vendor 3 | composer.phar 4 | composer.lock 5 | .DS_Store 6 | .phpcs-cache 7 | .phpunit.cache 8 | .phpunit.result.cache 9 | 10 | 11 | .idea 12 | .vscode 13 | 14 | /tests/Stubs/storage/framework/views/* 15 | !/tests/Stubs/storage/framework/views/.gitkeep 16 | /tests/Stubs/storage/doctrine.generated.php 17 | laravel-doctrine-orm.iml 18 | /workbench/bootstrap/cache/* 19 | !/workbench/bootstrap/cache/.gitkeep 20 | /workbench/storage/logs/* 21 | /workbench/vendor -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | exclude('vendor') 5 | ->in(__DIR__); 6 | 7 | return Symfony\CS\Config\Config::create() 8 | ->setUsingCache(true) 9 | ->level(Symfony\CS\FixerInterface::PSR2_LEVEL) 10 | ->fixers(array( 11 | 'psr4', 12 | 'encoding', 13 | 'short_tag', 14 | 'blankline_after_open_tag', 15 | 'namespace_no_leading_whitespace', 16 | 'no_blank_lines_after_class_opening', 17 | 'single_array_no_trailing_comma', 18 | 'no_empty_lines_after_phpdocs', 19 | 'concat_with_spaces', 20 | 'eof_ending', 21 | 'ordered_use', 22 | 'extra_empty_lines', 23 | 'single_line_after_imports', 24 | 'trailing_spaces', 25 | 'remove_lines_between_uses', 26 | 'return', 27 | 'indentation', 28 | 'linefeed', 29 | 'braces', 30 | 'visibility', 31 | 'unused_use', 32 | 'whitespacy_lines', 33 | 'php_closing_tag', 34 | 'phpdoc_order', 35 | 'phpdoc_params', 36 | 'phpdoc_trim', 37 | 'phpdoc_scalar', 38 | 'short_array_syntax', 39 | 'align_double_arrow', 40 | 'align_equals', 41 | 'lowercase_constants', 42 | 'lowercase_keywords', 43 | 'multiple_use', 44 | 'line_after_namespace', 45 | ))->finder($finder); 46 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Laravel Doctrine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | Laravel Doctrine ACL 7 | ==================== 8 | 9 | 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. 10 | 11 | [![Build Status](https://github.com/laravel-doctrine/acl/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/laravel-doctrine/acl/actions) 12 | [![Code Coverage](https://codecov.io/gh/laravel-doctrine/acl/graph/badge.svg?token=3CpQzDXOWX)](https://codecov.io/gh/laravel-doctrine/acl) 13 | [![PHPStan](https://img.shields.io/badge/PHPStan-level%201-brightgreen.svg)](https://img.shields.io/badge/PHPStan-level%201-brightgreen.svg) 14 | [![Documentation](https://readthedocs.org/projects/laravel-doctrine-acl-official/badge/?version=latest)](https://laravel-doctrine-acl-official.readthedocs.io/en/latest/) 15 | [![Packagist Downloads](https://img.shields.io/packagist/dd/laravel-doctrine/acl)](https://packagist.org/packages/laravel-doctrine/acl) 16 | 17 | Installation 18 | ------------ 19 | 20 | Via composer: 21 | 22 | ```bash 23 | composer require laravel-doctrine/acl 24 | ``` 25 | 26 | The ServiceProvider and Facades are autodiscovered. 27 | 28 | Publish the configuration: 29 | 30 | ```bash 31 | php artisan vendor:publish --tag="config" --provider="LaravelDoctrine\ACL\AclServiceProvider" 32 | ``` 33 | 34 | Documentation 35 | ------------- 36 | 37 | Full documentation at https://laravel-doctrine-acl.readthedocs.io/en/latest/index.html 38 | or in the docs directory. 39 | 40 | Versions 41 | -------- 42 | 43 | * Version 2 supports Laravel 11-12, ORM ^3.0, DBAL ^4.0, and PHP 8.2. 44 | * Version 1 supports Laravel 6 - 11, DBAL ^2.0, ORM ^2.0, and PHP ^5.5 - ^8.0. 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-doctrine/acl", 3 | "type": "library", 4 | "description": "ACL for Laravel and Doctrine", 5 | "license": "MIT", 6 | "keywords": [ 7 | "doctrine", 8 | "laravel", 9 | "orm", 10 | "data mapper", 11 | "database", 12 | "acl", 13 | "abilities", 14 | "policies", 15 | "permissions", 16 | "roles", 17 | "organisations" 18 | ], 19 | "authors": [ 20 | { 21 | "name": "Patrick Brouwers", 22 | "email": "patrick@maatwebsite.nl" 23 | }, 24 | { 25 | "name": "Pavlo Zhytomyrskyi", 26 | "email": "pavelz@scholarshipowl.com" 27 | } 28 | ], 29 | "require": { 30 | "php": "^8.2", 31 | "illuminate/auth": "^11.0|^12.0", 32 | "illuminate/config": "^11.0|^12.0", 33 | "illuminate/contracts": "^11.0|^12.0", 34 | "illuminate/support": "^11.0|^12.0", 35 | "laravel-doctrine/orm": "^3.1" 36 | }, 37 | "require-dev": { 38 | "mockery/mockery": "^1.3.1", 39 | "phpunit/phpunit": "^11.5", 40 | "laravel/framework": "^11.0|^12.0", 41 | "orchestra/testbench": "^10.2", 42 | "laravel-doctrine/migrations": "^3.4", 43 | "doctrine/coding-standard": "^12.0", 44 | "php-parallel-lint/php-parallel-lint": "^1.4", 45 | "phpstan/phpstan": "^2.1", 46 | "phpstan/phpstan-deprecation-rules": "^2.0" 47 | }, 48 | "autoload": { 49 | "psr-4": { 50 | "LaravelDoctrine\\ACL\\": "src/" 51 | } 52 | }, 53 | "autoload-dev": { 54 | "psr-4": { 55 | "Tests\\": "tests", 56 | "Workbench\\App\\": "workbench/app/", 57 | "Workbench\\Database\\Factories\\": "workbench/database/factories/", 58 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" 59 | } 60 | }, 61 | "extra": { 62 | "laravel": { 63 | "providers": [ 64 | "LaravelDoctrine\\ACL\\AclServiceProvider" 65 | ] 66 | } 67 | }, 68 | "config": { 69 | "allow-plugins": { 70 | "dealerdirect/phpcodesniffer-composer-installer": true 71 | } 72 | }, 73 | "scripts": { 74 | "test": [ 75 | "vendor/bin/parallel-lint src tests", 76 | "vendor/bin/phpcs", 77 | "vendor/bin/phpunit", 78 | "vendor/bin/phpstan analyze src --level 1" 79 | ], 80 | "coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage", 81 | "post-autoload-dump": [ 82 | "@clear", 83 | "@prepare" 84 | ], 85 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 86 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 87 | "build": "@php vendor/bin/testbench workbench:build --ansi", 88 | "serve": [ 89 | "Composer\\Config::disableProcessTimeout", 90 | "@build", 91 | "@php vendor/bin/testbench serve --ansi" 92 | ], 93 | "lint": [ 94 | "@php vendor/bin/phpstan analyse --verbose --ansi" 95 | ] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /config/acl.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'driver' => 'config', 16 | 'entity' => LaravelDoctrine\ACL\Permissions\Permission::class, 17 | 'list' => [], 18 | ], 19 | 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Roles 24 | |-------------------------------------------------------------------------- 25 | */ 26 | 'roles' => [ 27 | 'entity' => App\Entities\Role::class, 28 | ], 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Organisations 33 | |-------------------------------------------------------------------------- 34 | */ 35 | 'organisations' => [ 36 | 'entity' => App\Entities\Organisation::class, 37 | ], 38 | ]; 39 | -------------------------------------------------------------------------------- /docs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-doctrine/acl/bbf7cb96bd7bc11186827b5402b886f36ffab2da/docs/banner.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | from sphinx.highlighting import lexers 3 | from pygments.lexers.web import PhpLexer 4 | 5 | lexers['php'] = PhpLexer(startinline=True, linenos=0) 6 | lexers['php-annotations'] = PhpLexer(startinline=True, linenos=0) 7 | primary_domain = 'php' 8 | 9 | extensions = [] 10 | templates_path = ['_templates'] 11 | source_suffix = '.rst' 12 | master_doc = 'index' 13 | project = u'Laravel Doctrine ACL' 14 | copyright = u'2025 laraveldoctrine.org' 15 | version = '9' 16 | html_title = "ACL for Laravel and Doctrine" 17 | html_short_title = "Laravel Doctrine ACL" 18 | html_favicon = 'favicon.ico' 19 | 20 | exclude_patterns = ['_build'] 21 | html_static_path = ['_static'] 22 | 23 | ##### Guzzle sphinx theme 24 | 25 | import guzzle_sphinx_theme 26 | html_translator_class = 'guzzle_sphinx_theme.HTMLTranslator' 27 | html_theme_path = guzzle_sphinx_theme.html_theme_path() 28 | html_theme = 'guzzle_sphinx_theme' 29 | 30 | # Custom sidebar templates, maps document names to template names. 31 | html_sidebars = { 32 | '**': ['logo-text.html', 'globaltoc.html', 'searchbox.html'] 33 | } 34 | 35 | # Register the theme as an extension to generate a sitemap.xml 36 | extensions.append("guzzle_sphinx_theme") 37 | 38 | # Guzzle theme options (see theme.conf for more information) 39 | html_theme_options = { 40 | 41 | # Set the path to a special layout to include for the homepage 42 | # "index_template": "homepage.html", 43 | 44 | # Allow a separate homepage from the master_doc 45 | # homepage = index 46 | 47 | # Set the name of the project to appear in the nav menu 48 | # "project_nav_name": "Guzzle", 49 | 50 | # Set your Disqus short name to enable comments 51 | # "disqus_comments_shortname": "my_disqus_comments_short_name", 52 | 53 | # Set you GA account ID to enable tracking 54 | # "google_analytics_account": "my_ga_account", 55 | 56 | # Path to a touch icon 57 | # "touch_icon": "", 58 | 59 | # Specify a base_url used to generate sitemap.xml links. If not 60 | # specified, then no sitemap will be built. 61 | "base_url": "https://docs.acl.laraveldoctrine.org" 62 | 63 | # Allow the "Table of Contents" page to be defined separately from "master_doc" 64 | # tocpage = Contents 65 | 66 | # Allow the project link to be overriden to a custom URL. 67 | # projectlink = http://myproject.url 68 | } 69 | -------------------------------------------------------------------------------- /docs/configurations.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Config File 3 | =========== 4 | 5 | This document describes the options available in the `config/acl.php` configuration file for the Laravel Doctrine ACL package. 6 | 7 | Permissions 8 | =========== 9 | 10 | .. code-block:: php 11 | 12 | 'permissions' => [ 13 | 'driver' => 'config', 14 | 'entity' => LaravelDoctrine\ACL\Permissions\Permission::class, 15 | 'list' => [], 16 | ], 17 | 18 | - **driver**: The permissions driver to use. Supported drivers: 19 | 20 | - `config`: Permissions are defined statically in the `list` array below. 21 | - `doctrine`: Permissions are managed as Doctrine entities in the database. 22 | 23 | - **list**: (Only for `config` driver) An array of permission names to be recognized by the system. Example: `['edit.posts', 'delete.posts']` 24 | 25 | .. code-block:: php 26 | 27 | 'list' => [ 28 | 'edit.posts', 29 | 'delete.posts', 30 | ], 31 | 32 | - **entity**: (Only for `doctrine` driver) The fully qualified class name of your Permission entity. Defaults to `LaravelDoctrine\ACL\Permissions\Permission`. 33 | 34 | Roles 35 | ===== 36 | 37 | .. code-block:: php 38 | 39 | 'roles' => [ 40 | 'entity' => App\Entities\Role::class, 41 | ], 42 | 43 | - **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`. 44 | 45 | 46 | Organisations 47 | ============= 48 | 49 | .. code-block:: php 50 | 51 | 'organisations' => [ 52 | 'entity' => App\Entities\Organisation::class, 53 | ], 54 | 55 | - **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`. 56 | 57 | 58 | Entities 59 | ======== 60 | 61 | You can use the stubs as a starting point for your own entities. 62 | 63 | You may publish the stubs for the entities by running the following command: 64 | 65 | .. code-block:: bash 66 | 67 | php artisan vendor:publish --tag="acl-entities" 68 | 69 | This command will publish the stubs for the entities to the `app/Entities` directory. 70 | 71 | * [`app/Entities/Permission.php`](../stubs/Permission.php) - The stub for the Permission entity. 72 | * [`app/Entities/Role.php`](../stubs/Role.php) - The stub for the Role entity. 73 | * [`app/Entities/Organisation.php`](../stubs/Organisation.php) - The stub for the Organisation entity. 74 | 75 | > **Note**: Pay attention that we published a stub for Permission so you should update `acl.permission.entity` in the config file. 76 | 77 | .. role:: raw-html(raw) 78 | :format: html 79 | 80 | .. include:: footer.rst 81 | -------------------------------------------------------------------------------- /docs/core-concepts.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Core Concepts 3 | ============= 4 | 5 | 6 | Permissions 7 | =========== 8 | 9 | A permission is a singular ability to perform an action. 10 | 11 | Read more at `permissions `_. 12 | 13 | * Both users and roles can have permissions. 14 | * Implement ``LaravelDoctrine\ACL\Contracts\HasPermissions`` and use the ``WithPermissions`` trait. 15 | * Permissions can be managed via config or Doctrine database tables (see below). 16 | 17 | 18 | Permission Storage Drivers 19 | ========================== 20 | 21 | * Config Driver: Store permissions in ``acl.permissions.list`` as simple array in your config file. 22 | * Doctrine Driver: Store permissions in the database. Use the default ``Permission`` entity or your own (must implement `LaravelDoctrine\ACL\Contracts\Permission`). 23 | 24 | 25 | Roles 26 | ===== 27 | 28 | A role is a collection of permissions. 29 | 30 | Read more about `roles `_. 31 | 32 | * Implement ``LaravelDoctrine\ACL\Contracts\Role`` in your Role entity. 33 | * Configure ``acl.roles.entity`` in your config to point to your Role entity. 34 | * Users can have multiple roles; roles can have permissions. 35 | 36 | 37 | Organisations 38 | ============= 39 | 40 | An organisation is a group of users. 41 | 42 | Read more about `organisations `_. 43 | 44 | * Implement ``LaravelDoctrine\ACL\Contracts\Organisation`` in your organisation entity (e.g., ``Team``). 45 | * Set ``acl.organisations.entity`` in your config. 46 | * Users can belong to one or multiple organisations (implement ``BelongsToOrganisation`` or ``BelongsToOrganisations``). 47 | 48 | .. role:: raw-html(raw) 49 | :format: html 50 | 51 | .. include:: footer.rst 52 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-doctrine/acl/bbf7cb96bd7bc11186827b5402b886f36ffab2da/docs/favicon.ico -------------------------------------------------------------------------------- /docs/footer.rst: -------------------------------------------------------------------------------- 1 | 2 | ---------- 3 | 4 | This is documentation for 5 | `laravel-doctrine/acl `_. 6 | Please add your ★ star to the project. 7 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Laravel Doctrine ACL 3 | ==================== 4 | 5 | .. image:: banner.png 6 | :align: center 7 | :scale: 25 % 8 | 9 | 10 | This is the documentation for `laravel-doctrine/acl `_ 11 | 12 | An security library for Laravel and Doctrine ORM. 13 | Version 2 of this library supports Laravel 12, 14 | Doctrine ORM ^3.0, and Doctrine DBAL ^4.0. 15 | 16 | For older versions use 1.x 17 | 18 | 19 | .. toctree:: 20 | 21 | :caption: Table of Contents 22 | 23 | introduction 24 | install 25 | configurations 26 | 27 | usage 28 | core-concepts 29 | permissions 30 | roles 31 | organisations 32 | 33 | 34 | .. role:: raw-html(raw) 35 | :format: html 36 | 37 | .. include:: footer.rst 38 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Install 3 | ======= 4 | 5 | Installation of this module uses composer. For composer documentation, please 6 | refer to `getcomposer.org `_ :: 7 | 8 | .. code-block:: bash 9 | 10 | composer require laravel-doctrine/acl 11 | 12 | To publish the config use: 13 | 14 | .. code-block:: bash 15 | 16 | php artisan vendor:publish --tag="config" --provider="LaravelDoctrine\ACL\AclServiceProvider" 17 | 18 | Thanks to Laravel auto package discovery, the ServiceProvider is 19 | automatically registered. However they can still be manually registered if 20 | required (see below). 21 | 22 | Manual Registration 23 | =================== 24 | 25 | After updating composer, add the ServiceProvider to the providers 26 | array in ``bootstrap/providers.php`` 27 | 28 | .. code-block:: php 29 | 30 | LaravelDoctrine\ACL\AclServiceProvider::class, 31 | 32 | .. role:: raw-html(raw) 33 | :format: html 34 | 35 | .. include:: footer.rst 36 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Introduction 3 | ============ 4 | 5 | Laravel Doctrine ACL brings robust, flexible Access Control List (ACL) support to 6 | Laravel using Doctrine ORM. It enables you to manage permissions, roles, and organisations 7 | in a way that integrates seamlessly with Laravel’s native authorization system. 8 | 9 | * Users and roles can have permissions. 10 | * Users can belong to organisations. 11 | * Flexible permission storage (config or database). 12 | 13 | 14 | Role-Based Access Control (RBAC) 15 | ================================ 16 | 17 | RBAC is the core and most important feature of this package. 18 | 19 | * Define roles (e.g., Admin, Editor, User) as Doctrine entities. 20 | * Assign roles to users. Users can have multiple roles. 21 | * Assign permissions to roles and/or directly to users. 22 | * Permission checks automatically include both direct user permissions and those 23 | inherited through roles. 24 | * Integrates with Laravel's native authorization system (policies, gates, middleware). 25 | 26 | This enables you to implement classic RBAC, where permissions are grouped into roles and 27 | roles are assigned to users, as well as more advanced scenarios such as direct user permissions 28 | and organisational structures. 29 | 30 | 31 | 32 | .. role:: raw-html(raw) 33 | :format: html 34 | 35 | .. include:: footer.rst 36 | -------------------------------------------------------------------------------- /docs/organisations.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Organisations 3 | ============= 4 | 5 | A lot of applications have an organisations structure. Teams, 6 | Organisations, Offices, … To add this functionality to your Application, 7 | you will have to create an entity that implements 8 | ``LaravelDoctrine\ACL\Contracts\Organisation``. Next change 9 | ``acl.organisations.entity`` to your entity. 10 | 11 | .. code:: php 12 | 13 | name; 35 | } 36 | } 37 | 38 | You can use the Organisation stub as a starting point for your own entity. 39 | 40 | .. code-block:: bash 41 | 42 | php artisan vendor:publish --tag="acl-entity-organisation" 43 | 44 | This command will publish the [`Organisation`](../stubs/Organisation.php) stub for the Organisation entity to the `app/Entities` directory. 45 | 46 | > **Note**: Pay attention that we published a stub for Organisation so you should update `acl.organisation.entity` in the config file. 47 | 48 | User can belong to one organisation 49 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 50 | 51 | The User class should implement 52 | ``LaravelDoctrine\ACL\Contracts\BelongsToOrganisation``. You can use the 53 | ``#[ACL\BelongsToOrganisation]`` attribute to define the relation. 54 | 55 | .. code:: php 56 | 57 | organisation; 75 | } 76 | } 77 | 78 | User can belong to multiple organisations 79 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 80 | 81 | The User class should implement 82 | ``LaravelDoctrine\ACL\Contracts\BelongsToOrganisations``. You can use 83 | the ``#[ACL\BelongsToOrganisations]`` attribute to define the relation. 84 | 85 | .. code:: php 86 | 87 | organisations; 105 | } 106 | } 107 | 108 | Checking if a User has a certain Organisation 109 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 110 | 111 | The ``LaravelDoctrine\ACL\Organisations\BelongsToOrganisation`` trait 112 | provides methods to check if the User has a certain Organisation. 113 | 114 | .. code:: php 115 | 116 | $user->belongsToOrganisation($org); 117 | 118 | An array of Organisations or Organisation names can also be checked for. 119 | 120 | .. code:: php 121 | 122 | $user->belongsToOrganisation([$org1,$org2,$org3]); 123 | $user->belongsToOrganisation(['Company 1','Company 2','Company 3']); 124 | 125 | Specifying ``true`` for the second argument will check that **all** 126 | roles are present. 127 | 128 | .. code:: php 129 | 130 | $user->belongsToOrganisation([$org1,$org2,$org3], true); //User must belong to all three organisations to return true 131 | $user->belongsToOrganisation(['Company 1','Company 2','Company 3'], true); 132 | 133 | 134 | .. role:: raw-html(raw) 135 | :format: html 136 | 137 | .. include:: footer.rst 138 | -------------------------------------------------------------------------------- /docs/permissions.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Permissions 3 | =========== 4 | 5 | Both User and Role can have permissions. To add this behaviour we can 6 | simply add the ``LaravelDoctrine\ACL\Contracts\HasPermissions`` 7 | interface to them. We can also add the 8 | ``LaravelDoctrine\ACL\Permissions\WithPermissions`` trait to have some 9 | nice helpers. We can use the ``#[ACL\HasPermissions]`` attribute to 10 | define the permissions relation. 11 | 12 | .. code:: php 13 | 14 | permissions; 32 | } 33 | } 34 | 35 | You can use the Permission stub as a starting point for your own entity. 36 | 37 | .. code-block:: bash 38 | 39 | php artisan vendor:publish --tag="acl-entity-permission" 40 | 41 | This command will publish the [`Permission`](../stubs/Permission.php) stub for the Permission entity to the `app/Entities` directory. 42 | 43 | > **Note**: Pay attention that we published a stub for Permission so you should update `acl.permission.entity` in the config file. 44 | 45 | Getting all permissions 46 | ~~~~~~~~~~~~~~~~~~~~~~~ 47 | 48 | You can get a list of all permissions with the 49 | ``LaravelDoctrine\ACL\PermissionManager`` 50 | 51 | .. code:: php 52 | 53 | $manager = app(PermissionManager::class); 54 | $manager->getAllPermissions(); 55 | 56 | Config Permissions 57 | ~~~~~~~~~~~~~~~~~~ 58 | 59 | By setting the permissions driver to ``config``, no additional 60 | ``permissions`` table will be created, but permissions will be expected 61 | to be added inside the config: ``acl.permissions.list`` The given 62 | permissions will now be stored in the Entity as json. 63 | 64 | .. code:: php 65 | 66 | [ 70 | 'driver' => 'config', 71 | 'list' => [ 72 | 'create.posts' 73 | ] 74 | ] 75 | ]; 76 | 77 | Database Permissions 78 | ~~~~~~~~~~~~~~~~~~~~ 79 | 80 | By setting the permissions driver to ``doctrine``, an additional 81 | ``permissions`` table will be created. Permissions will be stored in 82 | Pivot tables for roles and users. A default ``Permission`` entity is 83 | included in this package. You can replace that one by your own inside 84 | the config as long as it implements the 85 | ``LaravelDoctrine\ACL\Contracts\Permission`` interface. 86 | 87 | Checking if a User or Role has permission 88 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | On the User or Role entity 91 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 92 | 93 | When adding the ``LaravelDoctrine\ACL\Permissions\WithPermissions`` trait 94 | you will get a ``hasPermissionTo`` method. First the ``User`` entity 95 | will check if it has the right permission itself. If not it will search 96 | in its roles. If none of them has permission, it will return false. 97 | 98 | .. code:: php 99 | 100 | $user->hasPermissionTo('create.posts'); 101 | $role->hasPermissionTo('create.posts'); 102 | 103 | An array of permissions can also checked for. 104 | 105 | .. code:: php 106 | 107 | $user->hasPermissionTo(['create.posts','create.page']); 108 | $role->hasPermissionTo(['create.posts','create.page']); 109 | 110 | Specifying ``true`` for the second argument will check that **all** 111 | permissions are present. 112 | 113 | .. code:: php 114 | 115 | $user->hasPermissionTo(['create.posts','create.page'], true); //all permissions are required to return true 116 | $role->hasPermissionTo(['create.posts','create.page'], true); 117 | 118 | Using the Gate helper 119 | ^^^^^^^^^^^^^^^^^^^^^ 120 | 121 | All permissions are automatically defined inside Laravel’s Gate helper. 122 | 123 | .. code:: php 124 | 125 | Gate::allows('create.posts'); 126 | @can('create.posts'); 127 | $user->can('create.posts'); 128 | 129 | Using Permissions Middleware with Gate 130 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 131 | 132 | You can use Laravel’s built-in ``can`` middleware to protect routes 133 | based on permissions defined by Gate (and thus by this package): 134 | 135 | .. code:: php 136 | 137 | // Require a specific permission for this route 138 | Route::post('/posts', function () { 139 | // Only users with the 'create.posts' permission can access this route 140 | })->middleware('can:create.posts'); 141 | 142 | // Or using the route's can() method (Laravel 9+) 143 | Route::post('/posts', function () { 144 | // Only users with the 'create.posts' permission can access this route 145 | })->can('create.posts'); 146 | 147 | If the user does not have the required permission, Laravel will return a 148 | 403 response automatically. 149 | 150 | You can also check multiple permissions by creating custom middleware or 151 | using Gate logic in controllers. 152 | 153 | 154 | 155 | .. role:: raw-html(raw) 156 | :format: html 157 | 158 | .. include:: footer.rst 159 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | guzzle_sphinx_theme 2 | -------------------------------------------------------------------------------- /docs/roles.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Roles 3 | ===== 4 | 5 | Role Entity 6 | =========== 7 | 8 | To add Roles to your application, you’ll have to create a ``Role`` 9 | entity. This entity should implement 10 | ``LaravelDoctrine\ACL\Contracts\Role``. Next you should change the class 11 | name the ``acl.roles.entity`` config to your class, by default this is 12 | set to ``App\Entities\Role``. 13 | 14 | .. code:: php 15 | 16 | name; 37 | } 38 | } 39 | 40 | You can use the Role stub as a starting point for your own entity. 41 | 42 | .. code-block:: bash 43 | 44 | php artisan vendor:publish --tag="acl-entity-role" 45 | 46 | This command will publish the [`Role`](../stubs/Role.php) stub for the Role entity to the `app/Entities` directory. 47 | 48 | > **Note**: Pay attention that we published a stub for Role so you should update `acl.role.entity` in the config file. 49 | 50 | A User has Roles 51 | ---------------- 52 | 53 | Inside your ``User`` entity, you have to define the relation with the 54 | role. The ``User`` entity should implement the 55 | ``LaravelDoctrine\ACL\Contracts\HasRoles`` interface. You can use the 56 | ``#[ACL\HasRoles]`` attribute to define the relations (instead of 57 | defining the ManyToMany manually). Import 58 | ``use LaravelDoctrine\ACL\Attribute as ACL;`` in top of the class. 59 | 60 | .. code:: php 61 | 62 | roles; 85 | } 86 | } 87 | 88 | How Permissions Are Checked with Roles 89 | -------------------------------------- 90 | 91 | When you assign roles to a user, permission checks are performed on both 92 | the user and their roles. This means: 93 | 94 | - If a user does **not** have a permission directly, but one of their 95 | roles has that permission, the user is considered to have that 96 | permission. 97 | - If you call ``$user->hasPermissionTo('edit.posts')``, the system 98 | will: 99 | 100 | 1. Check the user’s direct permissions. 101 | 2. If not found, check all permissions assigned to each of the user’s 102 | roles. 103 | 104 | - This logic is implemented in the ``WithPermissions`` trait (see 105 | source), which first checks the user’s permissions, then iterates 106 | over all roles (if any) and checks their permissions recursively. 107 | 108 | **Pseudocode:** 109 | 110 | .. code:: php 111 | 112 | function hasPermissionTo($permission) { 113 | if (this->hasPermissionDirectly($permission)) { 114 | return true; 115 | } 116 | foreach ($this->roles as $role) { 117 | if ($role->hasPermission($permission)) { 118 | return true; 119 | } 120 | } 121 | return false; 122 | } 123 | 124 | This allows for flexible RBAC: grant permissions to roles, assign roles 125 | to users, and users inherit all permissions from their roles 126 | automatically. 127 | 128 | Checking if a User has a certain Role 129 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 130 | 131 | The ``LaravelDoctrine\ACL\Roles\HasRoles`` trait provides methods to 132 | check if the User has a certain Role. 133 | 134 | .. code:: php 135 | 136 | $user->hasRole($role); 137 | $user->hasRoleByName('Super Admin'); 138 | 139 | An array of roles or role names can also checked for. 140 | 141 | .. code:: php 142 | 143 | $user->hasRole([$role1,$role2,$role3]); 144 | $user->hasRoleByName(['User','Admin','Manager']); 145 | 146 | Specifying ``true`` for the second argument will check that **all** 147 | roles are present. 148 | 149 | .. code:: php 150 | 151 | $user->hasRole([$role1,$role2,$role3], true); //User must have all roles 152 | $user->hasRoleByName(['User','Admin','Manager'], true); 153 | 154 | 155 | 156 | .. role:: raw-html(raw) 157 | :format: html 158 | 159 | .. include:: footer.rst 160 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | 6 | Powerful RBAC with Roles & Permissions 7 | ====================================== 8 | 9 | * Assign roles to users by implementing ``HasRoles`` and using the ``HasRoles`` trait. 10 | * Assign permissions directly to users or to roles for flexible, scalable RBAC. 11 | * Users inherit all permissions from their assigned roles automatically. 12 | 13 | .. code-block:: php 14 | 15 | $user->hasRole('admin'); // Check if user has a role 16 | $user->hasPermissionTo('edit.posts'); // Checks both direct and role permissions 17 | $user->hasPermissionTo(['edit.posts', 'publish.articles']); // Any permission 18 | $user->hasPermissionTo(['edit.posts', 'publish.articles'], true); // All permissions 19 | 20 | Seamless Integration with Laravel Gate 21 | ====================================== 22 | 23 | All permissions are automatically available via Laravel's Gate, allowing you to use 24 | familiar authorization patterns: 25 | 26 | .. code-block:: php 27 | 28 | // In controllers or policies 29 | if (Gate::allows('edit.posts')) { 30 | // User can edit posts 31 | } 32 | 33 | Protecting Routes with RBAC 34 | =========================== 35 | 36 | You can also protect routes using middleware: 37 | 38 | .. code-block:: php 39 | 40 | // Or via middleware 41 | Route::post('/posts', function () { 42 | // ... 43 | })->middleware('can:edit.posts'); 44 | 45 | Route::group(['middleware' => ['can:manage.users']], function () { 46 | // Only users with 'manage.users' permission (direct or via role) can access these routes 47 | }); 48 | 49 | Policy-based checks 50 | =================== 51 | 52 | You can define custom policies for your models or actions and use permissions or roles 53 | inside your policy methods: 54 | 55 | .. code-block:: php 56 | 57 | // app/Policies/PostPolicy.php 58 | public function update(User $user, Post $post) 59 | { 60 | // Use permissions or roles 61 | return $user->hasPermissionTo('edit.posts') || $user->hasRole('editor'); 62 | } 63 | 64 | 65 | This allows you to combine RBAC with custom business logic for fine-grained authorization. 66 | 67 | Getting All Permissions 68 | ======================= 69 | 70 | Use the `PermissionManager` to retrieve all permissions: 71 | 72 | .. code-block:: php 73 | 74 | $manager = app(LaravelDoctrine\ACL\PermissionManager::class); 75 | $manager->getAllPermissions(); 76 | 77 | 78 | 79 | .. role:: raw-html(raw) 80 | :format: html 81 | 82 | .. include:: footer.rst 83 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | src 14 | tests 15 | workbench/app 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 0 3 | paths: 4 | - src -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/ 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/AclServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishConfig(); 23 | $this->publishEntities(); 24 | } 25 | 26 | public function register(): void 27 | { 28 | $this->mergeConfig(); 29 | 30 | $this->registerPaths(); 31 | $this->registerGatePermissions(); 32 | $this->registerDoctrineMappings(); 33 | } 34 | 35 | protected function registerDoctrineMappings(): void 36 | { 37 | $manager = $this->app->make(DoctrineManager::class); 38 | $manager->extendAll(RegisterMappedEventSubscribers::class); 39 | } 40 | 41 | protected function registerPaths(): void 42 | { 43 | $permissionManager = $this->app->make(PermissionManager::class); 44 | 45 | if (! $permissionManager->useDefaultPermissionEntity()) { 46 | return; 47 | } 48 | 49 | $manager = $this->app->make(DoctrineManager::class); 50 | $manager->addPaths([ 51 | __DIR__ . DIRECTORY_SEPARATOR . 'Permissions', 52 | ]); 53 | } 54 | 55 | protected function registerGatePermissions(): void 56 | { 57 | $this->app->afterResolving(Gate::class, function (Gate $gate): void { 58 | $manager = $this->app->make(PermissionManager::class); 59 | 60 | foreach ($manager->getPermissionsWithDotNotation() as $permission) { 61 | $gate->define($permission, static function (HasPermissions $user) use ($permission) { 62 | return $user->hasPermissionTo($permission); 63 | }); 64 | } 65 | }); 66 | } 67 | 68 | protected function publishConfig(): void 69 | { 70 | $this->publishes([ 71 | $this->getConfigPath() => config_path('acl.php'), 72 | ], 'config'); 73 | } 74 | 75 | protected function mergeConfig(): void 76 | { 77 | $this->mergeConfigFrom( 78 | $this->getConfigPath(), 79 | 'acl', 80 | ); 81 | } 82 | 83 | protected function getConfigPath(): string 84 | { 85 | return __DIR__ . '/../config/acl.php'; 86 | } 87 | 88 | /** 89 | * Publish default entity stubs separately with specific tags/groups. 90 | */ 91 | protected function publishEntities(): void 92 | { 93 | // Permission entity 94 | $this->publishes([ 95 | __DIR__ . '/../stubs/Permission.php' => app_path('Entities/Permission.php'), 96 | ], ['acl-entities', 'acl-entity-permission']); 97 | 98 | // Role entity 99 | $this->publishes([ 100 | __DIR__ . '/../stubs/Role.php' => app_path('Entities/Role.php'), 101 | ], ['acl-entities', 'acl-entity-role']); 102 | 103 | // Organisation entity 104 | $this->publishes([ 105 | __DIR__ . '/../stubs/Organisation.php' => app_path('Entities/Organisation.php'), 106 | ], ['acl-entities', 'acl-entity-organisation']); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Attribute/BelongsToOrganisation.php: -------------------------------------------------------------------------------- 1 | targetEntity = $targetEntity; 22 | $this->cascade = $cascade; 23 | $this->fetch = $fetch; 24 | $this->orphanRemoval = $orphanRemoval; 25 | $this->indexBy = $indexBy; 26 | } 27 | 28 | public function getTargetEntity(Config $config): string|null 29 | { 30 | return $this->targetEntity ?: $config->get('acl.organisations.entity', 'Organisation'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Attribute/BelongsToOrganisations.php: -------------------------------------------------------------------------------- 1 | targetEntity = $targetEntity; 23 | $this->mappedBy = $mappedBy; 24 | $this->cascade = $cascade; 25 | $this->fetch = $fetch; 26 | $this->orphanRemoval = $orphanRemoval; 27 | $this->indexBy = $indexBy; 28 | } 29 | 30 | public function getTargetEntity(Config $config): string|null 31 | { 32 | return $this->targetEntity ?: $config->get('acl.organisations.entity', 'Organisation'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Attribute/HasPermissions.php: -------------------------------------------------------------------------------- 1 | targetEntity = $targetEntity; 23 | $this->mappedBy = $mappedBy; 24 | $this->cascade = $cascade; 25 | $this->fetch = $fetch; 26 | $this->orphanRemoval = $orphanRemoval; 27 | $this->indexBy = $indexBy; 28 | } 29 | 30 | public function getTargetEntity(Config $config): string|null 31 | { 32 | // Config driver has no target entity 33 | if ($config->get('acl.permissions.driver', 'config') === 'config') { 34 | return null; 35 | } 36 | 37 | return $this->targetEntity ?: $config->get('acl.permissions.entity', 'Permission'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Attribute/HasRoles.php: -------------------------------------------------------------------------------- 1 | targetEntity = $targetEntity; 23 | $this->mappedBy = $mappedBy; 24 | $this->cascade = $cascade; 25 | $this->fetch = $fetch; 26 | $this->orphanRemoval = $orphanRemoval; 27 | $this->indexBy = $indexBy; 28 | } 29 | 30 | public function getTargetEntity(Config $config): string|null 31 | { 32 | return $this->targetEntity ?: $config->get('acl.roles.entity', 'Role'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Attribute/MappingAttribute.php: -------------------------------------------------------------------------------- 1 | config->get('acl.permissions.list', [])); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Configurations/DoctrinePermissionsProvider.php: -------------------------------------------------------------------------------- 1 | getEntityManager(); 25 | $metadata = $em->getClassMetadata($this->getPermissionClass()); 26 | 27 | return new Doctrine(new EntityRepository($em, $metadata)); 28 | } 29 | 30 | protected function getPermissionClass(): string 31 | { 32 | $class = $this->config->get('acl.permissions.entity'); 33 | 34 | if (! $class) { 35 | throw new RunTimeException( 36 | 'Failed to configure doctrine permissions. No entity class provided.', 37 | ); 38 | } 39 | 40 | return $class; 41 | } 42 | 43 | protected function getEntityManager(): EntityManagerInterface 44 | { 45 | $em = $this->registry->getManagerForClass($this->getPermissionClass()); 46 | 47 | if (! $em) { 48 | throw new RunTimeException( 49 | 'Failed to configure doctrine permissions.' 50 | . ' No entity manager found for entity: ' . $this->getPermissionClass() . '.', 51 | ); 52 | } 53 | 54 | return $em; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Configurations/PermissionsProvider.php: -------------------------------------------------------------------------------- 1 | |Organisation[] */ 12 | public function getOrganisations(): Collection|array; 13 | } 14 | -------------------------------------------------------------------------------- /src/Contracts/HasPermissions.php: -------------------------------------------------------------------------------- 1 | |Permission[]|Collection|string[] */ 14 | public function getPermissions(): Collection|array; 15 | } 16 | -------------------------------------------------------------------------------- /src/Contracts/HasRoles.php: -------------------------------------------------------------------------------- 1 | |Role[] */ 12 | public function getRoles(): Collection|array; 13 | } 14 | -------------------------------------------------------------------------------- /src/Contracts/Organisation.php: -------------------------------------------------------------------------------- 1 | $property->getName(), 27 | 'type' => Types::JSON, 28 | ], 29 | ); 30 | 31 | $builder->build(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Mappings/Builders/ManyToManyBuilder.php: -------------------------------------------------------------------------------- 1 | $property->getName(), 27 | 'targetEntity' => $attribute->getTargetEntity($this->config), 28 | ], 29 | OrmClassMetadata::MANY_TO_MANY, 30 | ); 31 | 32 | if (isset($attribute->inversedBy) && $attribute->inversedBy) { 33 | $builder->inversedBy($attribute->inversedBy); 34 | } 35 | 36 | if (isset($attribute->mappedBy) && $attribute->mappedBy) { 37 | $builder->mappedBy($attribute->mappedBy); 38 | } 39 | 40 | $builder->build(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Mappings/Builders/ManyToOneBuilder.php: -------------------------------------------------------------------------------- 1 | $property->getName(), 27 | 'targetEntity' => $attribute->getTargetEntity($this->config), 28 | ], 29 | OrmClassMetadata::MANY_TO_ONE, 30 | ); 31 | 32 | if (isset($attribute->inversedBy) && $attribute->inversedBy) { 33 | $builder->inversedBy($attribute->inversedBy); 34 | } 35 | 36 | $builder->build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Mappings/RegisterMappedEventSubscribers.php: -------------------------------------------------------------------------------- 1 | > $subscribers */ 23 | protected array $subscribers = [ 24 | BelongsToOrganisationsSubscriber::class, 25 | BelongsToOrganisationSubscriber::class, 26 | HasRolesSubscriber::class, 27 | HasPermissionsSubscriber::class, 28 | ]; 29 | 30 | public function extend(Configuration $configuration, Connection $connection, EventManager $eventManager): void 31 | { 32 | $config = app(Config::class); 33 | foreach ($this->subscribers as $subscriber) { 34 | $eventManager->addEventSubscriber(new $subscriber($config)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Mappings/Subscribers/BelongsToOrganisationSubscriber.php: -------------------------------------------------------------------------------- 1 | getInstance($metadata) instanceof BelongsToOrganisationContract; 24 | } 25 | 26 | protected function getBuilder(MappingAttribute $attribute): Builder 27 | { 28 | return new ManyToOneBuilder($this->config); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Mappings/Subscribers/BelongsToOrganisationsSubscriber.php: -------------------------------------------------------------------------------- 1 | getInstance($metadata) instanceof BelongsToOrganisationsContract; 24 | } 25 | 26 | protected function getBuilder(MappingAttribute $attribute): Builder 27 | { 28 | return new ManyToManyBuilder($this->config); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Mappings/Subscribers/HasPermissionsSubscriber.php: -------------------------------------------------------------------------------- 1 | getInstance($metadata) instanceof HasPermissionsContract; 20 | } 21 | 22 | public function getAttributeClass(): string 23 | { 24 | return HasPermissions::class; 25 | } 26 | 27 | protected function getBuilder(MappingAttribute $attribute): Builder 28 | { 29 | // If there's a target entity, create pivot table 30 | if ($attribute->getTargetEntity($this->config)) { 31 | return new ManyToManyBuilder($this->config); 32 | } 33 | 34 | // Else save the permissions inside the table as json 35 | return new JsonArrayBuilder($this->config); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Mappings/Subscribers/HasRolesSubscriber.php: -------------------------------------------------------------------------------- 1 | getInstance($metadata) instanceof HasRolesContract; 24 | } 25 | 26 | protected function getBuilder(MappingAttribute $attribute): Builder 27 | { 28 | return new ManyToManyBuilder($this->config); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Mappings/Subscribers/MappedEventSubscriber.php: -------------------------------------------------------------------------------- 1 | */ 19 | abstract public function getAttributeClass(): string; 20 | 21 | abstract protected function shouldBeMapped(ClassMetadata $metadata): bool; 22 | 23 | abstract protected function getBuilder(MappingAttribute $attribute): Builder; 24 | 25 | public function __construct(protected Config $config) 26 | { 27 | } 28 | 29 | /** @return array */ 30 | public function getSubscribedEvents(): array 31 | { 32 | return [ 33 | Events::loadClassMetadata, 34 | ]; 35 | } 36 | 37 | public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void 38 | { 39 | $metadata = $eventArgs->getClassMetadata(); 40 | 41 | if (! $this->isInstantiable($metadata) || ! $this->shouldBeMapped($metadata)) { 42 | return; 43 | } 44 | 45 | foreach ($metadata->getReflectionClass()->getProperties() as $property) { 46 | foreach ($property->getAttributes($this->getAttributeClass()) as $refAttr) { 47 | $attribute = $refAttr->newInstance(); 48 | $builder = $this->getBuilder($attribute); 49 | $builder->build($metadata, $property, $attribute); 50 | } 51 | } 52 | } 53 | 54 | protected function getInstance(ClassMetadata $metadata): object 55 | { 56 | $reflection = new ReflectionClass($metadata->getName()); 57 | 58 | return $reflection->newInstanceWithoutConstructor(); 59 | } 60 | 61 | protected function isInstantiable(ClassMetadata $metadata): bool 62 | { 63 | if ($metadata->isMappedSuperclass) { 64 | return false; 65 | } 66 | 67 | return $metadata->getReflectionClass() && ! $metadata->getReflectionClass()->isAbstract(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Organisations/BelongsToOrganisation.php: -------------------------------------------------------------------------------- 1 | belongsToOrganisation($o); 20 | 21 | if ($hasOrganisation && ! $requireAll) { 22 | return true; 23 | } 24 | 25 | if (! $hasOrganisation && $requireAll) { 26 | return false; 27 | } 28 | } 29 | 30 | return $requireAll; 31 | } 32 | 33 | if ($this instanceof BelongsToOrganisationContract) { 34 | if ($this->getOrganisation() && $this->getOrganisationName($org) === $this->getOrganisation()->getName()) { 35 | return true; 36 | } 37 | } 38 | 39 | if ($this instanceof BelongsToOrganisations) { 40 | foreach ($this->getOrganisations() as $o) { 41 | if ($this->getOrganisationName($org) === $o->getName()) { 42 | return true; 43 | } 44 | } 45 | } 46 | 47 | return false; 48 | } 49 | 50 | protected function getOrganisationName(Organisation|string $org): string 51 | { 52 | return $org instanceof Organisation ? $org->getName() : $org; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/PermissionManager.php: -------------------------------------------------------------------------------- 1 | */ 20 | public function getPermissionsWithDotNotation(): array 21 | { 22 | $permissions = $this->driver()->getAllPermissions(); 23 | 24 | $list = $this->convertToDotArray( 25 | $permissions->toArray(), 26 | ); 27 | 28 | return Arr::flatten($list); 29 | } 30 | 31 | /** 32 | * @param array|string $permissions 33 | * 34 | * @return array 35 | */ 36 | protected function convertToDotArray(array|string $permissions, string $prepend = ''): array 37 | { 38 | $list = []; 39 | if (is_array($permissions)) { 40 | foreach ($permissions as $key => $permission) { 41 | $list[] = $this->convertToDotArray($permission, ! is_numeric($key) ? $prepend . $key . '.' : $prepend); 42 | } 43 | } else { 44 | $list[] = $prepend . $permissions; 45 | } 46 | 47 | return $list; 48 | } 49 | 50 | /** 51 | * Get the default driver name. 52 | */ 53 | public function getDefaultDriver(): string 54 | { 55 | return $this->container->make('config')->get('acl.permissions.driver', 'config'); 56 | } 57 | 58 | public function getNamespace(): string 59 | { 60 | return __NAMESPACE__ . '\\Configurations'; 61 | } 62 | 63 | public function getClassSuffix(): string 64 | { 65 | return 'PermissionsProvider'; 66 | } 67 | 68 | public function useDefaultPermissionEntity(): bool 69 | { 70 | if (! $this->needsDoctrine()) { 71 | return false; 72 | } 73 | 74 | $entityFqn = $this->container->make('config')->get('acl.permissions.entity', ''); 75 | $entityFqn = ltrim($entityFqn, '\\'); 76 | 77 | return $entityFqn === Permission::class; 78 | } 79 | 80 | public function needsDoctrine(): bool 81 | { 82 | return $this->getDefaultDriver() === 'doctrine'; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Permissions/Driver/Config.php: -------------------------------------------------------------------------------- 1 | */ 14 | public function __construct(array $permissions) 15 | { 16 | $this->collection = new Collection($permissions); 17 | } 18 | 19 | public function getAllPermissions(): Collection 20 | { 21 | return $this->collection; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Permissions/Driver/Doctrine.php: -------------------------------------------------------------------------------- 1 | repository->findAll(); 23 | $permissions = array_map(static fn (Permission $permission) => $permission->getName(), $permissions); 24 | 25 | return new Collection($permissions); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Permissions/Driver/PermissionDriver.php: -------------------------------------------------------------------------------- 1 | name = $name; 24 | } 25 | 26 | public function getId(): int|null 27 | { 28 | return $this->id; 29 | } 30 | 31 | public function getName(): string 32 | { 33 | return $this->name; 34 | } 35 | 36 | public function setName(string $name): self 37 | { 38 | $this->name = $name; 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Permissions/WithPermissions.php: -------------------------------------------------------------------------------- 1 | hasPermissionTo($n); 20 | 21 | if ($hasPermission && ! $requireAll) { 22 | return true; 23 | } 24 | 25 | if (! $hasPermission && $requireAll) { 26 | return false; 27 | } 28 | } 29 | 30 | return $requireAll; 31 | } 32 | 33 | if ($this instanceof HasPermissionsContract) { 34 | foreach ($this->getPermissions() as $permission) { 35 | if ($this->getPermissionName($permission) === $this->getPermissionName($name)) { 36 | return true; 37 | } 38 | } 39 | } 40 | 41 | if ($this instanceof HasRoles) { 42 | foreach ($this->getRoles() as $role) { 43 | if (! ($role instanceof HasPermissionsContract)) { 44 | continue; 45 | } 46 | 47 | if ($role->hasPermissionTo($name)) { 48 | return true; 49 | } 50 | } 51 | } 52 | 53 | return false; 54 | } 55 | 56 | protected function getPermissionName(Permission|string $permission): string 57 | { 58 | return $permission instanceof Permission ? $permission->getName() : $permission; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Roles/WithRoles.php: -------------------------------------------------------------------------------- 1 | hasRole($r); 19 | 20 | if ($hasRole && ! $requireAll) { 21 | return true; 22 | } 23 | 24 | if (! $hasRole && $requireAll) { 25 | return false; 26 | } 27 | } 28 | 29 | return $requireAll; 30 | } 31 | 32 | foreach ($this->getRoles() as $ownedRole) { 33 | if ($ownedRole === $role) { 34 | return true; 35 | } 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public function hasRoleByName(string|array $name, bool $requireAll = false): bool 42 | { 43 | if (is_array($name)) { 44 | foreach ($name as $n) { 45 | $hasRole = $this->hasRoleByName($n); 46 | 47 | if ($hasRole && ! $requireAll) { 48 | return true; 49 | } 50 | 51 | if (! $hasRole && $requireAll) { 52 | return false; 53 | } 54 | } 55 | 56 | return $requireAll; 57 | } 58 | 59 | foreach ($this->getRoles() as $ownedRole) { 60 | if ($ownedRole->getName() === $name) { 61 | return true; 62 | } 63 | } 64 | 65 | return false; 66 | } 67 | 68 | /** @return Collection|Role[] */ 69 | abstract public function getRoles(): Collection|array; 70 | } 71 | -------------------------------------------------------------------------------- /stubs/Organisation.php: -------------------------------------------------------------------------------- 1 | id; 24 | } 25 | 26 | public function getName(): string 27 | { 28 | return $this->name; 29 | } 30 | 31 | public function setName(string $name): static 32 | { 33 | $this->name = $name; 34 | 35 | return $this; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /stubs/Permission.php: -------------------------------------------------------------------------------- 1 | id; 22 | } 23 | 24 | public function getName(): string 25 | { 26 | return $this->name; 27 | } 28 | 29 | public function setName(string $name): self 30 | { 31 | $this->name = $name; 32 | 33 | return $this; 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /stubs/Role.php: -------------------------------------------------------------------------------- 1 | */ 31 | #[ACL\HasPermissions] 32 | public Collection $permissions; 33 | 34 | public function __construct() 35 | { 36 | $this->permissions = new ArrayCollection(); 37 | } 38 | 39 | public function getId(): int|null 40 | { 41 | return $this->id; 42 | } 43 | 44 | public function getName(): string 45 | { 46 | return $this->name; 47 | } 48 | 49 | public function setName(string $name): self 50 | { 51 | $this->name = $name; 52 | 53 | return $this; 54 | } 55 | 56 | /** @return Collection */ 57 | public function getPermissions(): Collection 58 | { 59 | return $this->permissions; 60 | } 61 | 62 | /** @param Collection|Permission[] $permissions */ 63 | public function setPermissions(Collection|array $permissions): self 64 | { 65 | $this->permissions = is_array($permissions) ? new ArrayCollection($permissions) : $permissions; 66 | 67 | return $this; 68 | } 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /testbench.yaml: -------------------------------------------------------------------------------- 1 | laravel: ./workbench 2 | workbench: 3 | start: '/' 4 | install: true 5 | health: false 6 | discovers: 7 | web: false 8 | api: false 9 | commands: false 10 | components: false 11 | views: false 12 | build: 13 | - asset-publish 14 | - create-sqlite-db 15 | - db-wipe 16 | sync: [] 17 | -------------------------------------------------------------------------------- /tests/Configurations/DoctrinePermissionsProviderTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('get')->with('acl.permissions.entity')->andReturn(null); 27 | 28 | $provider = new DoctrinePermissionsProvider($registry, $config); 29 | 30 | $this->expectException(RuntimeException::class); 31 | $this->expectExceptionMessage('Failed to configure doctrine permissions. No entity class provided.'); 32 | // Call protected method via reflection 33 | $reflection = new ReflectionClass($provider); 34 | $method = $reflection->getMethod('getPermissionClass'); 35 | $method->setAccessible(true); 36 | $method->invoke($provider); 37 | } 38 | 39 | public function testThrowsExceptionWhenNoEntityManagerFound(): void 40 | { 41 | $registry = m::mock(ManagerRegistry::class); 42 | $config = m::mock(Repository::class); 43 | $config->shouldReceive('get')->with('acl.permissions.entity')->andReturn('Some\\Entity\\Class'); 44 | $registry->shouldReceive('getManagerForClass')->with('Some\\Entity\\Class')->andReturn(null); 45 | 46 | $provider = new DoctrinePermissionsProvider($registry, $config); 47 | 48 | $this->expectException(RuntimeException::class); 49 | $this->expectExceptionMessage('Failed to configure doctrine permissions. No entity manager found for entity: Some\\Entity\\Class.'); 50 | // Call protected method via reflection 51 | $reflection = new ReflectionClass($provider); 52 | $method = $reflection->getMethod('getEntityManager'); 53 | $method->setAccessible(true); 54 | $method->invoke($provider); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Configurations/PermissionManagerTest.php: -------------------------------------------------------------------------------- 1 | driver = m::mock(PermissionDriver::class); 32 | 33 | $this->container = m::mock(Container::class); 34 | 35 | $this->manager = new PermissionManager($this->container); 36 | $this->manager->extend('config', function () { 37 | return $this->driver; 38 | }); 39 | } 40 | 41 | protected function tearDown(): void 42 | { 43 | m::close(); 44 | } 45 | 46 | public function testCanDotNotatedArrayOfPermissions(): void 47 | { 48 | $this->driver->shouldReceive('getAllPermissions')->once()->andReturn(new Collection([ 49 | 'permissionKey2' => [ 50 | 'permissionValue1', 51 | 'permissionValue2', 52 | ], 53 | 'permissionKey3' => [ 54 | 'permissionKey4' => [ 55 | 'permissionValue3', 56 | 'permissionValue4', 57 | ], 58 | ], 59 | ])); 60 | 61 | $config = m::mock(Repository::class); 62 | 63 | $this->container->shouldReceive('make')->with('config')->andReturn($config); 64 | 65 | $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); 66 | 67 | $this->assertEquals([ 68 | 'permissionKey2.permissionValue1', 69 | 'permissionKey2.permissionValue2', 70 | 'permissionKey3.permissionKey4.permissionValue3', 71 | 'permissionKey3.permissionKey4.permissionValue4', 72 | ], $this->manager->getPermissionsWithDotNotation()); 73 | } 74 | 75 | public function testWhenShouldUseDefaultPermissionEntity(): void 76 | { 77 | $config = m::mock(Repository::class); 78 | 79 | $this->container->shouldReceive('make')->with('config')->andReturn($config); 80 | 81 | $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('doctrine'); 82 | 83 | // Tests for leading slashes in case someone is providing a manually written FQN 84 | $config->shouldReceive('get')->with('acl.permissions.entity', null)->andReturn('\\' . Permission::class); 85 | 86 | $this->assertTrue($this->manager->useDefaultPermissionEntity()); 87 | } 88 | 89 | public function testWhenShouldNotUseDefaultPermissionEntityBecauseDriverIsNotDoctrine(): void 90 | { 91 | $config = m::mock(Repository::class); 92 | 93 | $this->container->shouldReceive('make')->with('config')->andReturn($config); 94 | 95 | $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); 96 | $config->shouldReceive('get')->with('acl.permissions.entity', null)->andReturn(Permission::class); 97 | 98 | $this->assertFalse($this->manager->useDefaultPermissionEntity()); 99 | } 100 | 101 | public function testWhenShouldNotUseDefaultPermissionEntityBecauseEntityIsDifferent(): void 102 | { 103 | $config = m::mock(Repository::class); 104 | 105 | $this->container->shouldReceive('make')->with('config')->andReturn($config); 106 | 107 | $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); 108 | $config->shouldReceive('get')->with('acl.permissions.entity', null)->andReturn('Namespace\Class'); 109 | 110 | $this->assertFalse($this->manager->useDefaultPermissionEntity()); 111 | } 112 | 113 | public function testNeedsDoctrine(): void 114 | { 115 | $config = m::mock(Repository::class); 116 | 117 | $this->container->shouldReceive('make')->with('config')->andReturn($config); 118 | 119 | $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('doctrine'); 120 | 121 | $this->assertTrue($this->manager->needsDoctrine()); 122 | } 123 | 124 | public function testDoesNotNeedDoctrine(): void 125 | { 126 | $config = m::mock(Repository::class); 127 | 128 | $this->container->shouldReceive('make')->with('config')->andReturn($config); 129 | 130 | $config->shouldReceive('get')->with('acl.permissions.driver', 'config')->andReturn('config'); 131 | 132 | $this->assertFalse($this->manager->needsDoctrine()); 133 | } 134 | 135 | public function testThrowsDriverNotFoundException(): void 136 | { 137 | $this->expectException(DriverNotFound::class); 138 | $manager = new PermissionManager($this->container); 139 | // Do not extend with any driver, so the requested driver does not exist 140 | $manager->driver('nonexistent'); 141 | } 142 | 143 | public function testDoctrinePermissionsProviderThrowsExceptionWhenNoEntityClassProvided(): void 144 | { 145 | $registry = m::mock(ManagerRegistry::class); 146 | $config = m::mock(Repository::class); 147 | $config->shouldReceive('get')->with('acl.permissions.entity')->andReturn(null); 148 | 149 | $provider = new DoctrinePermissionsProvider($registry, $config); 150 | 151 | $this->expectException(RuntimeException::class); 152 | $this->expectExceptionMessage('Failed to configure doctrine permissions. No entity class provided.'); 153 | // Call protected method via reflection 154 | $reflection = new ReflectionClass($provider); 155 | $method = $reflection->getMethod('getPermissionClass'); 156 | $method->setAccessible(true); 157 | $method->invoke($provider); 158 | } 159 | 160 | public function testDoctrinePermissionsProviderThrowsExceptionWhenNoEntityManagerFound(): void 161 | { 162 | $registry = m::mock(ManagerRegistry::class); 163 | $config = m::mock(Repository::class); 164 | $config->shouldReceive('get')->with('acl.permissions.entity')->andReturn('Some\\Entity\\Class'); 165 | $registry->shouldReceive('getManagerForClass')->with('Some\\Entity\\Class')->andReturn(null); 166 | 167 | $provider = new DoctrinePermissionsProvider($registry, $config); 168 | 169 | $this->expectException(RuntimeException::class); 170 | $this->expectExceptionMessage('Failed to configure doctrine permissions. No entity manager found for entity: Some\\Entity\\Class.'); 171 | // Call protected method via reflection 172 | $reflection = new ReflectionClass($provider); 173 | $method = $reflection->getMethod('getEntityManager'); 174 | $method->setAccessible(true); 175 | $method->invoke($provider); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /tests/Integration/AclServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | createMock(PermissionManager::class); 24 | $manager->method('getPermissionsWithDotNotation')->willReturn(['foo.bar', 'baz.qux']); 25 | $this->app->instance(PermissionManager::class, $manager); 26 | 27 | // Get the Gate 28 | $gate = $this->app->make(Gate::class); 29 | 30 | // Assert: Gate has the permissions defined 31 | $this->assertTrue($gate->has('foo.bar')); 32 | $this->assertTrue($gate->has('baz.qux')); 33 | 34 | $user = entity(User::class)->create(); 35 | $user->setPermissions(['foo.bar']); 36 | $this->actingAs($user); 37 | 38 | $this->assertTrue($gate->allows('foo.bar')); 39 | $this->assertFalse($gate->allows('baz.quxdkdkd')); 40 | } 41 | 42 | public function testNoPermissionsDefinedWhenManagerReturnsEmpty(): void 43 | { 44 | // $manager = $this->createMock(PermissionManager::class); 45 | // $manager->method('getPermissionsWithDotNotation')->willReturn([]); 46 | // $this->app->instance(PermissionManager::class, $manager); 47 | 48 | $gate = $this->app->make(Gate::class); 49 | 50 | $this->assertFalse($gate->has('any.permission')); 51 | } 52 | 53 | public function testRegisterPathsSkipsWhenNotUsingDefaultPermissionEntity(): void 54 | { 55 | // Arrange: Mock PermissionManager 56 | $manager = Mockery::mock(PermissionManager::class); 57 | $manager->shouldReceive('useDefaultPermissionEntity')->once()->andReturn(false); 58 | 59 | // We expect that DoctrineManager::addPaths should NOT be called 60 | $doctrineManager = Mockery::mock(DoctrineManager::class); 61 | $doctrineManager->shouldNotReceive('addPaths'); 62 | 63 | $this->app->instance(PermissionManager::class, $manager); 64 | $this->app->instance(DoctrineManager::class, $doctrineManager); 65 | 66 | // Act: Call registerPaths via reflection 67 | $provider = $this->app->getProvider(AclServiceProvider::class); 68 | $reflection = new ReflectionClass($provider); 69 | $method = $reflection->getMethod('registerPaths'); 70 | $method->setAccessible(true); 71 | 72 | $this->assertNull($method->invoke($provider)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Integration/ConfigPermissionDriverTest.php: -------------------------------------------------------------------------------- 1 | set('acl.permissions.driver', 'config'); 25 | } 26 | 27 | public function testPermissionsAreLoadedFromConfig(): void 28 | { 29 | $em = $this->app->make(EntityManager::class); 30 | 31 | $user = entity(UserJsonPermissions::class)->create(); 32 | $user->setPermissions(['role.attach']); 33 | 34 | $em->persist($user); 35 | $em->flush(); 36 | 37 | $this->actingAs($user); 38 | 39 | $gate = $this->app->make(Gate::class); 40 | 41 | $this->assertTrue($gate->allows('role.attach')); 42 | $this->assertFalse($gate->allows('no_permission')); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Integration/DoctrinePermissionPersistenceTest.php: -------------------------------------------------------------------------------- 1 | getRepository(User::class); 27 | 28 | // Create user, role, organisation, and permission 29 | $user = entity(User::class)->create(); 30 | $role = new Role('test-role'); 31 | $organisation = new Organisation('test-org'); 32 | $permission = new Permission('persisted.permission'); 33 | $permission->setName('persisted.permission'); // Just for test coverage 34 | $role->getPermissions()->add($permission); 35 | $user->getRoles()->add($role); 36 | $user->getOrganisations()->add($organisation); 37 | $user->getPermissions()->add($permission); 38 | 39 | // Persist all entities 40 | $em->persist($permission); 41 | $em->persist($role); 42 | $em->persist($organisation); 43 | $em->persist($user); 44 | $em->flush(); 45 | $em->clear(); 46 | 47 | $reloaded = $repo->findOneBy(['email' => $user->email]); 48 | assert($reloaded instanceof User); 49 | $this->assertNotNull($reloaded); 50 | $this->assertInstanceOf(User::class, $reloaded); 51 | 52 | // Permissions 53 | $permissions = $reloaded->getPermissions(); 54 | $this->assertInstanceOf(Collection::class, $permissions); 55 | $this->assertTrue($permissions->exists(static fn ($key, $perm) => $perm->getName() === 'persisted.permission')); 56 | 57 | $reloadedPermission = $permissions->filter(static fn ($perm) => $perm->getName() === 'persisted.permission')->first(); 58 | assert($reloadedPermission instanceof Permission); 59 | $this->assertNotNull($reloadedPermission); 60 | $this->assertInstanceOf(Permission::class, $reloadedPermission); 61 | $this->assertIsNumeric($reloadedPermission->getId()); 62 | 63 | // Roles 64 | $roles = $reloaded->getRoles(); 65 | $this->assertInstanceOf(Collection::class, $roles); 66 | $this->assertTrue($roles->exists(static fn ($key, $role) => $role->getName() === 'test-role')); 67 | $reloadedRole = $roles->filter(static fn ($role) => $role->getName() === 'test-role')->first(); 68 | $this->assertNotNull($reloadedRole); 69 | $this->assertTrue($reloadedRole->getPermissions()->exists(static fn ($key, $perm) => $perm->getName() === 'persisted.permission')); 70 | 71 | // Organisations 72 | $organisations = $reloaded->getOrganisations(); 73 | $this->assertInstanceOf(Collection::class, $organisations); 74 | $this->assertTrue($organisations->exists(static fn ($key, $org) => $org->getName() === 'test-org')); 75 | } 76 | 77 | public function testUserSingleOrgPermissionsArrayPersistence(): void 78 | { 79 | $em = app(EntityManagerInterface::class); 80 | assert($em instanceof EntityManagerInterface); 81 | $repo = $em->getRepository(UserSingleOrg::class); 82 | 83 | // Create user and permission 84 | $user = entity(UserSingleOrg::class)->create(); 85 | $user->setPermissions(['array.permission']); 86 | 87 | // Persist entities 88 | $em->persist($user); 89 | $em->flush(); 90 | $em->clear(); 91 | 92 | $reloaded = $repo->findOneBy(['email' => $user->email]); 93 | assert($reloaded instanceof UserSingleOrg); 94 | $this->assertNotNull($reloaded); 95 | $this->assertInstanceOf(UserSingleOrg::class, $reloaded); 96 | 97 | // Permissions should be loaded as an array 98 | $permissions = $reloaded->getPermissions(); 99 | $this->assertIsArray($permissions); 100 | $this->assertNotEmpty($permissions); 101 | $this->assertEquals(['array.permission'], $permissions); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/LaravelSetupTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($this->app); 17 | $this->assertInstanceOf(Application::class, $this->app); 18 | $this->assertEquals('testing', $this->app->environment()); 19 | 20 | $user = entity(User::class)->create(); 21 | 22 | $this->actingAs($user); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Organisations/BelongsToOrganisationTest.php: -------------------------------------------------------------------------------- 1 | user = entity(User::class)->create(); 28 | $this->userSingle = entity(UserSingleOrg::class)->create(); 29 | $this->orgMock1 = entity(Organisation::class)->create(['name' => 'org1']); 30 | $this->orgMock2 = entity(Organisation::class)->create(['name' => 'org2']); 31 | $this->orgMock3 = entity(Organisation::class)->create(['name' => 'org3']); 32 | } 33 | 34 | public function testBelongsToOrganisationVariousCases(): void 35 | { 36 | // No organisations assigned (single org user) 37 | $this->assertFalse($this->userSingle->belongsToOrganisation($this->orgMock1)); 38 | 39 | // Assign an organisation to userSingle and check positive/negative cases 40 | $this->userSingle->setOrganisation($this->orgMock1); 41 | $this->assertTrue($this->userSingle->belongsToOrganisation($this->orgMock1)); 42 | $this->assertTrue($this->userSingle->belongsToOrganisation('org1')); 43 | $this->assertFalse($this->userSingle->belongsToOrganisation($this->orgMock2)); 44 | $this->assertFalse($this->userSingle->belongsToOrganisation('org2')); 45 | 46 | // No organisations assigned (multi org user) 47 | $this->assertFalse($this->user->belongsToOrganisation($this->orgMock1)); 48 | $this->assertFalse($this->user->belongsToOrganisation('org1')); 49 | 50 | // Other organisation assigned 51 | $this->user->setOrganisations([entity(Organisation::class)->create(['name' => 'org4'])]); 52 | $this->assertFalse($this->user->belongsToOrganisation($this->orgMock1)); 53 | 54 | // Organisation assigned, check any/all/none by object and name 55 | $this->user->setOrganisations([$this->orgMock1]); 56 | $this->assertFalse($this->user->belongsToOrganisation([$this->orgMock2, $this->orgMock3])); 57 | $this->assertFalse($this->user->belongsToOrganisation(['org2', 'org3'])); 58 | $this->assertTrue($this->user->belongsToOrganisation($this->orgMock1)); 59 | $this->assertTrue($this->user->belongsToOrganisation('org1')); 60 | 61 | // Two organisations assigned 62 | $this->user->setOrganisations([$this->orgMock1, $this->orgMock2]); 63 | $this->assertFalse($this->user->belongsToOrganisation([$this->orgMock1, $this->orgMock2, $this->orgMock3], true)); 64 | $this->assertFalse($this->user->belongsToOrganisation(['org1', 'org2', 'org3'], true)); 65 | 66 | // Three organisations assigned 67 | $this->user->setOrganisations([$this->orgMock1, $this->orgMock2, $this->orgMock3]); 68 | $this->assertTrue($this->user->belongsToOrganisation([$this->orgMock1, $this->orgMock2])); 69 | $this->assertTrue($this->user->belongsToOrganisation([$this->orgMock1, $this->orgMock2, $this->orgMock3], true)); 70 | $this->assertTrue($this->user->belongsToOrganisation(['org1', 'org4'])); 71 | $this->assertTrue($this->user->belongsToOrganisation(['org1', 'org2', 'org3'], true)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Permissions/Driver/ConfigPermissionDriverTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($emptyConfig->getAllPermissions()->isEmpty()); 16 | 17 | $config = new Config(['mocked']); 18 | $this->assertTrue($config->getAllPermissions()->contains('mocked')); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Permissions/Driver/DoctrinePermissionDriverTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('findAll')->andReturn([ 19 | new Permission('mocked'), 20 | ]); 21 | 22 | $driver = new Doctrine($repository); 23 | 24 | $permissions = $driver->getAllPermissions(); 25 | $this->assertTrue($permissions->contains('mocked')); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Permissions/HasPermissionsTest.php: -------------------------------------------------------------------------------- 1 | user = entity(User::class)->create(); 23 | } 24 | 25 | public function testDoesntHavePermissionWhenNoRolesAndNoPermissions(): void 26 | { 27 | $this->assertFalse($this->user->hasPermissionTo('create.post')); 28 | } 29 | 30 | public function testDoesntHavePermissionWhenNoRolesWithOtherPermissions(): void 31 | { 32 | $this->user->setPermissions(['create.page']); 33 | 34 | $this->assertFalse($this->user->hasPermissionTo('create.post')); 35 | } 36 | 37 | public function testDoesntHavePermissionWithRolesAndOtherPermissions(): void 38 | { 39 | $this->user->setRoles([ 40 | entity(Role::class)->make(), 41 | ]); 42 | 43 | $this->user->setPermissions(['create.page']); 44 | 45 | $this->assertFalse($this->user->hasPermissionTo('create.post')); 46 | } 47 | 48 | public function testDoesntHavePermissionWithRolesWithOtherPermissionsAndOtherPermissions(): void 49 | { 50 | $role = entity(Role::class)->make(); 51 | $role->setPermissions(['create.page']); 52 | 53 | $this->user->setRoles([$role]); 54 | 55 | $this->user->setPermissions(['create.page']); 56 | 57 | $this->assertFalse($this->user->hasPermissionTo('create.post')); 58 | } 59 | 60 | public function testDoesntHavePermissionWithPermissionButNoOtherPermissions(): void 61 | { 62 | $this->user->setPermissions(['create.page']); 63 | 64 | $this->assertFalse($this->user->hasPermissionTo(['create.post', 'create.comment'])); 65 | } 66 | 67 | public function testDoesntHavePermissionWithPermissionButNotAllOtherPermissions(): void 68 | { 69 | $this->user->setPermissions([ 70 | 'create.page', 71 | 'create.post', 72 | ]); 73 | 74 | $this->assertFalse($this->user->hasPermissionTo(['create.post', 'create.page', 'create.comment'], true)); 75 | } 76 | 77 | public function testUserHasPermissionWhenNoRolesButHasThePermission(): void 78 | { 79 | $this->user->setPermissions(['create.post']); 80 | 81 | $this->assertTrue($this->user->hasPermissionTo('create.post')); 82 | } 83 | 84 | public function testUserHasPermissionWhenWithRolesButHasThePermission(): void 85 | { 86 | $this->user->setRoles([ 87 | entity(Role::class)->make(), 88 | ]); 89 | 90 | $this->user->setPermissions(['create.post']); 91 | 92 | $this->assertTrue($this->user->hasPermissionTo('create.post')); 93 | } 94 | 95 | public function testUserHasPermissionWhenRoleHasPermission(): void 96 | { 97 | $role = entity(Role::class)->make(); 98 | $role->setPermissions(['create.post']); 99 | 100 | $this->user->setRoles([$role]); 101 | 102 | $this->assertTrue($this->user->hasPermissionTo('create.post')); 103 | } 104 | 105 | public function testUserHasPermissionWhenOneRoleHasPermission(): void 106 | { 107 | $role = entity(Role::class)->make(); 108 | $role->setPermissions(['create.post']); 109 | 110 | $this->user->setRoles([ 111 | entity(Role::class)->make(), 112 | $role, 113 | ]); 114 | 115 | $this->assertTrue($this->user->hasPermissionTo('create.post')); 116 | } 117 | 118 | public function testCanCheckIfHasPermissionWithPermissionObjects(): void 119 | { 120 | $this->user->setPermissions([ 121 | new Permission('create.post'), 122 | ]); 123 | 124 | $this->assertTrue($this->user->hasPermissionTo('create.post')); 125 | } 126 | 127 | public function testUserHasPermissionWhenRoleHasPermissionWithObject(): void 128 | { 129 | $role = entity(Role::class)->create(); 130 | $role->setPermissions([ 131 | new Permission('create.post'), 132 | ]); 133 | 134 | $this->user->setRoles([$role]); 135 | 136 | $this->assertTrue($this->user->hasPermissionTo('create.post')); 137 | } 138 | 139 | public function testHasPermissionWithPermissionButNotAllOtherPermissions(): void 140 | { 141 | $this->user->setPermissions(['create.page']); 142 | 143 | $this->assertTrue($this->user->hasPermissionTo(['create.post', 'create.page', 'create.comment'])); 144 | } 145 | 146 | public function testHasPermissionAndAllPermissions(): void 147 | { 148 | $this->user->setPermissions([ 149 | 'create.page', 150 | 'create.post', 151 | ]); 152 | 153 | $this->assertTrue($this->user->hasPermissionTo(['create.post', 'create.page'], true)); 154 | } 155 | 156 | public function testUserHasPermissionByObject(): void 157 | { 158 | $this->user->setPermissions(['test.test']); 159 | 160 | $this->assertTrue($this->user->hasPermissionTo(new Permission('test.test'))); 161 | } 162 | 163 | public function testUserHasObjectPermissionByObject(): void 164 | { 165 | $this->user->setPermissions([new Permission('test.test')]); 166 | 167 | $this->assertTrue($this->user->hasPermissionTo(new Permission('test.test'))); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tests/Roles/HasRolesTest.php: -------------------------------------------------------------------------------- 1 | user = entity(User::class)->create(); 25 | $this->admin = entity(Role::class, 'admin')->create(); 26 | $this->extraRole1 = entity(Role::class)->create(['name' => 'extraRole1']); 27 | $this->extraRole2 = entity(Role::class)->create(['name' => 'extraRole2']); 28 | } 29 | 30 | public function testRolesAndRoleNamesCombinations(): void 31 | { 32 | // Initial state: user has no roles 33 | $this->assertFalse($this->user->hasRole($this->admin)); 34 | $this->assertFalse($this->user->hasRoleByName('admin')); 35 | 36 | // User has a different role (not admin) 37 | $this->user->setRoles([entity(Role::class, 'user')->create()]); 38 | $this->assertFalse($this->user->hasRole($this->admin)); 39 | $this->assertFalse($this->user->hasRoleByName('admin')); 40 | 41 | // User has only admin 42 | $this->user->setRoles([$this->admin]); 43 | $this->assertFalse($this->user->hasRole([$this->extraRole1, $this->extraRole2])); 44 | $this->assertFalse($this->user->hasRoleByName(['extraRole1', 'extraRole2'])); 45 | $this->assertTrue($this->user->hasRole($this->admin)); 46 | $this->assertTrue($this->user->hasRoleByName('admin')); 47 | 48 | // User has admin and extraRole1 49 | $this->user->setRoles([ 50 | $this->admin, 51 | $this->extraRole1, 52 | ]); 53 | $this->assertFalse($this->user->hasRole([ 54 | $this->admin, 55 | $this->extraRole1, 56 | $this->extraRole2, 57 | ], true)); 58 | $this->assertFalse($this->user->hasRoleByName([ 59 | 'admin', 60 | 'extraRole1', 61 | 'extraRole2', 62 | ], true)); 63 | 64 | // User has admin, extraRole1, extraRole2 65 | $this->user->setRoles([ 66 | $this->admin, 67 | $this->extraRole1, 68 | $this->extraRole2, 69 | ]); 70 | $this->assertTrue($this->user->hasRole([ 71 | $this->admin, 72 | $this->extraRole1, 73 | ])); 74 | $this->assertTrue($this->user->hasRole([ 75 | $this->admin, 76 | $this->extraRole1, 77 | $this->extraRole2, 78 | ], true)); 79 | $this->assertTrue($this->user->hasRoleByName([ 80 | 'admin', 81 | 'extraRole1', 82 | ])); 83 | $this->assertTrue($this->user->hasRoleByName([ 84 | 'admin', 85 | 'extraRole1', 86 | 'extraRole2', 87 | ], true)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | artisan('doctrine:schema:create'); 19 | } 20 | 21 | public function tearDown(): void 22 | { 23 | parent::tearDown(); 24 | } 25 | 26 | protected function em(): EntityManager 27 | { 28 | return $this->app->make(EntityManager::class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /workbench/app/Entities/Organisation.php: -------------------------------------------------------------------------------- 1 | name = $name; 24 | } 25 | 26 | public function getId(): int|null 27 | { 28 | return $this->id; 29 | } 30 | 31 | public function getName(): string 32 | { 33 | return $this->name; 34 | } 35 | 36 | public function setName(string $name): self 37 | { 38 | $this->name = $name; 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /workbench/app/Entities/Role.php: -------------------------------------------------------------------------------- 1 | */ 32 | #[ACL\HasPermissions] 33 | public Collection $permissions; 34 | 35 | public function __construct(string $name) 36 | { 37 | $this->name = $name; 38 | $this->permissions = new ArrayCollection(); 39 | } 40 | 41 | public function getName(): string 42 | { 43 | return $this->name; 44 | } 45 | 46 | public function getId(): int|null 47 | { 48 | return $this->id; 49 | } 50 | 51 | /** @return Collection */ 52 | public function getPermissions(): Collection 53 | { 54 | return $this->permissions; 55 | } 56 | 57 | /** @param Collection|Permission[] $permissions */ 58 | public function setPermissions(Collection|array $permissions): self 59 | { 60 | $this->permissions = is_array($permissions) ? new ArrayCollection($permissions) : $permissions; 61 | 62 | return $this; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /workbench/app/Entities/User.php: -------------------------------------------------------------------------------- 1 | */ 51 | #[ACL\HasRoles] 52 | public Collection $roles; 53 | 54 | /** @var Collection */ 55 | #[ACL\HasPermissions] 56 | public Collection $permissions; 57 | 58 | /** @var Collection */ 59 | #[ACL\BelongsToOrganisations] 60 | public Collection $organisations; 61 | 62 | public function __construct() 63 | { 64 | $this->roles = new ArrayCollection(); 65 | $this->permissions = new ArrayCollection(); 66 | $this->organisations = new ArrayCollection(); 67 | } 68 | 69 | /** @return Collection */ 70 | public function getRoles(): Collection 71 | { 72 | return $this->roles; 73 | } 74 | 75 | /** @param Collection|Role[] $roles */ 76 | public function setRoles(Collection|array $roles): self 77 | { 78 | $this->roles = is_array($roles) ? new ArrayCollection($roles) : $roles; 79 | 80 | return $this; 81 | } 82 | 83 | /** @return Collection */ 84 | public function getPermissions(): Collection 85 | { 86 | return $this->permissions; 87 | } 88 | 89 | /** @param Collection|Permission[] $permissions */ 90 | public function setPermissions(Collection|array $permissions): self 91 | { 92 | $this->permissions = is_array($permissions) ? new ArrayCollection($permissions) : $permissions; 93 | 94 | return $this; 95 | } 96 | 97 | /** @return Collection */ 98 | public function getOrganisations(): Collection 99 | { 100 | return $this->organisations; 101 | } 102 | 103 | /** @param Collection|Organisation[] $organisations */ 104 | public function setOrganisations(Collection|array $organisations): self 105 | { 106 | $this->organisations = is_array($organisations) ? new ArrayCollection($organisations) : $organisations; 107 | 108 | return $this; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /workbench/app/Entities/UserJsonPermissions.php: -------------------------------------------------------------------------------- 1 | */ 41 | #[ACL\HasPermissions(inversedBy: 'users')] 42 | public array $permissions = []; 43 | 44 | /** @return array */ 45 | public function getPermissions(): array 46 | { 47 | return $this->permissions; 48 | } 49 | 50 | /** @param array $permissions */ 51 | public function setPermissions(array $permissions): self 52 | { 53 | $this->permissions = $permissions; 54 | 55 | return $this; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /workbench/app/Entities/UserSingleOrg.php: -------------------------------------------------------------------------------- 1 | */ 51 | #[ACL\HasRoles()] 52 | public Collection $roles; 53 | 54 | /** @var array */ 55 | #[ORM\Column(type: 'json')] 56 | public array $permissions = []; 57 | 58 | #[ACL\BelongsToOrganisation()] 59 | public Organisation|null $organisation = null; 60 | 61 | public function __construct() 62 | { 63 | $this->roles = new ArrayCollection(); 64 | } 65 | 66 | public function getId(): int|null 67 | { 68 | return $this->id; 69 | } 70 | 71 | /** @return Collection */ 72 | public function getRoles(): Collection 73 | { 74 | return $this->roles; 75 | } 76 | 77 | /** @param Collection|Role[] $roles */ 78 | public function setRoles(Collection|array $roles): self 79 | { 80 | $this->roles = is_array($roles) ? new ArrayCollection($roles) : $roles; 81 | 82 | return $this; 83 | } 84 | 85 | /** @return array */ 86 | public function getPermissions(): array 87 | { 88 | return $this->permissions; 89 | } 90 | 91 | /** @param array $permissions */ 92 | public function setPermissions(array $permissions): self 93 | { 94 | $this->permissions = $permissions; 95 | 96 | return $this; 97 | } 98 | 99 | public function getOrganisation(): Organisation|null 100 | { 101 | return $this->organisation; 102 | } 103 | 104 | public function setOrganisation(Organisation|null $organisation): self 105 | { 106 | $this->organisation = $organisation; 107 | 108 | return $this; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /workbench/app/Providers/WorkbenchServiceProvider.php: -------------------------------------------------------------------------------- 1 | withRouting( 14 | web: __DIR__ . '/../routes/web.php', 15 | commands: __DIR__ . '/../routes/console.php', 16 | ) 17 | ->withMiddleware(static function (Middleware $middleware): void { 18 | }) 19 | ->withExceptions(static function (Exceptions $exceptions): void { 20 | })->create(); 21 | -------------------------------------------------------------------------------- /workbench/bootstrap/cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-doctrine/acl/bbf7cb96bd7bc11186827b5402b886f36ffab2da/workbench/bootstrap/cache/.gitkeep -------------------------------------------------------------------------------- /workbench/bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'driver' => 'doctrine', 22 | 'entity' => Permission::class, 23 | 'list' => [ 24 | 'role.attach', 25 | 'role.detach', 26 | ], 27 | ], 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Roles 32 | |-------------------------------------------------------------------------- 33 | */ 34 | 'roles' => [ 35 | 'entity' => Role::class, 36 | ], 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Organisations 41 | |-------------------------------------------------------------------------- 42 | */ 43 | 'organisations' => [ 44 | 'entity' => Organisation::class, 45 | ], 46 | ]; 47 | -------------------------------------------------------------------------------- /workbench/config/auth.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'guard' => env('AUTH_GUARD', 'web'), 22 | 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), 23 | ], 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Authentication Guards 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Next, you may define every authentication guard for your application. 31 | | Of course, a great default configuration has been defined for you 32 | | which utilizes session storage plus the Eloquent user provider. 33 | | 34 | | All authentication guards have a user provider, which defines how the 35 | | users are actually retrieved out of your database or other storage 36 | | system used by the application. Typically, Eloquent is utilized. 37 | | 38 | | Supported: "session" 39 | | 40 | */ 41 | 42 | 'guards' => [ 43 | 'web' => [ 44 | 'driver' => 'session', 45 | 'provider' => 'users', 46 | ], 47 | ], 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | User Providers 52 | |-------------------------------------------------------------------------- 53 | | 54 | | All authentication guards have a user provider, which defines how the 55 | | users are actually retrieved out of your database or other storage 56 | | system used by the application. Typically, Eloquent is utilized. 57 | | 58 | | If you have multiple user tables or models you may configure multiple 59 | | providers to represent the model / table. These providers may then 60 | | be assigned to any extra authentication guards you have defined. 61 | | 62 | | Supported: "database", "eloquent" 63 | | 64 | */ 65 | 66 | 'providers' => [ 67 | 'users' => [ 68 | 'driver' => 'doctrine', 69 | 'model' => env('AUTH_MODEL', User::class), 70 | ], 71 | 72 | // 'users' => [ 73 | // 'driver' => 'database', 74 | // 'table' => 'users', 75 | // ], 76 | ], 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Resetting Passwords 81 | |-------------------------------------------------------------------------- 82 | | 83 | | These configuration options specify the behavior of Laravel's password 84 | | reset functionality, including the table utilized for token storage 85 | | and the user provider that is invoked to actually retrieve users. 86 | | 87 | | The expiry time is the number of minutes that each reset token will be 88 | | considered valid. This security feature keeps tokens short-lived so 89 | | they have less time to be guessed. You may change this as needed. 90 | | 91 | | The throttle setting is the number of seconds a user must wait before 92 | | generating more password reset tokens. This prevents the user from 93 | | quickly generating a very large amount of password reset tokens. 94 | | 95 | */ 96 | 97 | 'passwords' => [ 98 | 'users' => [ 99 | 'provider' => 'users', 100 | 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 101 | 'expire' => 60, 102 | 'throttle' => 60, 103 | ], 104 | ], 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Password Confirmation Timeout 109 | |-------------------------------------------------------------------------- 110 | | 111 | | Here you may define the amount of seconds before a password confirmation 112 | | window expires and users are asked to re-enter their password via the 113 | | confirmation screen. By default, the timeout lasts for three hours. 114 | | 115 | */ 116 | 117 | 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), 118 | 119 | ]; 120 | -------------------------------------------------------------------------------- /workbench/config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'sqlite'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Database Connections 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Below are all of the database connections defined for your application. 29 | | An example configuration is provided for each database system which 30 | | is supported by Laravel. You're free to add / remove connections. 31 | | 32 | */ 33 | 34 | 'connections' => [ 35 | 36 | 'sqlite' => [ 37 | 'driver' => 'sqlite', 38 | 'url' => env('DB_URL'), 39 | 'database' => env('DB_DATABASE', ':memory:'), 40 | 'prefix' => '', 41 | 'prefix_indexes' => null, 42 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 43 | 'busy_timeout' => null, 44 | 'journal_mode' => null, 45 | 'synchronous' => null, 46 | ], 47 | 48 | 'mysql' => [ 49 | 'driver' => 'mysql', 50 | 'url' => env('DB_URL'), 51 | 'host' => env('DB_HOST', '127.0.0.1'), 52 | 'port' => env('DB_PORT', '3306'), 53 | 'database' => env('DB_DATABASE', 'laravel'), 54 | 'username' => env('DB_USERNAME', 'root'), 55 | 'password' => env('DB_PASSWORD', ''), 56 | 'unix_socket' => env('DB_SOCKET', ''), 57 | 'charset' => env('DB_CHARSET', 'utf8mb4'), 58 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 59 | 'prefix' => '', 60 | 'prefix_indexes' => true, 61 | 'strict' => true, 62 | 'engine' => null, 63 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 64 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 65 | ]) : [], 66 | ], 67 | 68 | 'mariadb' => [ 69 | 'driver' => 'mariadb', 70 | 'url' => env('DB_URL'), 71 | 'host' => env('DB_HOST', '127.0.0.1'), 72 | 'port' => env('DB_PORT', '3306'), 73 | 'database' => env('DB_DATABASE', 'laravel'), 74 | 'username' => env('DB_USERNAME', 'root'), 75 | 'password' => env('DB_PASSWORD', ''), 76 | 'unix_socket' => env('DB_SOCKET', ''), 77 | 'charset' => env('DB_CHARSET', 'utf8mb4'), 78 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 79 | 'prefix' => '', 80 | 'prefix_indexes' => true, 81 | 'strict' => true, 82 | 'engine' => null, 83 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 84 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 85 | ]) : [], 86 | ], 87 | 88 | 'pgsql' => [ 89 | 'driver' => 'pgsql', 90 | 'url' => env('DB_URL'), 91 | 'host' => env('DB_HOST', '127.0.0.1'), 92 | 'port' => env('DB_PORT', '5432'), 93 | 'database' => env('DB_DATABASE', 'laravel'), 94 | 'username' => env('DB_USERNAME', 'root'), 95 | 'password' => env('DB_PASSWORD', ''), 96 | 'charset' => env('DB_CHARSET', 'utf8'), 97 | 'prefix' => '', 98 | 'prefix_indexes' => true, 99 | 'search_path' => 'public', 100 | 'sslmode' => 'prefer', 101 | ], 102 | 103 | 'sqlsrv' => [ 104 | 'driver' => 'sqlsrv', 105 | 'url' => env('DB_URL'), 106 | 'host' => env('DB_HOST', 'localhost'), 107 | 'port' => env('DB_PORT', '1433'), 108 | 'database' => env('DB_DATABASE', 'laravel'), 109 | 'username' => env('DB_USERNAME', 'root'), 110 | 'password' => env('DB_PASSWORD', ''), 111 | 'charset' => env('DB_CHARSET', 'utf8'), 112 | 'prefix' => '', 113 | 'prefix_indexes' => true, 114 | // 'encrypt' => env('DB_ENCRYPT', 'yes'), 115 | // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), 116 | ], 117 | 118 | ], 119 | 120 | /* 121 | |-------------------------------------------------------------------------- 122 | | Migration Repository Table 123 | |-------------------------------------------------------------------------- 124 | | 125 | | This table keeps track of all the migrations that have already run for 126 | | your application. Using this information, we can determine which of 127 | | the migrations on disk haven't actually been run on the database. 128 | | 129 | */ 130 | 131 | 'migrations' => [ 132 | 'table' => 'migrations', 133 | 'update_date_on_publish' => true, 134 | ], 135 | 136 | /* 137 | |-------------------------------------------------------------------------- 138 | | Redis Databases 139 | |-------------------------------------------------------------------------- 140 | | 141 | | Redis is an open source, fast, and advanced key-value store that also 142 | | provides a richer body of commands than a typical key-value system 143 | | such as Memcached. You may define your connection settings here. 144 | | 145 | */ 146 | 147 | 'redis' => [ 148 | 149 | 'client' => env('REDIS_CLIENT', 'phpredis'), 150 | 151 | 'options' => [ 152 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 153 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'), 154 | 'persistent' => env('REDIS_PERSISTENT', false), 155 | ], 156 | 157 | 'default' => [ 158 | 'url' => env('REDIS_URL'), 159 | 'host' => env('REDIS_HOST', '127.0.0.1'), 160 | 'username' => env('REDIS_USERNAME'), 161 | 'password' => env('REDIS_PASSWORD'), 162 | 'port' => env('REDIS_PORT', '6379'), 163 | 'database' => env('REDIS_DB', '0'), 164 | ], 165 | 166 | 'cache' => [ 167 | 'url' => env('REDIS_URL'), 168 | 'host' => env('REDIS_HOST', '127.0.0.1'), 169 | 'username' => env('REDIS_USERNAME'), 170 | 'password' => env('REDIS_PASSWORD'), 171 | 'port' => env('REDIS_PORT', '6379'), 172 | 'database' => env('REDIS_CACHE_DB', '1'), 173 | ], 174 | 175 | ], 176 | 177 | ]; 178 | -------------------------------------------------------------------------------- /workbench/config/doctrine.php: -------------------------------------------------------------------------------- 1 | Warning: Proxy auto generation should only be enabled in dev! 29 | | 30 | */ 31 | 'managers' => [ 32 | 'default' => [ 33 | 'dev' => env('APP_DEBUG', false), 34 | 'meta' => env('DOCTRINE_METADATA', 'attributes'), 35 | 'connection' => env('DB_CONNECTION', 'sqlite'), 36 | 'paths' => [app_path('Entities')], 37 | 38 | 'repository' => EntityRepository::class, 39 | 40 | 'proxies' => [ 41 | 'namespace' => 'DoctrineProxies', 42 | 'path' => storage_path('proxies'), 43 | 'auto_generate' => env('DOCTRINE_PROXY_AUTOGENERATE', false), 44 | ], 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Doctrine events 49 | |-------------------------------------------------------------------------- 50 | | 51 | | The listener array expects the key to be a Doctrine event 52 | | e.g. Doctrine\ORM\Events::onFlush 53 | | 54 | */ 55 | 'events' => [ 56 | 'listeners' => [], 57 | 'subscribers' => [], 58 | ], 59 | 60 | 'filters' => [], 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Doctrine mapping types 65 | |-------------------------------------------------------------------------- 66 | | 67 | | Link a Database Type to a Local Doctrine Type 68 | | 69 | | Using 'enum' => 'string' is the same of: 70 | | $doctrineManager->extendAll(function (\Doctrine\ORM\Configuration $configuration, 71 | | \Doctrine\DBAL\Connection $connection, 72 | | \Doctrine\Common\EventManager $eventManager) { 73 | | $connection->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); 74 | | }); 75 | | 76 | | References: 77 | | https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/custom-mapping-types.html 78 | | https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#custom-mapping-types 79 | | https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/advanced-field-value-conversion-using-custom-mapping-types.html 80 | | https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/basic-mapping.html 81 | | https://symfony.com/doc/current/doctrine/dbal.html#registering-custom-mapping-types-in-the-schematool 82 | |-------------------------------------------------------------------------- 83 | */ 84 | 'mapping_types' => [], 85 | 86 | /** 87 | * References: 88 | * https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/architecture.html#middlewares 89 | */ 90 | 'middlewares' => [], 91 | ], 92 | ], 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Doctrine Extensions 96 | |-------------------------------------------------------------------------- 97 | | 98 | | Enable/disable Doctrine Extensions by adding or removing them from the list 99 | | 100 | | If you want to require custom extensions you will have to require 101 | | laravel-doctrine/extensions in your composer.json 102 | | 103 | */ 104 | 'extensions' => [], 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Doctrine custom types 108 | |-------------------------------------------------------------------------- 109 | | 110 | | Create a custom or override a Doctrine Type 111 | |-------------------------------------------------------------------------- 112 | */ 113 | 'custom_types' => [], 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | DQL custom datetime functions 117 | |-------------------------------------------------------------------------- 118 | */ 119 | 'custom_datetime_functions' => [], 120 | /* 121 | |-------------------------------------------------------------------------- 122 | | DQL custom numeric functions 123 | |-------------------------------------------------------------------------- 124 | */ 125 | 'custom_numeric_functions' => [], 126 | /* 127 | |-------------------------------------------------------------------------- 128 | | DQL custom string functions 129 | |-------------------------------------------------------------------------- 130 | */ 131 | 'custom_string_functions' => [], 132 | /* 133 | |-------------------------------------------------------------------------- 134 | | Register custom hydrators 135 | |-------------------------------------------------------------------------- 136 | */ 137 | 'custom_hydration_modes' => [], 138 | /* 139 | |-------------------------------------------------------------------------- 140 | | Cache 141 | |-------------------------------------------------------------------------- 142 | | 143 | | Configure meta-data, query and result caching here. 144 | | Optionally you can enable second level caching. 145 | | 146 | | Available: apc|array|file|illuminate|memcached|php_file|redis 147 | | 148 | */ 149 | 'cache' => [ 150 | 'second_level' => false, 151 | 'default' => env('DOCTRINE_CACHE', 'array'), 152 | 'namespace' => null, 153 | 'metadata' => [ 154 | 'driver' => env('DOCTRINE_METADATA_CACHE', env('DOCTRINE_CACHE', 'array')), 155 | 'namespace' => 'metadata', 156 | ], 157 | 'query' => [ 158 | 'driver' => env('DOCTRINE_QUERY_CACHE', env('DOCTRINE_CACHE', 'array')), 159 | 'namespace' => 'query', 160 | ], 161 | 'result' => [ 162 | 'driver' => env('DOCTRINE_RESULT_CACHE', env('DOCTRINE_CACHE', 'array')), 163 | 'namespace' => 'result', 164 | ], 165 | ], 166 | /* 167 | |-------------------------------------------------------------------------- 168 | | Gedmo extensions 169 | |-------------------------------------------------------------------------- 170 | | 171 | | Settings for Gedmo extensions 172 | | If you want to use this you will have to require 173 | | laravel-doctrine/extensions in your composer.json 174 | | 175 | */ 176 | 'gedmo' => ['all_mappings' => false], 177 | /* 178 | |-------------------------------------------------------------------------- 179 | | Validation 180 | |-------------------------------------------------------------------------- 181 | | 182 | | Enables the Doctrine Presence Verifier for Validation 183 | | 184 | */ 185 | 'doctrine_presence_verifier' => true, 186 | 187 | /* 188 | |-------------------------------------------------------------------------- 189 | | Notifications 190 | |-------------------------------------------------------------------------- 191 | | 192 | | Doctrine notifications channel 193 | | 194 | */ 195 | 'notifications' => ['channel' => 'database'], 196 | ]; 197 | -------------------------------------------------------------------------------- /workbench/config/migrations.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'table_storage' => [ 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Migration Repository Table 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This table keeps track of all the migrations that have already run for 26 | | your application. Using this information, we can determine which of 27 | | the migrations on disk haven't actually been run in the database. 28 | | 29 | */ 30 | 'table_name' => 'migrations', 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Schema filter 35 | |-------------------------------------------------------------------------- 36 | | 37 | | Tables which are filtered by Regular Expression. You optionally 38 | | exclude or limit to certain tables. The default will 39 | | filter all tables. 40 | | 41 | */ 42 | 'schema_filter' => '/^(?!password_resets|failed_jobs).*$/', 43 | ], 44 | 45 | 'migrations_paths' => [ 46 | 'Database\\Migrations' => database_path('migrations'), 47 | ], 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Migration Organize Directory 52 | |-------------------------------------------------------------------------- 53 | | 54 | | Organize migrations file by directory. 55 | | Possible values: "year", "year_and_month" and "none" 56 | | 57 | | none: 58 | | directory/ 59 | | "year": 60 | | directory/2020/ 61 | | "year_and_month": 62 | | directory/2020/01/ 63 | | 64 | */ 65 | 'organize_migrations' => 'none', 66 | ], 67 | ]; 68 | -------------------------------------------------------------------------------- /workbench/config/session.php: -------------------------------------------------------------------------------- 1 | env('SESSION_DRIVER', 'array'), 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Session Lifetime 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Here you may specify the number of minutes that you wish the session 31 | | to be allowed to remain idle before it expires. If you want them 32 | | to expire immediately when the browser is closed then you may 33 | | indicate that via the expire_on_close configuration option. 34 | | 35 | */ 36 | 37 | 'lifetime' => (int) env('SESSION_LIFETIME', 120), 38 | 39 | 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Session Encryption 44 | |-------------------------------------------------------------------------- 45 | | 46 | | This option allows you to easily specify that all of your session data 47 | | should be encrypted before it's stored. All encryption is performed 48 | | automatically by Laravel and you may use the session like normal. 49 | | 50 | */ 51 | 52 | 'encrypt' => env('SESSION_ENCRYPT', false), 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Session File Location 57 | |-------------------------------------------------------------------------- 58 | | 59 | | When utilizing the "file" session driver, the session files are placed 60 | | on disk. The default storage location is defined here; however, you 61 | | are free to provide another location where they should be stored. 62 | | 63 | */ 64 | 65 | 'files' => storage_path('framework/sessions'), 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Session Database Connection 70 | |-------------------------------------------------------------------------- 71 | | 72 | | When using the "database" or "redis" session drivers, you may specify a 73 | | connection that should be used to manage these sessions. This should 74 | | correspond to a connection in your database configuration options. 75 | | 76 | */ 77 | 78 | 'connection' => env('SESSION_CONNECTION'), 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Session Database Table 83 | |-------------------------------------------------------------------------- 84 | | 85 | | When using the "database" session driver, you may specify the table to 86 | | be used to store sessions. Of course, a sensible default is defined 87 | | for you; however, you're welcome to change this to another table. 88 | | 89 | */ 90 | 91 | 'table' => env('SESSION_TABLE', 'sessions'), 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Session Cache Store 96 | |-------------------------------------------------------------------------- 97 | | 98 | | When using one of the framework's cache driven session backends, you may 99 | | define the cache store which should be used to store the session data 100 | | between requests. This must match one of your defined cache stores. 101 | | 102 | | Affects: "apc", "dynamodb", "memcached", "redis" 103 | | 104 | */ 105 | 106 | 'store' => env('SESSION_STORE'), 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Session Sweeping Lottery 111 | |-------------------------------------------------------------------------- 112 | | 113 | | Some session drivers must manually sweep their storage location to get 114 | | rid of old sessions from storage. Here are the chances that it will 115 | | happen on a given request. By default, the odds are 2 out of 100. 116 | | 117 | */ 118 | 119 | 'lottery' => [2, 100], 120 | 121 | /* 122 | |-------------------------------------------------------------------------- 123 | | Session Cookie Name 124 | |-------------------------------------------------------------------------- 125 | | 126 | | Here you may change the name of the session cookie that is created by 127 | | the framework. Typically, you should not need to change this value 128 | | since doing so does not grant a meaningful security improvement. 129 | | 130 | */ 131 | 132 | 'cookie' => env( 133 | 'SESSION_COOKIE', 134 | Str::slug(env('APP_NAME', 'laravel'), '_') . '_session', 135 | ), 136 | 137 | /* 138 | |-------------------------------------------------------------------------- 139 | | Session Cookie Path 140 | |-------------------------------------------------------------------------- 141 | | 142 | | The session cookie path determines the path for which the cookie will 143 | | be regarded as available. Typically, this will be the root path of 144 | | your application, but you're free to change this when necessary. 145 | | 146 | */ 147 | 148 | 'path' => env('SESSION_PATH', '/'), 149 | 150 | /* 151 | |-------------------------------------------------------------------------- 152 | | Session Cookie Domain 153 | |-------------------------------------------------------------------------- 154 | | 155 | | This value determines the domain and subdomains the session cookie is 156 | | available to. By default, the cookie will be available to the root 157 | | domain and all subdomains. Typically, this shouldn't be changed. 158 | | 159 | */ 160 | 161 | 'domain' => env('SESSION_DOMAIN'), 162 | 163 | /* 164 | |-------------------------------------------------------------------------- 165 | | HTTPS Only Cookies 166 | |-------------------------------------------------------------------------- 167 | | 168 | | By setting this option to true, session cookies will only be sent back 169 | | to the server if the browser has a HTTPS connection. This will keep 170 | | the cookie from being sent to you when it can't be done securely. 171 | | 172 | */ 173 | 174 | 'secure' => env('SESSION_SECURE_COOKIE'), 175 | 176 | /* 177 | |-------------------------------------------------------------------------- 178 | | HTTP Access Only 179 | |-------------------------------------------------------------------------- 180 | | 181 | | Setting this value to true will prevent JavaScript from accessing the 182 | | value of the cookie and the cookie will only be accessible through 183 | | the HTTP protocol. It's unlikely you should disable this option. 184 | | 185 | */ 186 | 187 | 'http_only' => env('SESSION_HTTP_ONLY', true), 188 | 189 | /* 190 | |-------------------------------------------------------------------------- 191 | | Same-Site Cookies 192 | |-------------------------------------------------------------------------- 193 | | 194 | | This option determines how your cookies behave when cross-site requests 195 | | take place, and can be used to mitigate CSRF attacks. By default, we 196 | | will set this value to "lax" to permit secure cross-site requests. 197 | | 198 | | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value 199 | | 200 | | Supported: "lax", "strict", "none", null 201 | | 202 | */ 203 | 204 | 'same_site' => env('SESSION_SAME_SITE', 'lax'), 205 | 206 | /* 207 | |-------------------------------------------------------------------------- 208 | | Partitioned Cookies 209 | |-------------------------------------------------------------------------- 210 | | 211 | | Setting this value to true will tie the cookie to the top-level site for 212 | | a cross-site context. Partitioned cookies are accepted by the browser 213 | | when flagged "secure" and the Same-Site attribute is set to "none". 214 | | 215 | */ 216 | 217 | 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), 218 | 219 | ]; 220 | -------------------------------------------------------------------------------- /workbench/config/view.php: -------------------------------------------------------------------------------- 1 | [ 19 | resource_path('views'), 20 | ], 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Compiled View Path 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This option determines where all the compiled Blade templates will be 28 | | stored for your application. Typically, this is within the storage 29 | | directory. However, as usual, you are free to change this value. 30 | | 31 | */ 32 | 33 | 'compiled' => env( 34 | 'VIEW_COMPILED_PATH', 35 | realpath(storage_path('framework/views')), 36 | ), 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /workbench/database/README.md: -------------------------------------------------------------------------------- 1 | # Laravel Doctrine ORM Entity Factories 2 | 3 | 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. 4 | 5 | ## Defining an Entity Factory 6 | 7 | To define a factory for an entity, use the `$factory->define()` method in a factory file (e.g., `UserEntityFactory.php`). 8 | 9 | ```php 10 | $factory->define(App\Entities\User::class, function(Faker\Generator $faker) { 11 | return [ 12 | 'name' => $faker->name, 13 | 'emailAddress' => $faker->email 14 | ]; 15 | }); 16 | ``` 17 | - Use Doctrine entity property names (not database column names). 18 | - You can define multiple types for the same entity using `defineAs`: 19 | 20 | ```php 21 | $factory->defineAs(App\Entities\User::class, 'admin', function(Faker\Generator $faker) { 22 | return [ 23 | 'name' => $faker->name, 24 | 'emailAddress' => $faker->email, 25 | 'isAdmin' => true 26 | ]; 27 | }); 28 | ``` 29 | 30 | ## Using Factories in Seeds and Tests 31 | 32 | After defining factories, you can generate entities for tests or seeds using the `entity()` helper or the factory directly. 33 | 34 | - **Create (persist) a single entity:** 35 | ```php 36 | entity(App\Entities\User::class)->create(); 37 | // or 38 | $factory->of(App\Entities\User::class)->create(); 39 | ``` 40 | 41 | - **Make (do not persist) a single entity:** 42 | ```php 43 | entity(App\Entities\User::class)->make(); 44 | ``` 45 | 46 | - **Create multiple entities:** 47 | ```php 48 | entity(App\Entities\User::class, 3)->create(); 49 | // or 50 | $factory->of(App\Entities\User::class)->times(3)->create(); 51 | ``` 52 | 53 | - **Create a specific type:** 54 | ```php 55 | entity(App\Entities\User::class, 'admin')->create(); 56 | ``` 57 | 58 | ## Passing Extra Attributes 59 | 60 | You can override default attributes by passing an array: 61 | 62 | ```php 63 | $factory->define(App\Entities\User::class, function(Faker\Generator $faker, array $attributes) { 64 | return [ 65 | 'name' => $attributes['name'] ?? $faker->name, 66 | 'emailAddress' => $faker->email 67 | ]; 68 | }); 69 | 70 | $user = entity(App\Entities\User::class)->make(['name' => 'Taylor']); 71 | ``` 72 | 73 | ## Notes 74 | - The `entity()` helper returns an `Illuminate\Support\Collection` if you request multiple entities. 75 | - Use `->make()` to get an instance without saving, or `->create()` to persist to the database. 76 | - Always use property names as defined in your Doctrine entity. 77 | 78 | ## References 79 | - [Official Docs: Testing - Entity Factories](https://laravel-doctrine-orm-official.readthedocs.io/en/latest/testing.html) 80 | -------------------------------------------------------------------------------- /workbench/database/factories/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-doctrine/acl/bbf7cb96bd7bc11186827b5402b886f36ffab2da/workbench/database/factories/.gitkeep -------------------------------------------------------------------------------- /workbench/database/factories/OrganisationFactory.php: -------------------------------------------------------------------------------- 1 | define(Organisation::class, static function (Generator $faker, array $attributes) { 12 | return [ 13 | 'name' => $attributes['name'] ?? $faker->unique()->company, 14 | ]; 15 | }); 16 | -------------------------------------------------------------------------------- /workbench/database/factories/PermissionFactory.php: -------------------------------------------------------------------------------- 1 | define(Permission::class, static function (Generator $faker, array $attributes = []) { 15 | return [ 16 | 'name' => $attributes['name'] ?? $faker->unique()->word . '-' . $faker->unique()->word, 17 | ]; 18 | }); 19 | 20 | $factory->defineAs(Permission::class, 'view', static function () { 21 | return ['name' => 'view']; 22 | }); 23 | 24 | $factory->defineAs(Permission::class, 'edit', static function () { 25 | return ['name' => 'edit']; 26 | }); 27 | 28 | $factory->defineAs(Permission::class, 'delete', static function () { 29 | return ['name' => 'delete']; 30 | }); 31 | -------------------------------------------------------------------------------- /workbench/database/factories/RoleFactory.php: -------------------------------------------------------------------------------- 1 | define(Role::class, static function (Generator $faker, array $attributes = []) { 15 | return [ 16 | 'name' => $attributes['name'] ?? $faker->unique()->word . '-' . $faker->unique()->word, 17 | ]; 18 | }); 19 | 20 | $factory->defineAs(Role::class, 'admin', static function () { 21 | return ['name' => 'admin']; 22 | }); 23 | 24 | $factory->defineAs(Role::class, 'user', static function () { 25 | return ['name' => 'user']; 26 | }); 27 | -------------------------------------------------------------------------------- /workbench/database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(User::class, static function (Generator $faker, array $attributes = []) { 15 | return [ 16 | 'name' => $faker->name(), 17 | 'email' => $faker->safeEmail, 18 | 'password' => 'password', 19 | ]; 20 | }); 21 | 22 | $factory->defineAs(User::class, 'test', static function (Generator $faker, array $attributes = []) { 23 | return [ 24 | 'name' => 'test', 25 | 'email' => 'test@test.tld', 26 | 'password' => 'password', 27 | ]; 28 | }); 29 | -------------------------------------------------------------------------------- /workbench/database/factories/UserJsonPermissions.php: -------------------------------------------------------------------------------- 1 | define(UserJsonPermissions::class, static function (Generator $faker, array $attributes = []) { 15 | return [ 16 | 'name' => $attributes['name'] ?? $faker->name(), 17 | 'email' => $attributes['email'] ?? $faker->safeEmail, 18 | 'password' => 'password', 19 | ]; 20 | }); 21 | -------------------------------------------------------------------------------- /workbench/database/factories/UserSingleOrgFactory.php: -------------------------------------------------------------------------------- 1 | define(UserSingleOrg::class, static function (Generator $faker, array $attributes = []) { 15 | return [ 16 | 'name' => $attributes['name'] ?? $faker->name(), 17 | 'email' => $attributes['email'] ?? $faker->safeEmail, 18 | 'password' => 'password', 19 | ]; 20 | }); 21 | -------------------------------------------------------------------------------- /workbench/database/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-doctrine/acl/bbf7cb96bd7bc11186827b5402b886f36ffab2da/workbench/database/migrations/.gitkeep -------------------------------------------------------------------------------- /workbench/database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 19 | 20 | UserFactory::new()->create([ 21 | 'name' => 'Test User', 22 | 'email' => 'test@example.com', 23 | ]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /workbench/resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-doctrine/acl/bbf7cb96bd7bc11186827b5402b886f36ffab2da/workbench/resources/views/.gitkeep -------------------------------------------------------------------------------- /workbench/routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 10 | })->purpose('Display an inspiring quote')->hourly(); 11 | -------------------------------------------------------------------------------- /workbench/routes/web.php: -------------------------------------------------------------------------------- 1 |