Laravel routes confusion with slugs - php

How can I handle 2 similar urls in my routes.php in Laravel Framework ?
Eg :
mysite/shoes (categories page)
mysite/adidas-model-1 (product pages)
Code :
#Categories Pages
Route::get('{catSlug}', array('uses' => 'CategoriesController#show'));
#Product Page
Route::get('{productSlug}', array('uses' => 'ProductController#show'));
If i browse to mysite/shoes show method in CategoriesController is fired, but if I browse to mysite/adidas-model-1 it's not show method of ProductController but the one of CategoriesController which is fired.
Is there a nice way to achieve this in routes.php file ? Or do I route all to CategoriesController#show and if object is not found fire the show method of ProductController ?
Thanks.

In the two routes you've shown, the router has no way of knowing when you're entering a catSlug and when you're entering a productSlug - they're both strings and there's no code there to distinguish them.
You can correct this by adding a where clause:
Route::get('{catSlug}', array('uses' => 'CategoriesController#show'))
->where('catSlug', '[A-Za-z]+');
Route::get('{productSlug}', array('uses' => 'ProductController#show'))
->where('productSlug', '[-A-Za-z0-9]+');
In the regular expressions above, I've assumed that categories are strings of upper and lower case letters only - no numbers, no spaces, no punctuation - and products include hyphens and numbers.
I should also add that the order of these declarations will be important. The product route also matches the category route, so the category route should be declared first, so it has a chance to fire. Otherwise, everything looks like a product.

Thanks for your answers.
I really need to be free of what I choose for my slugs. So i found an other solution.
# Objects (cats or products)
Route::get('{slug}', array('uses' => 'BaseController#route'));
and in my BaseController file :
public function route($slug)
{
// Category ?
if($categories = Categories::where('slug', '=', $slug)->first()){
return View::make('site/categories/swho', compact('categories'));
// Product ?
}elseif($products = Products::where('slug', '=', $slug)->first()){
return View::make('site/products/show', compact('products'));
}
}
I first test for a Category object (I have less categories than products) and if not found I test for a product.

Try to make it like this, this is how I use it.
Route::get('{slug}', function($slug) {
// IF IS CATEGORY...
if($category = Category::where('slug', '=', $slug)->first()):
return View::make('category')
->with('category', $category);
// IF IS PRODUCT ...
elseif($product = Product::where('slug', '=', $slug)->first()):
return View::make('product')
->with('product', $product);
// NOTHING? THEN ERROR
else:
App::abort(404);
endif;
});

Related

Return belonging name with ID form Laravel, check for the type?

sorry for the title of this question but I am not sure how to ask it...
I am working on a project where I have two Models Trains and Cars, to this model I have a belonging Route.
I want to make a query and check if the routeable_type is App\Car than with the selected routeable_id to get the data from the Car. And if the routeable_type is Train then with the ID to get the data from the Tran.
So my models go like this:
Train:
class Train extends Model
{
public function routes()
{
return $this->morphMany('App\Route', 'routeable');
}
}
Car:
class Car extends Model
{
public function routes()
{
return $this->morphMany('App\Route', 'routeable');
}
}
Route:
class Route extends Model
{
public function routeable()
{
return $this->morphTo();
}
}
And the query I have at the moment is:
$data = Route::leftjoin('cars', 'cars.id', '=', 'routes.routeable_id')
->leftjoin('trains', 'trains.id', '=', 'routes.routeable_id')
->select('routes.id', 'cars.model AS carmodel', 'trains.model AS trainmodel', 'routeable_type', 'routes.created_at');
With this query if I have the same ID in cars and trains I get the data from both and all messes up. How do I check if routeable_type is Car ... do this, if routeable_type is Train .. do that?
Will something like this be possible in a 1 single query:
$data = Route::select('routes.id', 'routeable_type', 'routes.created_at');
if(routeable_type == 'Car'){
$data = $data->leftjoin('cars', 'cars.id', '=', 'routes.routeable_id')->select('routes.id', 'cars.model AS carmodel', 'routeable_type', 'routes.created_at');
}else{
$data = $data->leftjoin('trains', 'trains.id', '=', 'routes.routeable_id')->select('routes.id', 'trains.model AS trainmodel', 'routeable_type', 'routes.created_at');
}
Maybe this is what you are looking for?
DB::table('routes')
->leftJoin('cars', function ($join) {
$join->on('cars.id', '=', 'routes.routeable_id')
->where('routes.routeable_type', 'App\Car');
})
->leftJoin('trains', function ($join) {
$join->on('trains.id', '=', 'routes.routeable_id')
->where('routes.routeable_type', 'App\Train');
})
->select('routes.id', 'cars.model AS car_model', 'trains.model AS train_model', 'routes.routeable_type', 'routes.created_at');
->get();
I think you may want to follow the morphedByMany design.
https://laravel.com/docs/5.7/eloquent-relationships#many-to-many-polymorphic-relations
This was also a neat visual for the different relation types.
https://hackernoon.com/eloquent-relationships-cheat-sheet-5155498c209
I was faced with a similar issue though I failed to follow the correct design initially and was forced to query the many possible relations then wrote custom logic after to collect the relation types and ids then do another query and assign them back through iteration. It was ugly but worked... very similar to how Eloquent does things normally.
i don't have enough repo, so i can't comment. that's why i am putting as an answer.
You should use 2 different queries, for each model.
This will be better, code wise as well as performance wise. also if both models have similar fields you should merge them to 1 table and add a 'type' column.
and put non-similar fields in a 'meta' column.
( in my opinion )

Laravel set Array in Route

Hello StackOverflow Community, after some research we decided to ask you for the solution.
We would like to have a link with a defined array. The goal would be to have a link such as:
www.testurl.com/restaurants/CUISINENAME/
Then, we would just like to see all restaurants with the particular cuisine. The filter is currently working on the website with a checkbox.
Router
Route::group(['prefix' => 'restaurants', 'namespace' => 'frontEnd', 'middleware'=>'checkzipcode'], function () {
Route::get('/', 'RestaurantController#showAllRestaurants');
Route::post('/', 'RestaurantController#showAllRestaurants');
});
Controller
if (request('cusineName')) {
if (is_array(request('cusineName'))) {
$cusineName = request('cusineName');
} else {
$cusineName = (explode(",", request('cusineName')));
}
$all_restaurant = $all_restaurant->whereIn('restaurant_cuisines.type_cuisine_id', $cusineName);
}
We were thinking of setting the array into the controller. Does anyone have any other suggestions?
Firstly, your array check is fine. However, you should return false if no cusineName has been passed through (As there is no point continuing).
You then are able to do an eloquent query for restaurants which have a particular cuisine by using the whereHas() method:
...
$restaurants = Restaurant::whereHas('cuisine', function($query) use ($cuisines) {
$query->whereIn('id', $cuisines);
})->get();
...
In the example, we pass through $cuisines to be used within the whereHas() eloquent method, to then use in the whereIn() method. This will check against the array if the cuisine's id is found.

Laravel Pagination with Get request

I've followed the instructions on the Laravel documentation for pagination with appends([]) however I'm having a little trouble with the persistence of these parameters.
Say for example, I pass home?category=Cars&make=Tesla to my view. What is the best way to paginate with them Get requests?
Right now I've passed the category as a parameter to the view as (where category is the model i've grabbed findOrFail with the request('category');)
$category_name = $category_model->name;
And then in my view it's like so:
{{ $vehicles->appends(['category' => $category_name])->links() }}
But when I go between pages in the pagination, this $category_name value doesn't seem to persist. Whats the recommended way to achieve what I want?
Thanks.
You can append the query string in your controller when you paginate the result. I'm not sure if that was your only question or even regarding applying the query string as a condition. So here is a sample showing you how to do both. This should give you an idea of how to do it. I just assumed the column names in this example.
$category = request('category');
$make = request('make');
$vehicles = Vehicle::when($category, function ($query) use ($category) {
return $query->where('category', $category);
})
->when($make, function ($query) use ($make) {
return $query->where('make', $make);
})
->paginate(10);
$vehicles->appends(request()->query());
return view('someview', compact('vehicles'));

Route precedence order

I have 2 routes with their methods written in same controller[LinkController]:
Route::get('/{country}/{category}', ['as' => 'tour.list', 'uses' => 'LinkController#tourlist']);
Route::get('/{category}/{slug}',['as' => 'single.tour', 'uses' => 'LinkController#singleTour']);
And my methods are:
public function tourlist($country, $category)
{
$tour = Tour::whereHas('category', function($q) use($category) {
$q->where('name','=', $category);
})
->whereHas('country', function($r) use($country) {
$r->where('name','=', $country);
})
->get();
return view('public.tours.list')->withTours($tour);
}
public function singleTour($slug,$category)
{
$tour = Tour::where('slug','=', $slug)
->whereHas('category', function($r) use($category) {
$r->where('name','=', $category);
})
->first();
return view('public.tours.show')->withTour($tour);
}
My code in view is:
{{$tour->title}}
The trouble i am having is the second route [single.tour] returns the view of the first route [tour.list]. I tried to return other view also in 2nd method but still it returns the view of first method. Does laravel have routing precedence ?
This is happening because, Laravel matches routes from the file, and the route which comes earlier and matches the pattern will execute first, you can use regex pattern technique to avoid this like:
Route::get('user/{name}', function ($name) {
//
})->where('name', '[A-Za-z]+'); // <------ define your regex here to differ the routes
See Laravel Routing Docs
Hope this helps!
Both your routes consist of two parameters in the same place. That means any url that matches route 1 will also match route 2. No matter in what order you put them in your routes definition, all requests will always go to the same route.
To avoid that you can specify restrictions on the parameters using regular expressions. For example, the country parameter may only accept two letter country codes, or the category parameter may have to be a numeric id.
Route::get('/{country}/{category}')
->where('country', '[A-Z]{2}')
->where('category', '[0-9]+');
https://laravel.com/docs/5.3/routing#parameters-regular-expression-constraints

How to represent averages and other aggregates in OOP?

I have a Model called Product. Product has among other things a price, a category and a created date.
I want to show a table showing the average price and average age by category in many places. Sometimes I want just one category, sometimes everything, sometimes only products that have been in stock for more than a certain time etc.
At the moment I'm using Laravel's Query Builder to generate those numbers in my controller, then passing that to a view.
To help try to reuse it, I have this in the methods before I need it:
$product_averages_base_query = DB::table('products')
->leftJoin('categories', 'products.MakeDescription', '=', 'categories.id')
->select(
DB::raw('count(products.id) as TotalNumber'),
DB::raw('AVG(Datediff("'.date('Y-m-d').'",products.created)) as AvgDaysInStock'),
DB::raw('AVG(Price) as AvgPrice')
);
Then when for the specific use case I use:
$averages = $product_averages_base_query->where('categories.name', '=', 'Example 1')->get();
or whatever the variant is.
This feels really "wrong" because I of course end up copying this code all over the place.
How do I represent this data in a way that will let me reuse it more easily? Should I have a class? What should it be called, and what's in it?
Should I have a Model somehow?
Any advice is welcome!
As for the appropriate place, you could very easily use query scopes and drop everything in your model. You'd probably want to start with your base query...
public function scopeBaseQuery($query)
{
return $query->leftJoin('categories', 'products.MakeDescription', '=', 'categories.id')
->select(
DB::raw('count(products.id) as TotalNumber'),
DB::raw('AVG(Datediff("'.date('Y-m-d').'",products.created)) as AvgDaysInStock'),
DB::raw('AVG(Price) as AvgPrice')
);
}
And then continue with the category name scope...
public function scopeOfName($query, $name)
{
return $query->where('categories.name', $name);
}
Add additional scopes as you need.
Then to use this, it would be quite easy...
$averages = Product::baseQuery()->ofName('Example 1')->get();
One solution would be to create an Eloquent class for the model and then put the functionality into a scope.
class Product extends Eloquent {
public function scopeAveragesByCategoryName($q, $catName) {
return $q->leftJoin('categories', 'products.MakeDescription', '=', 'categories.id')
->select(DB::raw('count(products.id) as TotalNumber'),
DB::raw('AVG(Datediff("'.date('Y-m-d').'",products.created)) as AvgDaysInStock'),
DB::raw('AVG(Price) as AvgPrice'))
->where('categories.name', '=', $catName);
}
}
$products = Product::averagesByCategoryName('Example 1')->get();
var_dump($products);
Or you could just as easily put the whole thing into a function.

Categories