eloquent/pathogen
Eloquent Pathogen scans Laravel Eloquent models for common anti-patterns and hidden issues, flagging risky relationships, unsafe mass assignment, missing casts, and other unhealthy code so you can keep your models clean, predictable, and maintainable.
Pathogen is a pure string-manipulation library: it gives you an immutable Path value object for joining, splitting, normalizing and inspecting filesystem paths. It performs no I/O — you still use Laravel's Storage/File facade to touch the disk — which is the key thing to internalise before adopting it.
Minimal steps:
composer require eloquent/pathogen (the package is archived but still installable from Packagist; if you need to guarantee reproducibility, commit the lockfile or vendor the small source tree).src/Eloquent/Pathogen/Path.php and PathInterface.php first — that is the entire public surface. Everything else (AbsolutePath, RelativePath, factories) is a thin specialisation.php artisan tinker:use Eloquent\Pathogen\Path;
$path = Path::create(storage_path('app'));
echo $path->join('uploads', 'invoice.pdf'); // /var/www/app/storage/app/uploads/invoice.pdf
First realistic use case in a Laravel app: building storage paths from untrusted or loosely-structured input (upload names, tenant folders, date-partitioned directories) without hand-rolling rtrim($a, '/') . '/' . ltrim($b, '/') in ten places.
Use Pathogen when you need to model a path. Use base_path(), storage_path() and Storage::path() when you just need a constant string.
Wrap once, pass the value object around. Don't sprinkle Path::create() calls through controllers. Normalise at the boundary — a form request, a DTO, or a small service — and hand the rest of the app a Path:
final class UploadPath
{
public static function for(string $tenant, string $originalName): Path
{
return Path::create(storage_path('app/tenants'))
->join($tenant, 'uploads', basename($originalName));
}
}
Path composition for generated artifacts. Report exports, PDF invoices, log rotation, cache-busting filenames — anywhere you concatenate segments, Pathogen's join() keeps the separator logic in one place and avoids // or missing-slash bugs:
$target = Path::create($base)->join('reports', $year, $month)->join("report-{$id}.pdf");
Storage::put($target->string(), $contents);
Extension and filename inspection for validation. getExtension(), getFilename() and the related accessors give you a cheap, filesystem-free way to validate before you write:
$path = Path::create($request->input('path'));
abort_unless($path->hasExtension() && in_array($path->getExtension(), ['csv','xlsx']), 422);
Normalize before persisting. Store the normalized form in the database so queries and comparisons are stable, and only expand to an absolute path at the point of I/O. This keeps environment-specific prefixes (/var/www/... on prod, /Users/... locally) out of your schema.
Compose, don't replace. Keep using Laravel's filesystem layer for existence checks, symlink resolution, atomic writes and stream-wrapped disks. Pathogen sits upstream of it: build the path → convert to string → hand to Storage. Resist the temptation to build a parallel filesystem abstraction on top of it.
Container binding for testability. If you build paths in more than one or two places, bind a small PathBuilder interface in a service provider and swap a fake in tests; Pathogen itself has no mocking story.
Symfony\Component\Filesystem\Path (well-maintained, handles Windows separators and symlinks) or simply Illuminate\Support\Str.normalize() cleans .. segments but does not sandbox them. Path::create('../../etc/passwd') is a legal path. Always re-validate with realpath() against an allowed root, or route everything through Storage::path() which won't escape its root.isAbsolute() tells you the shape of the string, not whether anything is there.$path->join(...) to mutate $path in place. Assign the result./. It does not adapt to Windows \. If you accept paths from Windows clients, normalize \ to / yourself before constructing.s3://bucket/key will be parsed as a weird relative path with a colon in it. Strip the scheme and use Storage::disk('s3') for the actual read/write; only feed Pathogen the key portion.url()/asset() for those.PathInterface if you need a domain-specific path type (e.g. a tenant-scoped path that always prefixes an ID), and use the absolute/relative factories if you want type-level guarantees about what you're holding. That's the main reason to pick this over Str helpers.var_dump($path->string()) at each step — the failure is almost always an extra segment passed to join() or a normalisation you assumed but never called.How can I help you explore Laravel packages today?