Goal
Remove the hard dependency webware/webware-admin -> webware/webware-acl. Admin should only suggest webware-acl. Admin's ACL usage should be abstracted around a neutral contract so consumers (host applications) can alias the ACL implementation to the common interface via their own ConfigProvider. Inverted direction: webware-acl will require webware-admin (its admin UI subsystem and dashboard widget are built on top of admin).
Current coupling (admin side)
composer.json requires webware/webware-acl: 0.1.x-dev (plus its VCS repo entry). Four source locations reference Webware\Acl\AclInterface:
src/ConfigProvider.php — getAclConfig() returns roles / resources / allow (Administrator => ['Member'], admin.dashboard.read), exported under the AclInterface::class config key. This role/resource/allow config should move to webware-acl's ConfigProvider.
src/Middleware/DashboardMiddleware.php — dispatches RegisterWidgetEvent, then filters the widget iterator through the ACL using the current user's roles. Also imports Webware\UserManager\UserInterface (reads the webware attribute key).
src/Container/DashboardMiddlewareFactory.php — resolves $container->get(Webware\Acl\AclInterface::class).
src/Widget/AclWidgetFilterIterator.php — wraps the widget iterator and calls $acl->isAllowed($user, $widget->resourceId, $widget->privilege).
test/unit/AclWidgetFilterIteratorTest.php exists but all tests are markTestSkipped('Blocked on webware-usermanager split-out (UserInterface typing).').
The acl -> admin direction is already event-based and not a problem: admin dispatches RegisterWidgetEvent (PSR-14 event); webware-acl's ConfigProvider registers RegisterWidgetListener under listeners for that event.
Research: abstraction options
Option A — Laminas\Permissions\Acl\AclInterface
- Admin already requires
laminas/laminas-permissions-acl: ^2.16 (its WidgetInterface extends laminas ResourceInterface), so zero new packages.
Webware\Acl\Acl already extends Laminas\Permissions\Acl\Acl implements Laminas\Permissions\Acl\AclInterface — webware's ACL is a drop-in implementation.
isAllowed($role, $resource, $privilege) is object-friendly: webware's UserInterface implements laminas RoleInterface, so $acl->isAllowed($user, $resourceId, $privilege) works against both a stock laminas ACL and webware's ACL.
- Preserves webware's object-centric design:
UserRoleIterator requires the UserInterface aggregate object (not string role names), and assertions (e.g. OwnershipAssertion / ProprietaryInterface) can only be satisfied by objects. See docblock on Webware\Acl\AclInterface::isAllowedRoute().
- App wiring becomes a one-line alias in the host app ConfigProvider:
'aliases' => [Laminas\Permissions\Acl\AclInterface::class => Webware\Acl\Acl::class].
- Admin changes: swap the
AclInterface import in the 4 locations above; drop getAclConfig() and the AclInterface::class config key; move the Administrator/admin.dashboard.read ACL config into webware-acl; move AclWidgetFilterIterator (+ test) into webware-acl; suggest webware-acl in composer.json and remove the VCS repo entry.
- Also drop the
Webware\UserManager\UserInterface import in DashboardMiddleware — read the standard Mezzio\Authentication\UserInterface::class request attribute (mezzio-authentication is already an admin dependency) and pass the value through opaquely. The app pipeline owns populating that attribute.
Option B — mezzio-authorization / mezzio-authorization-acl
- Adds
mezzio/mezzio-authorization + mezzio/mezzio-authorization-acl dependencies to admin.
AuthorizationInterface::isGranted(string $role, string $resource, ?string $privilege) — the role is a string. This fights webware's ACL design:
UserRoleIterator requires the UserInterface object (laminas RoleInterface implementation) to iterate roles.
- Assertions (
ProprietaryInterface) cannot be satisfied by a plain string role.
- Webware would need a custom adapter that recovers the user object from the request context — added friction for a weaker contract in this use case.
- Chosen over Option A only if the goal is to support non-laminas ACL implementations; webware's own ACL is laminas-based.
Decision (settled)
Option A is adopted. Admin's ACL usage will type-hint against Laminas\Permissions\Acl\AclInterface — purely as a decoupling mechanism; in practice the bound implementation will always be webware-acl's Acl (which implements the laminas interface). The abstraction implementation will be done by the maintainer; this issue records the research.
Dependency direction (final)
webware-admin: suggest webware/webware-acl; code references Laminas\Permissions\Acl\AclInterface only (no Webware\Acl\* references). Registrations may be guarded as optional wiring where appropriate.
webware-acl: require webware/webware-admin (0.1.x-dev) — its management UI (CRUD handlers, overview middleware, dashboard widget, templates) is built on webware-admin and is an intrinsic part of the package; consumers must not be expected to roll their own UI.
- Host app ConfigProvider:
'aliases' => [Laminas\Permissions\Acl\AclInterface::class => Webware\Acl\Acl::class].
- webware-acl owns the admin ACL config (roles/resources/allow —
Administrator => ['Member'], admin.dashboard.read), the AclWidgetFilterIterator (moved from admin, typed to the laminas interface), and the widget listener.
Related notes
- webware-acl currently has a bogus
suggest entry (webware/admin with a mention of a nonexistent RegisterAclWidgetListener) that should be replaced by the real require.
- Related docs:
webware-acl/docs/planning/todo-2026-05-31.md — "webware-admin resources/allow config" TODO item.
webware-acl/docs/admin-ui-workflows.md — "Redundant Administrator Parent" (admin's ConfigProvider currently contributes Administrator => ['Member']; after the move this config lives in webware-acl).
webware-acl/docs/integration-guide.md — acl <-> usermanager circular-dependency notes (adjacent concern).
Goal
Remove the hard dependency
webware/webware-admin->webware/webware-acl. Admin should onlysuggestwebware-acl. Admin's ACL usage should be abstracted around a neutral contract so consumers (host applications) can alias the ACL implementation to the common interface via their own ConfigProvider. Inverted direction:webware-aclwillrequirewebware-admin(its admin UI subsystem and dashboard widget are built on top of admin).Current coupling (admin side)
composer.jsonrequireswebware/webware-acl: 0.1.x-dev(plus its VCS repo entry). Four source locations referenceWebware\Acl\AclInterface:src/ConfigProvider.php—getAclConfig()returnsroles/resources/allow(Administrator => ['Member'],admin.dashboard.read), exported under theAclInterface::classconfig key. This role/resource/allow config should move to webware-acl's ConfigProvider.src/Middleware/DashboardMiddleware.php— dispatchesRegisterWidgetEvent, then filters the widget iterator through the ACL using the current user's roles. Also importsWebware\UserManager\UserInterface(reads the webware attribute key).src/Container/DashboardMiddlewareFactory.php— resolves$container->get(Webware\Acl\AclInterface::class).src/Widget/AclWidgetFilterIterator.php— wraps the widget iterator and calls$acl->isAllowed($user, $widget->resourceId, $widget->privilege).test/unit/AclWidgetFilterIteratorTest.phpexists but all tests aremarkTestSkipped('Blocked on webware-usermanager split-out (UserInterface typing).').The acl -> admin direction is already event-based and not a problem: admin dispatches
RegisterWidgetEvent(PSR-14 event); webware-acl's ConfigProvider registersRegisterWidgetListenerunderlistenersfor that event.Research: abstraction options
Option A —
Laminas\Permissions\Acl\AclInterfacelaminas/laminas-permissions-acl: ^2.16(itsWidgetInterfaceextends laminasResourceInterface), so zero new packages.Webware\Acl\Aclalreadyextends Laminas\Permissions\Acl\Acl implements Laminas\Permissions\Acl\AclInterface— webware's ACL is a drop-in implementation.isAllowed($role, $resource, $privilege)is object-friendly: webware'sUserInterfaceimplements laminasRoleInterface, so$acl->isAllowed($user, $resourceId, $privilege)works against both a stock laminas ACL and webware's ACL.UserRoleIteratorrequires theUserInterfaceaggregate object (not string role names), and assertions (e.g.OwnershipAssertion/ProprietaryInterface) can only be satisfied by objects. See docblock onWebware\Acl\AclInterface::isAllowedRoute().'aliases' => [Laminas\Permissions\Acl\AclInterface::class => Webware\Acl\Acl::class].AclInterfaceimport in the 4 locations above; dropgetAclConfig()and theAclInterface::classconfig key; move theAdministrator/admin.dashboard.readACL config into webware-acl; moveAclWidgetFilterIterator(+ test) into webware-acl;suggestwebware-acl in composer.json and remove the VCS repo entry.Webware\UserManager\UserInterfaceimport inDashboardMiddleware— read the standardMezzio\Authentication\UserInterface::classrequest attribute (mezzio-authentication is already an admin dependency) and pass the value through opaquely. The app pipeline owns populating that attribute.Option B —
mezzio-authorization/mezzio-authorization-aclmezzio/mezzio-authorization+mezzio/mezzio-authorization-acldependencies to admin.AuthorizationInterface::isGranted(string $role, string $resource, ?string $privilege)— the role is a string. This fights webware's ACL design:UserRoleIteratorrequires theUserInterfaceobject (laminasRoleInterfaceimplementation) to iterate roles.ProprietaryInterface) cannot be satisfied by a plain string role.Decision (settled)
Option A is adopted. Admin's ACL usage will type-hint against
Laminas\Permissions\Acl\AclInterface— purely as a decoupling mechanism; in practice the bound implementation will always be webware-acl'sAcl(which implements the laminas interface). The abstraction implementation will be done by the maintainer; this issue records the research.Dependency direction (final)
webware-admin:suggestwebware/webware-acl; code referencesLaminas\Permissions\Acl\AclInterfaceonly (noWebware\Acl\*references). Registrations may be guarded as optional wiring where appropriate.webware-acl:requirewebware/webware-admin(0.1.x-dev) — its management UI (CRUD handlers, overview middleware, dashboard widget, templates) is built on webware-admin and is an intrinsic part of the package; consumers must not be expected to roll their own UI.'aliases' => [Laminas\Permissions\Acl\AclInterface::class => Webware\Acl\Acl::class].Administrator => ['Member'],admin.dashboard.read), theAclWidgetFilterIterator(moved from admin, typed to the laminas interface), and the widget listener.Related notes
suggestentry (webware/adminwith a mention of a nonexistentRegisterAclWidgetListener) that should be replaced by the realrequire.webware-acl/docs/planning/todo-2026-05-31.md— "webware-admin resources/allow config" TODO item.webware-acl/docs/admin-ui-workflows.md— "Redundant Administrator Parent" (admin's ConfigProvider currently contributesAdministrator => ['Member']; after the move this config lives in webware-acl).webware-acl/docs/integration-guide.md— acl <-> usermanager circular-dependency notes (adjacent concern).