jeffersongoncalves/laravel-report-url
A lightweight Laravel package for adding URL reporting to your application. It provides configuration, routes, and controller scaffolding to collect, store, and manage reports associated with URLs, making it easy to integrate into existing Laravel projects.
composer require <vendor>/report-url
php artisan vendor:publish --tag="report-url-config"
php artisan vendor:publish --tag="report-url-migrations"
php artisan migrate
Look here first: config/report-url.php. Four keys drive almost all behaviour — disk (must be a private disk), ttl (default 7 days), middleware (applied to the report-url.download route, alongside signed), and restrict_to_owner. Confirm your disk before writing any code; pointing this at public defeats the entire purpose of the package.
First use case — stop emailing large report attachments:
use Vendor\ReportUrl\Facades\ReportUrl;
$link = ReportUrl::store($pathToGeneratedFile, ttl: now()->addDays(3));
return (new ReportReady($link->url(), $link->expires_at))
->to($user->email);
// routes/console.php
Schedule::command('report-url:prune')->daily()->onOneServer();
Two entry points, pick by what you already have. storeFile() when the report generator hands you an UploadedFile/File-like object; store() when you have a path on disk or raw contents from a PDF/CSV writer. Both return the same object exposing ->url() and ->expires_at.
Per-call TTL for tiered retention. Keep the config default sensible (7 days) and override at the call site for reports with different sensitivity:
ReportUrl::store($path); // config default
ReportUrl::store($path, ttl: now()->addHours(24)); // short-lived
ReportUrl::store($path, ttl: now()->addDays(30)); // monthly statements
Email the link, keep the file private. The whole workflow is: generate → store() → put ->url() in a Mailable/Notification → let the recipient click through the signed route. Render ->expires_at in the email body so users don't discover expiry by hitting a 410.
Audit and analytics via the event. ReportUrlDownloaded is the hook for "who actually opened their report" — listen for it and persist to an activity log or fire an internal webhook. The built-in download counter and last_downloaded_at cover the simple case; the event is where you add IP, user agent, or per-recipient attribution.
Owner restriction for multi-tenant apps. With restrict_to_owner enabled, treat the link as user-scoped: generate it for a specific user and ensure guests/anonymous links are handled deliberately. This is the difference between "signed link" (anyone holding it) and "signed link for this user" (session-bound), and it materially changes what you can safely put behind it.
Scheduled-report integration. With laravel-saved-filter 1.9+ installed, the package binds HostsReportFiles automatically — scheduled reports too large to attach get emailed as download links with no code change on your side. Pin the constraint in composer.json so the binding doesn't silently disappear on a downgrade or a lockfile rebuild.
Testing. Storage::fake() for the disk, Event::fake() to assert ReportUrlDownloaded, and $this->get($link->url()) to hit the real signed route. Use $this->travel(8)->days() to prove the 410 path rather than mocking clocks by hand.
ttl in config has no effect on links already issued; there is no "extend this link" operation. If you need sliding expiry, issue a new link.restrict_to_owner is the sharp edge. Enabling it changes access semantics for every existing link style, and behaviour for guests and queued notifications is easy to get wrong. Verify with a second user account before shipping, and check what happens when the owning user is deleted or the session is on another device.report-url:prune --dry-run in production first and read the count. Keep onOneServer() and consider withoutOverlapping() — concurrent prunes or a prune racing an in-flight download will produce confusing 404/500s for a user who was legitimately mid-download.web group. If you put session/auth middleware on the route, the web group must be present or auth silently never resolves a user. The signed middleware stays regardless — don't remove it while adding your own.report-url.download is a stable public identifier; if you override the route or its prefix, update any hardcoded route() calls and any links already sitting in users' inboxes (they won't heal).disk, add middleware, and listen to the event. Those four cover most customisation; avoid forking the route or the model unless you must.laravel-saved-filter binding requires 1.9+; below that, oversized scheduled reports fall back to attachment attempts and can fail at the mail layer rather than in an obvious place.How can I help you explore Laravel packages today?