I'm building my first Laravel app. I have posts that belongs to different categories.
I'm trying to make SEO friendly urls /categories/category_name for categories without saving a 'slug' in database.
Here is what I came with so far:
My category model:
class Category extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
public function getSlugAttribute(): string
{
return str_slug($this->name);
}
public function getUrlAttribute(): string
{
return action('CategoriesController#index', [$this->id, $this->slug]);
}
}
My category controller
class CategoriesController extends Controller
{
public function index($id, $slug = '')
{
$posts = Category::findOrFail($id)->posts;
return view('index', compact('posts'));
}
}
My route:
Route::get('/categories/{slug}', 'CategoriesController#index')->name('category');
With {{ $post->category->url }} I get something like http://localhost/category/1?alpha but it works and display the appropriate posts. Route is returning an error.
How I get ride of the id? and make the route works?
Thank you in advance for your help!
I'm assuming that you have a human readable category name (ex My Category) and you are converting that to a slug in your URLs (ex my-category).
A naive approach to doing this would be to attempt to reverse the slug and lookup the category by name, but there are use-cases where this won't work.
class CategoriesController extends Controller
{
public function index($slug)
{
$name = ucwords(str_replace('-', ' ', $slug));
$posts = Category::where('name', '=', $name)->first()->posts;
return view('index', compact('posts'));
}
}
The problem with that approach is there are cases where information is lost when converting the name to a slug, for example if the proper category name contains a hyphen, the hyphen becomes a space when converting the resulting slug.
You can approach this a different way, without storing the slug, by modifying your route:
Route::get('/categories/{category}/{slug}', 'CategoriesController#index')->name('category');
In this case, you pass both the ID and the slug in the URL. You can either fetch the category from the ID or use route model binding to fetch it automatically. This technically means you can pass any string as the slug and it will still work. You can handle this by determining the actual slug and redirecting to the canonical URL if the end user provides an incorrect slug.
class CategoriesController extends Controller
{
public function index(Category $category, $slug)
{
if(str_slug($category->name) != $slug) {
// Redirect to the canonical URL
}
$posts = $category->posts;
return view('index', compact('posts'));
}
}
A benefit of keeping both the ID and the slug in the URL is you get redirection for free: If the category name changes, the new URL will work and the old URL will automatically redirect to the new one.
Side note: If you look the URL for his question you'll notice this is the approach StackOverflow uses.
I don't think this is possible. The reason behind is that you will need to search for the category using the slug but is not stored in the database, so you will need to convert the slug to its original value.
For example, if you have a category named "Modern art", the slug would look something like modern-art. If you pass that slug to your controller, you need to convert it back to "Modern art" to be able to retrieve the category by its name. But maybe the real name was "Modern Art" or "Modern art!" or something else.
I recommend you storing the slug in the database.
Related
I want to change the naming convention of the following url :
http://example.org/designs/CV%20Designs
To be the following:
http://example.org/designs/cv-designs
Here is my web.php route file:
Route::get('designs/{design}', 'DesignsController#show')
->name('designs.show');
And here is my Design Model:
public function getRouteKeyName()
{
$slug = Str::slug('Laravel 5 Framework', '-');
dd($slug);
return 'designName';
}
When I dd(slug); the output is 'Laravel 5 Framework' but I want it to be designName
Ok, so I am going to make some assumptions here, but lets say you have this function on your Design model:
Design.php
class Design extends Model
{
...
/**
* Assuming you dont have a slug column on your designs table
*
* Also assuming the slug is built from a column called 'name' on
* your designs table
*/
public function getSlugAttribute()
{
return \Illuminate\Support\Str::slug($this->name);
}
// This assumes there is a column on posts table of 'design_id'
public function posts()
return $this->hasMany(Post::class);
}
...
}
Now let's make an example of how you can build the desired route.
EDIT
Through further discussions with the asker, they wan't to show all Posts related to the Design they are showing (See model above). The setup in this answer is suitable for that, and you can refer to the show method defined below.
Assume we have DesignsController.php:
class DesignsController extends Controller
{
...
public function index()
{
return view('designs.index', [
'designs' => Design::all(),
]);
}
public function show(Request $request, string $design)
{
// It is worth noting here if name has a double space you wont
// be able to build backwards to it for a query
// ie) Design\s\sOne !== $lower(Design\sOne)\
$spaced = str_replace('-', ' ', $design);
$lower = strtolower($spaced);
$design = Design::with('posts')->whereRaw("LOWER(name) = '$lower'")->first();
return view('designs.show', [
'design' => $design,
]);
}
...
}
Now in the 'designs/index.blade.php' file you could do something like:
#foreach($designs as $design)
{{ $design->name }}
#endforeach
That would list all of your designs by name, linked to the designs.show route by their slug.
If you would always like the slug value to be loaded when serializing to an array or json, you can add it to the protected $appends array on the model.
If you don't ALWAYS want it appended, you need to append it at run time using for example $design->append('slug').
Or if you have a collection of Designs you can do $designs->each->append('slug').
Now in your designs.show blade file you can, access the Design's posts using the relation we loaded using Design::with('posts'), by doing the following:
#foreach ($design->posts as $post)
<img src="{{ asset('storage/'.$post->postImage) }}">
#endforeach
I have a project on Laravel 5.8. It's internet-market with categories, brands and products. I used in my controllers a variable from model:
route:
Route::prefix('categories')->get('/{category}', 'ProductsController#openCategory')->name('openCategory');
model:
class Category extends Model
{
public function products()
{
return $this->hasMany(Product::class);
}
public function getRouteKeyName()
{
return 'category_alias';
}
}
controller-method:
public function openCategory(Category $category = null)
{
$allInfo = $this->getAllInfo();
$categories = $this->getCategories();
$brands = $this->getBrands();
return view("pages.category", compact('allInfo','category', 'categories', 'brands'));
}
I don't use relation by ID, I use relation by 'category_alias'. But if I write category name with error, I get message - OPPS! We Couldn’t Find this Page
Uh... So it looks like you brock something. The page you are looking for has up and Vanished. Why? But if I use relation by ID, I get page 404 - it's success for me.
By defining the route with ->get('/{category}' and typehinting on the controller, Category $category you are telling Laravel that you want it to resolve the category for you.
This is handled by the service container as described at https://laravel.com/docs/master/container#introduction
If you provide a value which doesn't resolve to the id for a category in the database, the service container will be be unable to load anything. Laravel responds to those scenarios by returning the 404.
I want a URI like this
http://localhost:8000/category/1/3
The first id is Category_id and second is Food_id.
My route is:
Route::get('category/{Category_id?}/{Food_id?}', 'DetailsController#categ');
And in Controller I have:
public function categ($Category_id,$Food_id)
{
$category = Categories::with('food')->findOrFail($Category_id);
$food = Food::with('restaurant','categories')->findOrFail($Food_id);
return view('category', compact('category','food'));
}
But it gives error Missing argument 2 for App\Http\Controllers\Detailscontroller::categ().Can anyone tell where is the problem.I am new to laravel.What I want to do is first shows food items based on category_id and then shows the deatails of foods according to food_id.
For showing relevant category of foods,in my view I have
#foreach ($Category as $categories)
{{$categories->CategoryName}}
#endforeach
and it shows me food items.Then I want when I click on any food item it shows me detail based on food_id. so my nxt view look like:
#foreach ($category->food as $food)
{{ $food->FoodName }}
#endforeach
The comment Anish left was correct, however, you main problem will come when you're trying to find models with null. To get around this you could have something like:
public function categ($Category_id,$Food_id)
{
$category = is_null($Category_id) ? []: Categories::with('food')->findOrFail($Category_id);
$food = is_null($Food_id) ? [] : Food::with('restaurant','categories')->findOrFail($Food_id);
return view('category', compact('category','food'));
}
NB They may be more errors in your view file depending on if you're trying to access.
However, I would go with a much more RESTful approach: https://laravel.com/docs/5.2/controllers#restful-resource-controllers
Essentially, this would mean having a controller for you Categories:
public function index() {
//Code to get all categories (if you have a lot you may want to paginate them)
}
public function show($Category_Id) {
$category = Categories::with('food')->findOrFail($Category_id);
//etc
}
and then a controller for you Foods with just a show() method:
public function show($Food_Id) {
$food = Food::with('restaurant','categories')->findOrFail($Food_id);
}
OR depending on how you set your route up you could also include the category as well if you need to (but if it's just a one2Many relationship it might be redundant) so you would have
public function show($category_ID, $Food_Id) //etc
Your routes would then be set up like this:
Route::get('categories', 'CategoriesController#index');
Route::get('categories/{$category_id}', 'CategoriesController#show');
//Assuming you go with the first option - something like:
Route::get('foods/{$food_id}', 'FoodsController#show');
//Assuming you go with the section option for Foods
Route::get('categories/{$category_id}/{$food_id}', 'FoodsController#show');
Obviously, the above is just an example so feel free to set you controllers/routes up how you like.
If you do end up going down the RESTful route (recommended) you then might want to look at: https://laravel.com/docs/5.2/routing#route-model-binding
Hope this help!
I declared resource route like below
Route::resource('{slug}/user','ManageUsersController');
This gives me a route
{slug}/user/{id}
where slug is the slug of a company name and id is the specific id of user.
In view, I declared anchor tag : <a href=user/{!! $user->id!!}>User</a>
This directs me to the function show in ManageUserController
public function show($id)
{
return $id;
}
But the return result is the slug, not the the id of the user. My URL is project/company-name/user/5
Where did i go wrong ? how can i get the user ID in return rather than the slug ?
-thanx
you have to catch both slug and id in your method,so your method will like
public function show($slug, $id)
{
//for slug
return $slug;
//for id
return $id;
}
Pass both parameters to your controller method, because Laravel will consider the first parameter to be the first parameter in the route (which would be your slug).
I'm implementing relationships in Eloquent, and I'm facing the following problem:
An article can have many followers (users), and a user can follow many articles (by follow I mean, the users get notifications when a followed article is updated).
Defining such a relationship is easy:
class User extends Eloquent {
public function followedArticles()
{
return $this->belongsToMany('Article', 'article_followers');
}
}
also
class Article extends Eloquent {
public function followers()
{
return $this->belongsToMany('User', 'article_followers');
}
}
Now, when listing articles I want to show an extra information about each article: if the current user is or is not following it.
So for each article I would have:
article_id
title
content
etc.
is_following (extra field)
What I am doing now is this:
$articles = Article::with(array(
'followers' => function($query) use ($userId) {
$query->where('article_followers.user_id', '=', $userId);
}
)
);
This way I have an extra field for each article: 'followers` containing an array with a single user, if the user is following the article, or an empty array if he is not following it.
In my controller I can process this data to have the form I want, but I feel this kind of a hack.
I would love to have a simple is_following field with a boolean (whether the user following the article).
Is there a simple way of doing this?
One way of doing this would be to create an accessor for the custom field:
class Article extends Eloquent {
protected $appends = array('is_following');
public function followers()
{
return $this->belongsToMany('User', 'article_followers');
}
public function getIsFollowingAttribute() {
// Insert code here to determine if the
// current instance is related to the current user
}
}
What this will do is create a new field named 'is_following' which will automatically be added to the returned json object or model.
The code to determine whether or not the currently logged in user is following the article would depend upon your application.
Something like this should work:
return $this->followers()->contains($user->id);