Merging two models into one database call - laravel - php

I have 2 models, that I am calling via all:
$blocks = Block::all();
$cats = BlockCategory::all();
A block has a category associated with it and I want to associate the data so that I can display all categories that have certain blocks. I hope I am explaining this correctly.
so I can call:
#foreach($cats as $cats)
{{$cats->title}}
#foreach($cats->blocks as $blocks)
{{$block->title}}
#endforeach
#endforeach
any ideas how to merge the data? I am basically building a menu if that helps.

You need to define relationship between blocks and block category. Make sure that your db table has a foreign key relationship.
Add following code to your block category model
public function blocks()
{
return $this->hasMany('App\Block');
}
Make sure your block model is inside app/ directory.
Now, just retrieve categories in your controller,
$categories = BlockCategory::all();
Then you can display the data like this:
#foreach($categories as $category)
{{$category->title}}
#foreach($category->blocks as $block)
{{$block->title}}
#endforeach
#endforeach

You have a few options on how to approach this.
Define your relation like this:
Model
public function blocks()
{
return $this->hasMany('App\Block');
}
Controller
Using with()
$categories = Blockcategory::with('blocks')->get();
Once you have defined the relationship you can call the blocks() method from your model to retrieve the blocks
Examples
$categories = Blockcategory::find(1)->blocks()->get();
// another way
$blocks = Blockcategory::blocks()->get();
Sagar's answer is fine, but it is always better to get your categories and their relationships in the same query. By just calling Blockcategory::all() you can get the relation in your blade file, but laravel has to do an additional query for each block in your for each loop. This can give performance issues when having 1000s of records.
Laravel relationships

Related

Laravel Eloquent ORM One-to-Many Relationship, Select from two Tables

I have two mySQL tables of an online shop:
products (cols: id, etc...)
pictures (cols: id, product_id, filepath, etc...)
Any product of the products-table may have an infinite number of pictures!
Therefore, my product-model looks like this:
class Product extends Model
{
public function pictures()
{
return $this->hasMany('App\Picture');
}
}
My picture model looks like this:
class Picture extends Model
{
//
}
Now, I'am trying to list all products together with their corresponding picture datasets. I tried running multiple codes. However, none of the following codes works:
\App\Product::pictures()->get()
doesn't work! This error is shown:
Non-static method App\Product::pictures() should not be called statically
I also tried some other methods, e.g.
\App\Product::with('pictures')->get());
Error:
Trying to get property 'filepath' of non-object (View: /home/vagrant/testproject/resources/views/products.blade.php)
Remark: Filepath is a column of pictures. I am trying to access it in the blade like this:
{{$products->picture->filepath}}
I am now working for hours on this tiny thing - but I just don't get it what I am doing wrong? Any help is highly appreciated! Thank you guys!
There are two potential issues with what you are doing.
You are using {{$products->picture->filepath}} so I assume you are doing soething like:
$products = \App\Product::with('pictures')->get());
Here $products is a collection of Product models. The collection does not have a picture attribute.
You are looping over the products something like
#foreach($products as $product)
{{$product->picture->filepath}}
#endforeach
Unless EVERY product has a picture, this can result in the error you stated. If the product does not have a picture, then you will be calling filepath on null. You can address this in several ways, I will list one below:
#foreach($products as $product)
#if(!empty($product->picture))
{{$product->picture->filepath}}
#endif
#endforeach
As OP noted in comments below the root issue is that the relationship is has many. So to use pictures you should do:
#foreach($products as $product)
#foreach($product->pictures as $picture)
{{$picture->filepath}}
#endforeach
#endforeach

Eloquent Eager Loading "Property [service_code] does not exist on this collection instance"

I cannot figure out how eager loading works with the following example. This is my current DB with two tables, Quotes: where the general information is stored, and QuotesDetails: where details of each Quote in Quotes is stored.
In models I have the following structures:
Quotes.php
class Quotes extends Model
{
public function quotesdetails()
{
return $this->hasMany('App\QuotesDetails', 'quoteid', 'id');
}
}
and QuotesDetails.php with the following model:
class QuotesDetails extends Model
{
public function quotes()
{
return $this->belongsTo('App\Quotes', 'id', 'quoteid');
}
}
I used hasMany (in Quotes) because each quote can have/display 3-4 quotesdetails.
In my controller im using the following query:
$returnquotes = Quotes::with('quotesdetails')->where('id', '=', $quoteid)->get();
In my view im using the following structure:
#foreach ($returnquotes as $quotes)
{{$quotes->shipcity }}
{{$quotes->quotesdetails->service_code }}
#endforeach
shipcity displays the information with no problems, but service_code is not displayed and gives error.
Honestly I believe it has to work with this schema but I cannot figure out why is not working. My thoughts:
in controller: using "with('quotesdetails')" in controller it must establish the relation that appears in Quotes.php with name -> quotedetails that hasmany registries associated in QuotesDetails.php
in view: using "$quotes->quotesdetails->service_code" must retrieve the service_code associated to the quotes table (i'm using foreach because quotesdetails can have multiple registries per quote)
in model: Im using "hasMany" for Quotes.php because the dependent table is QuotesDetails and "belongsTo" in QuotesDetails.php for the inverse reason.
any help to understand the logic of eager loading with eloquent and laravel appreciated.
$quotes->quotesdetails is a collection itself (has many) so you need to iterate over it using another foreach:
#foreach ($returnquotes as $quotes)
{{$quotes->shipcity }}
#foreach ($quotes->quotesdetails as $quotedetail)
{{$quotedetail->service_code }}
#endforeach
#endforeach

Laravel retrieves too many model on a simple paginate query with loaded relations

I have a simple database with couple of tables. orders - the main table, about 16_000 records. Each order has payments. The order_payments table is about 17_000 records. Also each could have zero or more travellers in a order_tourists table - 33_000 rows.
The Order model looks like this
namespace App;
class Order extends \Illuminate\Database\Eloquent\Model
{
public function payments()
{
return $this->hasMany('App\OrderPayment');
}
public function tourists()
{
return $this->hasMany('App\OrderTourist');
}
}
I have a simple task to display a paginated list of all orders. Each item in this list should display some order's information (from the orders table), the sum of all payments and names of all travellers.
The first that came to my mind was something like this.
Get all orders with payments and travellers and pass this data to a view
Route::get('search_offers', static function () {
$orders = \App\Order::query()->with(['payments', 'tourists']);
return view('orders', [
'orders' => $orders->paginate(),
]);
});
And my orders.blade.php is pretty straightforward
<ol>
#foreach($orders as $order)
<li>
{{$order->request_id}}; {{ $order->payments->sum('amount') }}; {{ $order->tourists->implode('fullName', ', ') }}
</li>
#endforeach
</ol>
However, even the number of queries is very small I have a lot of manipulations with models. I add a custom hook to track all eloquent.* events and in first case there are more than 23_000 of such events.
Then I try to remove the loading of relations so for each order Laravel have to run a separate query to get all order's payments and travellers. I simply did like this in my controller
return view('orders', [
'orders' => \App\Order::query()->paginate(),
]);
So, I get more queries but page speed improves and a number of affected models significantly decreased
To track affected models I add a listner in a AppServiceProvider::boot method and output a AppServiceProvider::$hydratedModels value in a debugbar panel
Event::listen('eloquent.*', static function ($event) {
if (strpos($event, 'eloquent.retrieved') !== false) {
AppServiceProvider::$hydratedModels++;
}
});
So, my question how it possible that so many models are retrieved even if I display only 15 items. Looks like Laravel somehow get all of them, processed but retruns only those for a required page.
Right now it's not a big deal to run extra queries to fetch some data but I'm wondering maybe I'm doing something wrong and it's possible to load relations but do not cause Laravel to retrieve all models.
I found the issue. Thanks, #dparoli for a suggestion.
My models have a primary key as UUID. But I did not change a Model::$keyType so internally Laravel cast it into an integer and queries look like this.
Instead of a UUID string, I got integers as an order_id for related records.
So, I add protected $keyType = 'string'; and everything starts work as expected.
PS. Although, I'm wondering how it works before.

Access relation of pivot table in Laravel

I have three models Bill, Product and Process. Bill has a ManyToMany relationship with Product and the pivot table have some extra fields. I write the Bill model class like follow:
<?php
class Bill extends Model
{
function products(){
return $this->belongsToMany(\App\Product::class)
->withPivot('process_id') // the id of the Process
->withTimestamps();
}
}
The Process model is a simple table with an id and a name. I am associating the id in the pivot table for reference the Process, the name could change over time but still referring the same concept so I can't associate the name.
The show page for a single Bill lists the products associated in a table like follow:
#foreach($bill->products as $product)
<tr>
<td>{{$product->barcode}}</td>
<td>{{$product->pivot->process_id}}</td>
</tr>
#endforeach
So the problem is that I need the name of the process, but I have the id. I'm not sure how I could get the name.
Thanks
I think you can use an own Pivot Model, e.g. ProductBill in order to achieve this.
class ProductBill extends Pivot {
public function process() {
return $this->belongsTo(Process::class);
}
}
By using this model in your products relation on Bill
class Bill extends Model {
function products() {
return $this->belongsToMany(\App\Product::class)
->withPivot('process_id')
->using(ProductBill::class)
->withTimestamps();
}
}
When accessing $product->pivot you should get an instance of ProductBill now, hence you should be able to do the following:
<td>{{$product->pivot->process->name}}</td>
(Unfortunatelly I not able to doublecheck right now :/)
Without having a direct relation to Process you will likely need to add a helper on your Product model to get the name of Process.
In your Product model:
public function processName($processId) {
return Process::where('id', $processId)->pluck('name')->first();
}
In your view:
<td>{{$product->processName($product->pivot->process_id) }}</td>
There may be a better way, but concept this should work.
I know it's not the most elegant of solutions. but you could always simply do:
Process::find($product->pivot->process_id)->name;
I wouldn't advice this though as you are looping through an array already, so the overheads to doing something like this would be quite large.
Another solution would be to create a Pivot class called say BillProductPivot which would have a relationship to return both Product and Process, then when you call them you should be using eager loading to get the relationships. end product might look like this:
$bill->load('productPivots', 'productPivot.process', 'productPivot.product');
#foreach($bill->productPivots as $productPivot)
<tr>
<td>{{$productPivot->product->barcode}}</td>
<td>{{$productPivot->process->name}}</td>
</tr>
#endforeach

Eliminating Duplicate Queries with Eager Loading

I am trying to eliminate unnecessary queries on my site but am struggling to wrap my head around Eager Loading and Lazy Loading. All users on my site have listings, and listings have multiple users. They are connected through the table listing_users. Every listing then has one "order" associated with it. Here is the user model:
User Model:
public function listings(){
return $this->belongsToMany(Listing::class)->withPivot('role_id');
}
Listing Model:
public function order(){
return $this->hasOne(Order::class)->first();
}
My current dashboard is loaded by calling this viewListings in the UserController:
public function viewListings(){
$user = Auth::user();
$listings = $user->listings()->orderBy('created_at','desc')->get();
return view('user.listings', compact('listings'));
}
The problem occurs in my blade view user.listings where I have a foreach loop for every listing and then call each order as well. I need a way to pass the listings to the page, with their related orders.
#foreach($listings as $listing)
#if($listing->order()->status == 'completed')
{{-- Display the listing details here --}}
#endif
#endforeach
Any advice into the above situation would be greatly appreciated! I'm sure there is a simple Laravel solution for this that I'm overlooking.
Try this:
Listing Model:
public function order(){
return $this->hasOne(Order::class); //without first()
}
UserController:
Here we use method with('order') for eager loading Order model for each Listing models retrieved by query. So now in your blade will not be unnecessary queries.
When accessing Eloquent relationships as properties, the relationship
data is "lazy loaded". This means the relationship data is not
actually loaded until you first access the property. However, Eloquent
can "eager load" relationships at the time you query the parent model.
public function viewListings(){
$user = Auth::user();
$listings = $user->listings()->orderBy('created_at','desc')
->with('order')->get();//added with('order')
return view('user.listings', compact('listings'));
}
user.listings:
You should use order without () if you need retrieve your model. So if you want modify order then use it with () as query builder, and add further constraints like where, orderBy etc. and in the end add first().
Here you can understand why we removed first() from hasOne above.
#foreach($listings as $listing)
#if($listing->order->status == 'completed')
{{-- order instead of order() --}}
{{-- Display the listing details here --}}
#endif
#endforeach

Categories