owenvoke/laravel-xdg
A Laravel package for working with the XDG Base Directory specification, helping resolve standard config, data, cache, and state directories across platforms. It provides convenient helpers for using XDG paths within Laravel applications.
owenvoke/laravel-xdg wires the XDG Base Directory Specification into Laravel. Instead of scattering storage_path(), config_path() and hardcoded ~/.myapp strings around a CLI or desktop-ish Laravel app, you resolve config/cache/data/state/runtime directories the way Linux (and other Unix) tooling expects. v1.5.0 only widens the framework constraint to include Laravel 12 — no API or behaviour changes — so existing setups keep working after composer update.
composer require owenvoke/laravel-xdg — the service provider is auto-discovered; no manual registration needed on a standard Laravel skeleton.php artisan vendor:publish --tag=xdg-config or the equivalent tag listed in the README — check vendor/owenvoke/laravel-xdg/config for the exact key name).php artisan tinker
>>> xdg_config_home(); // ~/.config
>>> xdg_config_home('myapp'); // ~/.config/myapp
The first use case is almost always a CLI/daemon built on Laravel (Laravel Zero, an artisan-driven sync tool, a worker) where you want the SQLite database, logs, lock files and a user-editable config.json to land in ~/.local/share/myapp, ~/.local/state/myapp and ~/.config/myapp rather than inside the checked-out project directory.
Resolve paths at runtime, not at config-load time. The package is a thin resolver over the XDG_* environment variables with spec fallbacks ($XDG_CONFIG_HOME → ~/.config, $XDG_CACHE_HOME → ~/.cache, $XDG_DATA_HOME → ~/.local/share, $XDG_STATE_HOME → ~/.local/state, $XDG_RUNTIME_DIR → no default). Treat the accessors as functions to call, not values to snapshot.
Rebase Laravel in a service provider. Do it in register()/boot() of your own provider so it runs before anything touches storage:
public function register(): void
{
$this->app->useStoragePath(xdg_data_home('myapp').'/storage');
}
public function boot(): void
{
config([
'database.connections.sqlite.database' => xdg_data_home('myapp').'/database.sqlite',
'logging.channels.single.path' => xdg_state_home('myapp').'/logs/app.log',
'cache.stores.file.path' => xdg_cache_home('myapp').'/cache',
]);
}
Read and write user config deliberately. xdg_config_home('myapp').'/config.json' is the file a user is supposed to edit; xdg_data_home() is where you keep things they shouldn't. Keeping that split makes later "reset my settings" instructions trivial.
Handle the search-path variants. XDG defines XDG_CONFIG_DIRS and XDG_DATA_DIRS as colon-separated lists with system defaults (/etc/xdg, /usr/local/share:/usr/share). Use the package's *_dirs accessors rather than concatenating yourself if you support system-wide defaults layered under the per-user directory.
Isolate it in tests. Because the values come from env, set them per test to a temp dir and let the resolver do the rest:
putenv('XDG_CONFIG_HOME='.($tmp = sys_get_temp_dir().'/xdg-test'));
$this->app->forgetInstance(...); // if you cache the resolved value anywhere
Guard the platform. If the same codebase ships to macOS or Windows, wrap the calls behind your own AppPaths class that branches: XDG on Linux, ~/Library/Application Support/{app} on macOS, %APPDATA% on Windows. Keep that branch in one place so the rest of the app calls AppPaths::config() and never a raw helper.
config:cache will freeze the absolute paths. If you bake xdg_data_home() results into config/*.php at load time, php artisan config:cache on machine A produces a cached config with machine A's home directory. Resolve in a provider after the cache is loaded (as above) or via config() overrides in boot(), and never ship a cached config with these paths.XDG_* values are invalid and the implementation should fall back to the default. Don't putenv('XDG_CONFIG_HOME=foo') and expect foo to be honoured — and don't rely on the package to normalise a trailing slash for you; rtrim($path, '/') before concatenation.XDG_RUNTIME_DIR is the fragile one. It's unset in cron jobs, CI containers, SSH sessions without a login manager, and typically absent on macOS. It has no spec default, so any code depending on it needs an explicit fallback (sys_get_temp_dir() keyed by UID) and a permissions check — it's meant to be 0700 and cleared on logout, so don't persist anything durable there.HOME changes with the invoking user. Running the same command via sudo, su, systemd units or a scheduler resolves a different ~. Be explicit about which user's config you intend, and prefer resolving inside the command rather than at container boot./, and consistent between the XDG helpers and wherever you build paths by hand. Changing it in a later release silently orphans users' existing config/data directories — if you must change it, add a migration that moves the old directory.config/ file's return array, or another package's register() running earlier) gives you defaults you didn't intend. If ordering matters, check bootstrap/providers.php and register your path-rebasing provider near the top.owenvoke/laravel-xdg explicitly, keep the constraint loose (^1.4) so the widened framework support actually reaches you. Re-run your test matrix on Laravel 12 because the framework's own path/storage internals are what you're overriding.How can I help you explore Laravel packages today?