phlak/directory-lister
PHLAK DirectoryLister is a lightweight, standalone PHP directory listing tool that turns folders into browsable indexes. It offers sorting, search, breadcrumbs, file icons, and customizable templates with no database required.
Directory Lister is a standalone PHP application, not a Laravel package — there is no service provider, facade, or php artisan vendor:publish workflow, and the composer package (phlak/directory-lister) simply ships the app. Treat it as a small self-hosted service you mount next to (or behind) your Laravel app, not as a library you compose into it.
Minimal path to a working listing:
./.. path segments, and direct-linked content reachable through query-string parameters, plus a HIDE_VCS_FILES typo that made the setting a no-op). Treat <5.8.0 as not deployable; put phlak/directory-lister: 5.8.0 (or a ^5.8 constraint) in your deploy manifest.public/files/_lister/ (or a sibling docroot), point its files/ directory at the folder you want to expose, and leave Laravel untouched.files.example.com): best when you want HTTP basic auth or nginx auth_request in front of it, sharing the Laravel session cookie via session.domain.composer create-project phlak/directory-lister in a scratch dir, or download the release zip. Copy it to your target path; never leave config/, vendor/, or the app source reachable from the web.ln -s /var/www/app/storage/app/public/downloads files (and php artisan storage:link on the Laravel side). The lister only sees what is inside its files/ directory.config/app.php: set view (theme), sort_order, date_format, timezone, hide_dot_files, hide_vcs_files, enable_search, enable_download_zip, zip_compress, direct_links, and — if you need it — the auth block with a password_hash() value.Where to look first: config/app.php (every behavior is here), the themes/ directory (copy one, rename it, set view), and — since 5.8.0 — the docs/ folder inside the repo, which is now the source of truth for the documentation (the website is generated from it). Reading docs/ at your pinned tag is the only reliable way to get config semantics for your version. Your first real use case is almost always "let clients/vendors browse a folder of exports, invoices, or release artifacts without me building a CRUD screen."
Subdirectory mount with Laravel-owned auth. Put the lister at public/downloads/ and protect the path with a Laravel route/middleware or an nginx auth_request that hits a signed Laravel endpoint. This keeps Directory Lister's own password auth out of the picture and reuses your existing guards and policies.
Subdomain with shared session. Serve on files.example.com, set Laravel's SESSION_DOMAIN=.example.com, and gate the vhost with the same cookie. Simpler than middleware, and lets you scale the lister independently.
Deploy-time config templating. Since config is a PHP array, keep a config/app.php.tpl in your repo and render it in your deploy script (envsubst, Ansible template, or a small PHP script) so timezone, date_format, auth, and feature flags differ per environment. Do not hand-edit on the server. This also gives you a single place to force zip_downloads/zip_compress to real booleans (see gotchas).
Theme as the extension point. Copy themes/<default> to themes/company, replace the logo/CSS, and adjust the header/footer templates. This is the sanctioned way to brand it; avoid patching app source, or your next upgrade will hurt.
Use DIRECT_LINKS for stable, shareable deep URLs (new in 5.8.0). Directories matching the pattern are linkable directly instead of only being reachable by browsing parents, and 5.8.0 also centralized directory URL generation and tightened path-segment encoding. Practical use: set direct_links to your client/vendor slug pattern (/^client-[a-z0-9-]+$/) and hand out https://files.example.com/?dir=client-acme in emails, invoices, or a Laravel-generated link — users no longer need the parent listing to exist or be browsable. Pair it with the auth layer, since a direct link is only as private as the gate in front of it.
README as landing content. 5.8.0 decoupled README rendering from hide_dot_files, so a README.md/README in a directory renders even when hidden-file filtering is on. Drop a short README per client folder ("these are your Q3 exports, contact X") and the listing becomes self-documenting without any Laravel code.
Pair it with object storage for big files. For large artifact sets, mount an S3/Spaces bucket via rclone/s3fs at files/, or front the lister with a CDN and let enable_download_zip handle bundling small groups. Turn zip off for multi-GB trees.
Operational pattern for sharing. Generate a per-client symlink (files/client-a -> /storage/clients/a/exports) and hand out the URL. Cheap multi-tenancy without touching the app — and with 5.8.0's canonical-path checks, ..-style URL tampering no longer escapes the intended boundary.
./, ../ segments) means hide_dot_files/hide_vcs_files were advisory on hostile input, and the direct-link query-string hole leaked content from outside the linked path. Both are fixed in 5.8.0 — but they are fixes, not hardening you can defer.<5.8.0, ZIP_DOWNLOADS=false and ZIP_COMPRESS=false were interpreted as enabled — the opposite of intent, which is exactly the kind of bug that turns "zip is off for the 40 GB tree" into an OOM incident. Fixed in 5.8.0, but audit your config template for quoted strings ('false') which are truthy in PHP, and keep the values as true/false literals.HIDE_VCS_FILES was silently ignored before 5.8.0 due to a variable typo. If you had a .git directory or .svn inside files/, assume it was listed. Re-check any listing that ever exposed a deploy checkout, and prefer keeping VCS metadata outside files/ entirely.files/ boundary; a bad symlink or a misconfigured docroot exposing config/ is a full disclosure bug. Keep config/ and vendor/ outside the webroot where possible.password_hash() value; there are no roles, sessions, or audit trail. For anything with more than one audience, put Laravel auth or HTTP basic auth in front.trusted_proxies matters. If ip_blacklist/ip_whitelist behave oddly behind a load balancer, set trusted proxies so the client IP is the real one, not the balancer's.ZipArchive and enough memory_limit/max_execution_time. 5.8.0 fixed incomplete archives in some compression paths — if you built tooling that "verifies" archives by size, re-baseline it, and re-run any previously produced bundles you still ship.file_types is extension-driven. Preview handlers won't fire for misnamed files (e.g. .bin containing a PDF). Check the mapping before blaming the theme.hide_dot_files and hide_vcs_files are display filters; files placed inside files/ are still reachable by direct URL. Never park .env, .git, or credentials inside the listing root.+, #, and non-ASCII — old links may not match byte-for-byte.config/app.php against yours on every minor/major bump, and read the tag's docs/ folder — the docs now move with the code, so they can change without a website changelog entry. Pin the version in your deploy manifest.files/ exists and is readable by the PHP user. A missing files/ dir is the single most common cause of an empty listing.How can I help you explore Laravel packages today?