Laravel Reusable Scope & Eager loading - php

I have three models: Restaurant, FoodItems, Taxes with the following relationships
Restaurant belongsToMany FoodItems (vice versa)
FoodItems belongsToMany Taxes (vice versa)
Restaurant hasMany Taxes (Taxes belongsTo Restaurant)
I have 2 routes
to show list of all menus in that restaurant /restaurant/{restaurantId}/menus
to show details of single menu item //restaurant/{restaurantId}/menus/{id}
The MenuController which handles the above is as follows
public function index(Restaurant $restaurant, Request $request)
{
$query = $restaurant->foodItems()->withRestaurantTaxes($restaurant);
$query->ofCategories($request->get('category'));
return $query->get();
}
public function show(Restaurant $restaurant, FoodItem $foodItem, Request $request)
{
$foodItem->load(['taxes' => function ($query) use ($restaurant) {
$query->ofRestaurant($restaurant->id);
}]);
return $foodItem;
}
Restaurant Model has the following relation
public function foodItems()
{
return $this->belongsToMany(FoodItem::class, 'restaurant_food_items');
}
Food Item Model has the following
public function taxes()
{
return $this->belongsToMany(Tax::class, 'food_item_taxes');
}
public function scopeWithRestaurantTaxes($query, $restaurant)
{
return $query->with(['taxes' => function ($query) use ($restaurant) {
$query->ofRestaurant($restaurant->id);
}]);
}
Tax model has the following
public function restaurants()
{
return $this->belongsTo(Restaurant::class);
}
public function scopeOfRestaurant($query, $restaurantId)
{
return $query->where('restaurant_id', $restaurantId);
}
I have 2 issues with my code,
In the controller's index method, withRestaurantTaxes() is a scope method that I have created, which is similar to the load constraint on show method. So is there a way to reuse the scope or restructure to avoid duplication.
In the controller's index method, the query builder is created from the restaurant model like this $restaurant->foodItems()->withRestaurantTaxes($restaurant), is there any way to avoid passing restaurant again as a parameter to the scope method.
Any help would be greatly appreciated.
Thank you.

I think you can do something like this:
public function scopeWithTaxes($query)
{
return $query->with(['taxes' => function ($query) {
$query->ofRestaurant($this->id);
}]);
}
But that will only work as long as you have a Restaurant instance.

Related

What would be the best approach to query eloquent relatioships where keys are on the same table

Considering this table
Services Table
id
company_id
category_id
1
2
4
2
4
6
And this model
CategoryModel.php
public function companies():Attribute
{
return new Attribute(
get: fn () => CompanyModel::whereHas('services', function ($q) {
$q->where('category_id', $this->id);
}),
);
}
Considering that the services table holds both company_id and category_id columns what would be the best approach to query companies relationship that have services under the current category (The companies table does not have a category_id column), my current implemetation is not optimal as it does not allow me to perform any relationship constrains.
EDIT
Each company offers multiple services and each service belongs to a single category.
I also have a reviews table (related to each service) with a rating column
The above query worked efficiently until I needed to constrain/order categories based on the reviews table.
CategoryModel.php
public function scopeHasReviews($query)
{
$query->whereHas('companies', fn ($q) => $q->whereHas('reviews'));
}
This ofcourse will not work since there is no relationship.
Using Attribute in this context is dangerous, because it can lead to N+1 problems. This is a usual many-to-many relationship, so it needs to be implemented in models:
Category.php
public function companies(): BelongsToMany
{
return $this->belongsToMany(Company::class, 'services');
}
Company.php
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class, 'services');
}
Further, since it is not clear exactly what problem must be solved - I will write an example with sorting by number of reviews:
Review.php
public function scopeWhereRawService(Builder $query, string $service): Builder
{
return $query->whereRaw('service_id = ' . $service);
}
Company.php
public function reviews(): HasManyThrough
{
return $this->hasManyThrough(Review::class, Service::class);
}
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class, 'services');
}
public function categoriesOrderedByReviews(): BelongsToMany
{
return $this->categories()->withCount([
'reviews as reviews_count' => fn(Builder $q) => $q->whereRawService('services.id')
])->orderByDesc('reviews_count');
}
Category.php
public function reviews(): HasManyThrough
{
return $this->hasManyThrough(Review::class, Service::class);
}
public function companies(): BelongsToMany
{
return $this->belongsToMany(Company::class, 'services');
}
public function companiesOrderedByReviews(): BelongsToMany
{
return $this->companies()->withCount([
'reviews as reviews_count' => fn(Builder $q) => $q->whereRawService('services.id')
])->orderByDesc('reviews_count');
}

Laravel return model with if statement

I'm trying to create offers and assign them to parent categories, to be more specific i have an Offer model and inside the offer model i have this many to many relationship
public function category() {
return $this->belongsToMany(Category::class);
}
I want the above function to return ONLY the categories which have NULL parent_category which mean they are the parent categories. Is it possible with the above code?
Without knowing the entire scope of your project, I'd suggest one of the following: either change the name of the relation (A) or keep the relation as is and query it when you need it (B).
Option A -
public function childCategory() {
return $this->belongsToMany(Category::class)->whereNull('parent_category');
}
Option B -
public function category() {
return $this->belongsToMany(Category::class);
}
$offer = Offer::with('category')
->whereHas('category' function ($query) {
$query->whereNull('parent_category');
});
public function category() {
return $this->belongsToMany(Category::class)->where('parent_category', null);
}

HasMany Deep Relationship

I have 5 models with one pivot table Country Province City Area Tour tour_location. How to achieve below functionality?
$country->tours
$province->tours
$city->tours
$area->tours
Country.php HasMany Provinces
public function provinces()
{
return $this->hasMany('App\Province', 'country_id', 'id');
}
Province.php HasMany Cities
public function cities()
{
return $this->hasMany('App\City', 'province_id', 'id');
}
City.php HasMany Areas
public function areas()
{
return $this->hasMany('App\Area', 'city_id', 'id');
}
Area.php BelongsToMany Tours
public function tours()
{
return $this->belongsToMany('App\Tour', 'tour_locations');
}
The direct way is do it with joins, another way is to make a custom relationship extending the hasManyThrough(). The third option -imo- is to use the Eloquent-has-many-deep package.
Using this package, you could do this:
class Country extends Model
{
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
public function tours()
{
return $this
->hasManyDeep('App\Tour', ['App\Province', 'App\City', 'App\Area', 'area_tour']);
}
}
Then in your controller:
// ...
$country = Country::find(1);
$tours = $country->tours;
Disclaimer: I'm not involved in this package in any way. I'm just suggesting it because is the simplest way to achieve your desired behavior.

How to get third relation in laravel

I have the following models: User, Device, Product.
User
public function devices()
{
return $this->hasMany('App\Device');
}
Device
public function user()
{
return $this->BelongsTo('App\User');
}
public function reading()
{
return $this->hasMany('App\Reading', 'device_id', 'part_id');
}
public function product()
{
return $this->hasOne('App\Product');
}
Product
public function device()
{
return $this->belongsTo('App\Device');
}
The following query pulls my users and all their devices, but inside that collection is not the relation from device to product.
$deviceSingles = User::with('devices')->get();
This query gets me all the products with all devices assigned to it
$deviceSinglesTwo = Product::with('device')->get();
How do I get that third relation, attached to my initial query so i can do this
$deviceSingles->device->product->title
Use nested eager loading.
To eager load nested relationships, you may use "dot" syntax.
User::with('devices.product')->get()

Laravel Eloquent for sale-product-customer relationship

So using Laravel 4, I have a Sales table that has a many to many relationship with a Products table, and it also has a one to many relation with a Customers table.
I set up my models as follows:
class Sale extends Eloquent {
...
public function products(){
return $this->belongsToMany('Product');
}
public function customers(){
return $this->belongsTo('Customer');
}
}
class Product extends Eloquent {
...
public function sales(){
return $this->belongsToMany('Sale');
}
}
class Customer extends Eloquent {
...
public function sales(){
return $this->hasMany('Sale');
}
}
What I want to do is return the data of all sales, including the data of each product included in each sale and the data of the customer that bought it.
In my SalesController I'm using eager loading to query my data like this:
public function index()
{
return Sale::with('products', 'customers')->get();
}
It returns an object with the Sale data, the Product data, but the Customer data is null.
How can I achieve this using Eloquent (or a custom query)?
EDIT
This is the object string it returns:
[{"id":1,"customer_id":1,"date":"2013-11-21","status":1,"created_at":"0000-00-00 00:00:00","updated_at":"0000-00-00 00:00:00","products":[{"id":1,"name":"Monitor","price":50,"status":1,"created_at":"0000-00-00 00:00:00","updated_at":"0000-00-00 00:00:00","pivot":{"sale_id":1,"product_id":1,"custom_price":25,"order":1}}],"customers":null}]
Try changing your customers relationship to singular:
class Sale extends Eloquent {
...
public function products(){
return $this->belongsToMany('Product');
}
public function customer(){ // <- here
return $this->belongsTo('Customer');
}
}
(Moved from comments to answer)

Categories