├── .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 | [](https://github.com/laravel-doctrine/acl/actions)
12 | [](https://codecov.io/gh/laravel-doctrine/acl)
13 | [](https://img.shields.io/badge/PHPStan-level%201-brightgreen.svg)
14 | [](https://laravel-doctrine-acl-official.readthedocs.io/en/latest/)
15 | [](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 |