webfactory/object-routing
Object-routing maps URLs to domain objects and back again, so your Laravel app can resolve incoming paths into models and generate links from objects without hand-written route rules.
webfactory/object-routing solves the outbound half of routing: instead of scattering route('users.show', $user->id) calls through controllers, mails, and Blade templates, you annotate a domain object once with the route it belongs to, and the package derives the route name and parameters from the object itself. Think of it as the mirror image of Laravel's route model binding — binding turns a URL into an object, object-routing turns an object into a URL.
composer require webfactory/object-routing
Read these first, in order:
ObjectRoute attribute/annotation class — this is the entire public surface you'll touch in your domain code.ObjectRouter / Router, depending on version) — this is what you'll wrap and inject.composer.json — note there is no Laravel service provider and no auto-discovery. You own the wiring.A first use case that pays for itself immediately: an object is used in a controller redirect, a queued notification, and three Blade partials. Annotate it once:
use Webfactory\ObjectRouting\Annotation\ObjectRoute;
#[ObjectRoute('users.show', params: ['id' => 'getId'])]
class User
{
public function getId(): int { /* ... */ }
}
Then register a thin adapter in a service provider that delegates URL generation to Laravel's UrlGenerator:
$this->app->singleton(ObjectRouter::class, fn ($app) => new ObjectRouter(
new LaravelRouteGenerator($app['url']) // implements the package's RouteGenerator contract
));
Calling app(ObjectRouter::class)->generate($user) should now return the same string route('users.show', $user->getId()) would.
Wrap, don't leak. The package's value is that your domain code no longer knows route names. Keep it that way: inject the object router into controllers, listeners, and mailables; never let a Blade template call route() with a raw ID for something that already has an ObjectRoute.
Implement the package's RouteGenerator interface against Laravel's Url facade rather than rewriting the metadata logic. The interface is narrow (route name + params → string), which keeps your adapter to a few lines and means upstream fixes to attribute reading, inheritance, and caching land for free.
Multiple routes per object. An object frequently has both an admin and a public representation. Use the annotation's route-type/name selector to register several attributes on one class and pass a discriminator at generation time, instead of branching on user role at every call site:
#[ObjectRoute('users.show', params: ['id' => 'getId'])]
#[ObjectRoute('admin.users.show', routeType: 'admin', params: ['id' => 'getId'])]
class User { /* ... */ }
Extract parameters from methods, not properties. The params map points at getter names, which means computed values (slugs, locale-prefixed keys, versioned identifiers) keep working without you having to materialize them as real columns.
API resources and iterables. Because generation is a plain function of the object, UserResource::collection($users) can map each model through the router without knowing route names. This is the single biggest consistency win when URLs appear in JSON responses, sitemaps, and emails.
Test at the route-name boundary. In feature tests, assert against the generated URL (assertRedirect(route('users.show', $user))); in unit tests of the object itself, assert the route name and parameter array via a fake RouteGenerator. That keeps domain tests fast while still catching annotation typos.
Precompute for hot paths. See the caching note below — for listing pages that emit hundreds of URLs, build a class => [route, params] map once at boot (or via php artisan config:cache-style config array) and skip reflection entirely.
2.2.0 is a maintenance release, but the constraint bump is the part that can bite you. The release moves the requirement to currently supported Symfony versions and updates PHPUnit. Laravel ships on Symfony components (symfony/routing, symfony/http-foundation), so on older Laravel majors you may hit a solver conflict or, worse, a silently older symfony/routing in the lockfile. Before upgrading, run composer why symfony/routing and composer why symfony/http-foundation. There are no API changes in 2.2.0 — the other change was switching the CS-fixer CI job to the friends-of-php/php-cs-fixer Docker image, which is purely internal and irrelevant to consumers.
No Laravel integration ships with the package. There is no ServiceProvider, no facade, and no config file. Anything that looks like framework support lives in your AppServiceProvider. Budget for that and document it; it is the most common source of "the package doesn't work" confusion.
Reflection on every request. Attribute/annotation lookup runs through reflection and, on older style setups, a Doctrine annotation reader. It is not free. On high-traffic listing endpoints, memoize per class or replace the reader with a precomputed map. Measure before you assume — single-object use is fine.
Annotations vs attributes. The package's API predates PHP 8 attributes. If you're on modern PHP, prefer attributes; if you're working in a codebase still using docblock annotations, check whether the installed version actually reads them rather than assuming the syntax in the README applies to your version.
Failure mode is Laravel's, not the package's. A wrong route name surfaces as RouteNotFoundException deep inside UrlGenerator with no mention of your domain class. Catch and re-throw with the class name in the message — it turns a five-minute debug into a five-second one.
Absolute vs relative. Laravel's route() defaults to absolute URLs. Emails and JSON need absolute; in-page links often don't. Make the flag explicit in your adapter signature (default absolute, opt out where appropriate) rather than having callers re-wrap the result.
Parameter methods returning null. Optional or lazy values (a slug that hasn't been generated yet) produce URLs with empty segments or a hard failure depending on the route definition. Decide the contract up front: throw, or omit the parameter.
Extension points worth knowing. The RouteGenerator implementation boundary is the one you'll actually use; the metadata reader is the second (swap it for a cached implementation). Everything else in the package is small and stable — pin the major version and treat 2.2.0 as a safe bump once your Symfony constraints line up.
How can I help you explore Laravel packages today?