0015. PathResolverService And WebDavPath Facade For Centralized Path Resolution¶
Status¶
Accepted
Context¶
The package assembles user-scoped storage paths in two places. DefaultSpaceResolver reads the configured root and
optional prefix from the space definition and combines them with the authenticated principal's identifier to produce
the disk-relative path handed to Flysystem. Because this formula lived entirely inside DefaultSpaceResolver, it was
not accessible to application code without going through SabreDAV internals or duplicating the logic.
Consuming applications may need to:
- resolve the public WebDAV mount URL for a space (e.g. to display it in a UI or return it in an API response)
- resolve the disk-internal user path for a space (e.g. to run background jobs on a user's storage area)
- replace the path formula to apply a custom layout without replacing the full space resolver
Without a dedicated service, application code has no stable, replaceable access point for either operation. Duplicating
the path formula outside DefaultSpaceResolver would produce two sources of truth that can diverge silently.
ADR 0008 already identifies the Facade pattern as approved for "presenting a smaller, intention-revealing interface to a
more complex subsystem". The Laravel service container's bindIf() pattern, used throughout this package, makes any
interface-backed service replaceable without editing package source.
Decision¶
A WebDavPrincipalInterface is introduced in Contracts\Auth with a single method getPrincipalId(): string.
AccountInterface extends it, and WebDavPrincipalValueObject implements it. This gives both the internal pipeline
and external application code a shared, minimal contract for principal identity without leaking the internal value
object into the public API.
A PathResolverInterface is introduced with two methods:
resolvePath(WebDavPrincipalInterface $principal, string $spaceKey): string— returns the disk-internal, user-scoped path following the{root}/{prefix}/{principal.id}formula. Accepts anyAccountInterfaceorWebDavPrincipalValueObject.resolveUrl(string $spaceKey): string— returns the public WebDAV mount URL for the given space key. This is the same URL for every user of a space; it does not include the user-scoped path.
PathResolverService is the default implementation. It reads the space configuration from the Laravel config
repository, validates the root, assembles the path parts, and builds the mount URL from app.url and
webdav-server.route_prefix.
DefaultSpaceResolver delegates the path formula to PathResolverInterface via constructor injection. It no longer
assembles the {root}/{prefix}/{principal.id} string itself.
WebDavPath is a Laravel Facade backed by PathResolverInterface::class. It exposes both methods as static calls
for ergonomic use in application code (views, controllers, job classes).
PathResolverInterface is bound to PathResolverService in StorageRegister via bindIf(). Applications that need
a custom path layout can replace only this binding in AppServiceProvider::register().
The architectural rules are:
resolvePath()is the single authoritative location for the user-scoped disk path formularesolveUrl()returns the public WebDAV mount URL only — it never includes the user-scoped pathDefaultSpaceResolvermust delegate toPathResolverInterface; it must not re-implement the formula- the Facade accessor must point to the interface, not the concrete class, to keep the override path working
Consequences¶
Advantages:
- the path formula lives once;
DefaultSpaceResolverand application code share the same implementation PathResolverInterfaceis DI-replaceable viabindIf()without touchingDefaultSpaceResolver- the
WebDavPathFacade gives application code (views, controllers, background jobs) ergonomic access to mount URLs and user paths - the change introduces no BC break:
SpaceResolverInterfaceis unchanged andDefaultSpaceResolverisfinal, so constructor changes are not a public API concern
Trade-offs:
resolveUrl()intentionally excludes the user path; consumers building full per-user WebDAV URLs must call bothresolveUrl()andresolvePath()and combine the results themselves — this is intentional because the public WebDAV address space and the internal disk layout are separate concerns- an extra abstraction layer is added for what is ultimately string concatenation; the trade-off is justified by the single-source-of-truth and replaceability benefits
Rejected alternatives:
- keep the path formula inside
DefaultSpaceResolverand expose a separate helper alongside it - rejected because it produces two sources of truth and offers no override path for the formula without replacing the full space resolver
- add a static helper function without a service contract
- rejected because static helpers cannot be replaced via the container, which contradicts the package's extension-oriented architecture
- make
resolveUrl()accept an optionalWebDavPrincipalValueObjectto optionally include the user path - rejected because the public WebDAV mount URL and the internal disk path are different concerns; combining them in one method conflates the public API with storage internals