Testing Your Laravel Package After Dependency Upgrades
When you maintain a Laravel package, upgrading a dependency changes the libraries your package uses. Neither Composer’s selection of new versions nor…
In Laravel, a route can identify a database record by a readable value in the URL, such as a slug, instead of the record’s numeric ID. A slug is a text value used in a URL, such as my-first-post. Laravel’s route model binding takes a value from the URL, looks up the matching Eloquent model (Laravel’s object representation of a database record), and passes that model to the route or controller.
Use {post:slug} when a particular route should look up a post by its slug column. Use a model-wide route key when routes for that model should generally use the same column. These choices tell Laravel which column to search when a request arrives. You still need to create slug values and make sure they are unique among the records Laravel searches for that route.
Laravel uses implicit route model binding when a route parameter matches an argument declared with an Eloquent model type. For example, {post} matches Post $post. Laravel looks up the model using its route key, which defaults to the model’s primary key, normally id. If Laravel finds no match, it returns a 404 response. The Laravel routing documentation describes this binding behavior.
Add the column name after a colon to make one route use a different lookup field:
use App\Models\Post;
use Illuminate\Support\Facades\Route;
Route::get('/posts/{post:slug}', function (Post $post) {
return $post;
});
For a request to /posts/my-first-post, Laravel queries the posts table for a row whose slug matches my-first-post, then passes that Post instance to the route. The placeholder name and argument name still need to correspond: {post} pairs with Post $post.
The route syntax only tells Laravel which column to query. It does not add a slug column, generate slug values, or enforce uniqueness. Make sure the database schema and the code that creates posts handle those responsibilities.
To have routes for a model usually resolve records by the same column, configure the route key on the model. Laravel 11’s routing documentation shows overriding getRouteKeyName():
public function getRouteKeyName(): string
{
return 'slug';
}
Laravel 13 also documents the RouteKey model attribute:
use Illuminate\Database\Eloquent\Attributes\RouteKey;
use Illuminate\Database\Eloquent\Model;
#[RouteKey('slug')]
class Post extends Model
{
// ...
}
Both approaches make slug the default column Laravel uses to find that model through its routes. Choose {post:slug} when only one route should use the slug; configure the model when slug lookup should be the default across its routes. Check the documentation for the Laravel version your project uses: the Laravel 11 routing page documents the method, while the Laravel 13 routing page also documents the attribute.
When a route identifies both the user and the post, each user can have a post named announcements; the slug only needs to be unique among that user's posts:
Route::get('/users/{user}/posts/{post:slug}', function (User $user, Post $post) {
return $post;
});
With this nested route and a custom child key, Laravel scopes the post lookup through the user’s posts relationship. The User model therefore needs a relationship Laravel can identify by that name. If you use different names for the relationship or route parameter, check that Laravel can match the child parameter to the right relationship.
Call scopeBindings() on a nested route to enforce the parent-child relationship without adding a custom key to the placeholder:
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
return $post;
})->scopeBindings();
Match the database’s uniqueness rule to the route’s scope. If a post slug only needs to be unique per user, enforce uniqueness for the user-and-slug combination rather than for the slug alone. Route binding performs the lookup; it does not choose or enforce the application’s uniqueness policy.
Laravel handles incoming requests and generates URLs in separate ways. Route model binding finds a model from a URL value. When the route helper receives an Eloquent model, Laravel uses that model’s route key to fill the URL parameter, as described in the URL generation documentation.
That distinction matters when one route uses {post:slug} but the model’s route key remains id. Test links generated for that route and confirm that the resulting URL contains the value the route expects. A model-wide slug key keeps model-based URL generation aligned with slug-based routes; a route-specific key leaves the model’s general route key unchanged.
If Laravel does not bind the expected model, first compare the route placeholder with the type-hinted argument. {post} should pair with Post $post. Also check whether the requested slug exists and whether the lookup is scoped through the relationship you intended. An implicit binding with no match returns a 404; Laravel also provides a route-level missing() hook when the route should handle that case differently, such as by redirecting.
Laravel 11’s routing documentation says implicit binding excludes soft-deleted models by default. Add withTrashed() to the route when it should resolve those records, and decide how the route should handle a deleted record once Laravel finds it.
Use custom binding logic only when a column lookup does not meet the requirement. Laravel supports registering parameter resolution with Route::bind() or customizing model resolution through resolveRouteBinding(). For a nested lookup scoped to a parent, account for resolveChildRouteBinding() too. Laravel uses that method to find a child model under its parent.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com