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