I'm building a shopping app using Laravel where each product's URL must be kept concise.
Instead of using the following permalink structure: (which is common, but unfavorable)
www.example.com/products/{product-slug}
I want to use this permalink structure:
www.example.com/{product-slug}
In order to accomplish this, I'm using an implicit route model binding in my routes file:
Route::get( '{product}', function ( App\Product $product ) {
return view( 'product' ); // this works, but is not what I want
});
And I am overriding the lookup behavior in my Product model:
class Product extends Model
{
public function getRouteKeyName()
{
return 'slug'; // use the 'product.slug' column for look ups within the database
}
}
Now, according to Laravel's documentation:
Laravel automatically resolves Eloquent models defined in routes or controller actions whose type-hinted variable names match a route segment name.
(View Source)
So I know that Laravel will match the {product} variable to a product stored within my database, or return a 404 response if one is not found.
And this all makes sense to me...
However...
Each product page is unique, so after the route matches a {product}, that {product} object needs to be passed to a controller for further processing.
So how do I pass this route to a controller, if I want to keep my implicit model binding?
Point the route to a controller function.
This would be your route (I named the controller ProductController and pointed it to show function, but you can rename both to your liking):
Route::get( '{product}', 'ProductController#show');
And this would be in your ProductController:
public function show(Request $request, \App\Product $product)
{
// Do stuff with $product
// Return view and pass it the $product variable
return view('product', compact('product'));
}
To answer my own question, I think I've found a great solution that combines my initial approach and devk's response:
Credits to Arjun's Blog for the idea.
As it turns out, you can also perform implicit model binding within a controller by passing an Eloquent model as a dependency:
/* App/Http/Controllers/ProductController.php */
/**
* Get the Product object.
*
* #param App\Models\Product
*/
public function show( Product $product )
{
return view( 'product', compact( 'product' ) );
}
Even though we are now referencing the model using a controller, Laravel will still automatically resolve the model. In fact, this behavior is clearly defined within the documentation:
Laravel automatically resolves Eloquent models defined in routes or
controller actions whose type-hinted variable names match a route
segment name.
(View Source)
I must have missed those words when I read it the first time...
Now, in order to set up the {product-slug} route (in the way I wanted to), you must set up your model and route definitions like so:
/* App/Models/Product.php */
class Product extends Model
{
/**
* Get the route key for the model.
*
* #return string
*/
public function getRouteKeyName()
{
return 'slug';
}
}
As mentioned earlier, overriding the getRouteKeyName() method will make Laravel search for a product using it's slug column in the database instead of its id column (which is the default).
/* routes/web.php */
Route::get( '{product}', 'ProductController#show' );
In our routes file, we still name our parameter {product} (instead of {product-slug}) because the name of the parameter must match the name of the Eloquent model.
Using this configuration, requests made on:
www.example.com/{product-slug}
will return a product page if the provided {product-slug} matches one stored inside the database. If a product is not found, a 404 Not Found response will be returned instead.
However, because we are binding this route to the base path /, this means that every URL requested by a client will be passed through this configuration.
To avoid this problem, make sure that your route definitions are in proper order within your routes file (from greatest to least precedence), and use validation when conflicts occur.
Related
For reference I used this post and Laravel documentation:
Laravel 5.6 getRouteKeyName() not working
https://laravel.com/docs/5.8/routing#explicit-binding
In my routes I have a resources array like this:
Route::resources([
...
'state' => 'StateController',
...
]);
In my controller I am trying to access models by the slug. My state table and model has name and slug columns.
StateController
public function show(State $state)
{
dd($state);
// return view('state.show', compact('state'));
}
public function getRouteKeyName()
{
return 'slug';
}
If I remove the State model typecast it prints out the string indiana for the url: http://codebase.localhost.com/state/indiana But when I put the typecast back in, it gives me a 404. It can't find the model.
I thought getRouteKeyName was supposed to retrieve the model by the string passed.
What gives?
Here are my models fillables
'name', 'order', 'slug'
It's definitely a record in my table.
Looks like the issue is with the getRouteKeyName method being defined in your controller.
From the docs:
you may override the getRouteKeyName method on the Eloquent model
Try moving the getRouteKeyName method into your State model.
I have two routes:
Route::get('subjects/{subject}/{tag?}', 'SubjectController#show');
Route::get('subjects/{subject}/{tag}/{lesson}','LessonController#show');
When I hit the first route, it works properly but when I hit the second route, I get the following error response:
Sorry, the page you are looking for could not be found.
Is this because laravel is trying to treat the /{tag}/{lesson} portion of 2nd route as the value of the parameter of 1st route?
My controller methods are as follows:
//SubjectController.php
public function show($subjectSlug, $tag = null)
{
dd('Inside SubjectController#show');
}
//LessonController.php
public function show(Subject $subject, Tag $tag, Lesson $lesson)
{
dd('Inside LessonController#show');
}
When I visit, say,
localhost:3000/subjects/mysubject-slug/1
It matches the first route and responds accordingly, but when I visit,
localhost:3000/subjects/mysubject-slug/1/mylesson-slug
it shows the page not found error. How can I fix this?
As mentioned in the comments, because of Route Model Binding you can end up with a 404 when the model for the binding can not be retrieved. When using implicit route model binding the primary key will be used to search against by default. This can be changed on the model to use a different field, in this case the slug field.
"If you would like model binding to use a database column other than id when retrieving a given model class, you may override the getRouteKeyName method on the Eloquent model"
Laravel 5.5 Docs - Routing - Route Model Binding - Implicit Binding
public function getRouteKeyName()
{
return 'slug';
}
Try to change your controller to
//LessonController.php
public function show($subject, $tag, $lesson)
{
dd('Inside LessonController#show');
}
And see if it gets hit. If it does, your binding is done incorrectly.
On the side note, I suppose you don't have Route::resource() set up somewhere up in the routes file?
I am working on a school project. while working on a schools detail page I am facing an issue with the URL. My client needs a clean URL to run AdWords. My school detail page URL: http://edlooker.com/schools/detail/4/Shiksha-Juniors-Ganapathy. But he needs it like http://edlooker.com/Shiksha-Juniors-Ganapathy. If anyone helps me out it will be helpful, thanks in advance.
You need to define this route after all routes in your web.php (if laravel 5.x) or in routes.php (if it is laravel 4.2).
Route::get('{school}','YourController#getIndex');
And your controller should be having getIndex method like this,
public function getIndex($school_name)
{
print_r($school_name);die; // This is just to print on page,
//otherwise you can write your logic or code to fetch school data and pass the data array to view from here.
}
This way, you don't need to use the database to get URL based on the URL segment and you can directly check for the school name in the database and after fetching the data from DB, you can pass it to the school details view. And it will serve your purpose.
Check Route Model Binding section in docs.
Customizing The Key Name
If you would like model binding to use a database column other than id when retrieving a given model class, you may override the getRouteKeyName method on the Eloquent model:
/**
* Get the route key for the model.
*
* #return string
*/
public function getRouteKeyName()
{
return 'slug';
}
In this case, you will have to use one front controller for all requests and get data by slugs, for example:
public function show($slug)
{
$page = Page::where('slug', $slug)->first();
....
}
Your route could look like this:
Route::get('{slug}', 'FrontController#show');
I have made eloquent-sluggable work on my app. Slugs are saved just fine. Buuuut... How do I use it to create a pretty url?
If possible, I would like to use them in my url instead of ID numbers.
Yes, you can use slug in your route and generated url, for example, if you declare a route something like this:
Route::get('users/{username}', 'UserController#profile')->where('profile', '[a-z]+');
Then in your controller, you may declare the method like this:
public function profile($username)
{
$user = User::where('username', $username)->first();
}
The username is your slug here and it must be a string because of where()... in the route declaration. If an integer is passed then route couldn't be found and 404 error will be thrown.
As of Laravel 5.2, if you use Route Model Binding, then you can make your routes that contain the object identifier as usual (Implicit Binding). For example:
In routes/web.php (Laravel 5.3) or app/Http/routes.php (Laravel 5.2):
Route::get('categories/{category}', 'CategoryController#show');
In your CategoryController:
show (Category $category) {
//
}
The only thing you need to do is telling Laravel to read the identifier from a different column like, for example, slug column, by customizing the key name in your eloquent model:
/**
* Get the route key for the model.
*
* #return string
*/
public function getRouteKeyName()
{
return 'slug';
}
Now, you can refer your url's that requires the object identifier with the slug identifier instead the id one.
See Laravel 5.3 (or 5.2) Route Model Biding
For future readers, as of Laravel 8.0, you can specify a column right in the path
Route::get('/users/{user:slug}', function (User $user) {
return $user->bio;
});
one question about Symfony2 and the routes.
In my routing file i have this route:
offers_category:
pattern: /offers/{category}
defaults: { _controller: MyBundle:Offers:category}
In this way i can call any url and all of them will respond with an 200 (HTTP CODE).
The categories list is dynamic, inserted by an admin panel (created with Sonata) and saved on db.
I would like to check if the "category" exist and then respond with 200 or 404.
There is a way to tell to Symfony2 (in the route file) the dictionary available for the placeholder?
I think that i have to check inside my controller calling a query on db, but i hope to find a better or cleaned solution.
Thanks all
I found the solution!
Thanks to #lenybernard for the precious advice on his post.
With the #lenybernard solution i was able to configure a route with an indexed field like:
www.sitename.com/offers/1
but i needed a url like:
www.sitename.com/offers/travel
and to do that i used this method to mapping a label not indexed to an url:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use NameProject\MyBundle\Entity\Category;
/**
* #Route("/offers/{category}")
* #ParamConverter("type", class="NameProjectMyBundle:Category", options={"mapping": {"category": "name"}})
*/
...and everthing works!
There is a simple and nice way called ParamConverter to do but there is nothing to do in the routing file directly (furthermore this is not its role) and you're right, this is ControllerSide :
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use My\Bundle\MyBundle\Entity\Offers\Category;
class OffersController
{
/**
* #param Category $category
* #ParamConverter("category", class="MyBundle:Offers/category")
*/
public function showAction(Category $category)
{
...
}
}
As said in the documentation, several things happen under the hood:
The converter tries to get a MyBundle:Offers/category object from the
request attributes (request attributes comes from route placeholders
-- here id); If no Category object is found, a 404 Response is generated; If a Category object is found, a new category request attribute is defined
(accessible via $request->attributes->get('category')); As for other
request attributes, it is automatically injected in the controller
when present in the method signature. If you use type hinting as in
the example above, you can even omit the #ParamConverter annotation
altogether because it's automatic :
So you just have to cast the variable and the param converter will throw a 404 automatically, cool right ?
use My\Bundle\MyBundle\Entity\Offers\Category;
class OffersController
{
/**
* #param Category $category
*/
public function showAction(Category $category)
{
...
}
}