Eloquent Relationship in Loop - php

I'm building a Laravel frontend to an existing database (an ERP system named Epicor) with a view to extending that functionality in a separate (new) database. At the moment I am trying to display pieces of "equipment" that have a status of being shipped to a customer, and include information from the Part table. The DB relationships are all there and I can get all the information I need using SSMS - so I believe I must be going wrong in my use of Eloquent. I have the following models:
Equipment - this is a serial number in the system, so in effect an instance of a part:
<?php
class Equipment extends Model
{
protected $table = 'ERP.SerialNo';
public $timestamps = false;
protected $primaryKey = 'SerialNumber';
protected $keyType = 'string';
protected $fillable = [
'SerialNumber',
'SNStatus',
'PartNum',
'TerritoryID',
'JobNum',
'PackNum',
'PackLine',
'RMANum',
'CustNum',
'SNStatus'
];
public function Part()
{
return $this->belongsTo(Part::class,'PartNum','PartNum');
}
public function Customer()
{
return $this->belongsTo(Customer::class,'CustNum', 'CustNum');
}
}
Part
class Part extends Model
{
protected $table = 'ERP.Part';
public $timestamps = false;
protected $primaryKey = 'PartNum';
protected $keyType = 'string';
protected $fillable = [
'PartNum',
'SearchWord',
'Innactive',
'PartDescription',
'ClassID',
'CommodityCode',
'NetWeight'
];
public function ShipmentLine()
{
return $this->hasMany(Shipment::class, 'PartNum', 'PartNum');
}
public function Equipment()
{
return $this->hasMany(Equipment::class,'PartNum', 'PartNum');
}
}
Customer Controller
public function show($CustID)
{
$Customer = Customer::find($CustID);
$Shipments = $Customer->Shipment->where('Voided', '0');
$Equipments = $Customer->Equipment->where('SNStatus', 'SHIPPED');
return view('Customer.show', compact('Equipments', 'Customer','Shipments', 'Parts'));
}
show.blade.php (under Customer)
<?php
#foreach($Equipments as $Equipment)
<tr>
<td>ClassID</td>
<td>{{$Equipment->PartNum}}</td>
<td>{{$Equipment->SerialNumber}}</td>
<td>PartDescription is sometimes really really really long.....even longer than this!</td>
</tr>
#endforeach
Which all works fine and I get a list of all of the Equipment that has a status of being shipped to that customer. What I'd like to do now is, in the list of equipment, including fields from the Part table that relate (ClassID and PartDescription).
I've tried a few things, but feel I'm clutching at straws and all of my attempts fail. I have managed to display on Equipment show.blade.php Part information, so I believe the models are set up OK.
Thanks in advance,
Richard

First of all, the relations methods inside the Part model (as well as inside the Customer model) must be written at plural, since you are matching multiple entities:
public function ShipmentLines()
{
return $this->hasMany(Shipment::class, 'PartNum', 'PartNum');
}
public function Equipments()
{
return $this->hasMany(Equipment::class,'PartNum', 'PartNum');
}
Second, you can use the relation to load the equipments in the controller, instead of using lazy loading:
public function show($CustID)
{
$Customer = Customer::find($CustID);
$Shipments = $Customer->ShipmentLines()
->where('Voided', '0')
->get();
$Equipments = $Customer->Equipments()
->with('Part') // load the Part too in a single query
->where('SNStatus', 'SHIPPED')
->get();
return view('Customer.show', compact('Equipments', 'Customer', 'Shipments'));
}
Finally, in the blade template, you can use the Part of the equipment very easy:
#foreach ($Equipments as $Equipment)
<tr>
<td>{{$Equipment->Part->ClassID}}</td>
<td>{{$Equipment->PartNum}}</td>
<td>{{$Equipment->SerialNumber}}</td>
<td>PartDescription is sometimes really really really long.....even longer than this!</td>
</tr>
#endforeach
Also, I would recommend using #forelse instead of #foreach to cover those situations when no equipments exists:
#forelse ($Equipments as $Equipment)
<tr>
<td>{{$Equipment->Part->ClassID}}</td>
<td>{{$Equipment->PartNum}}</td>
<td>{{$Equipment->SerialNumber}}</td>
<td>PartDescription is sometimes really really really long.....even longer than this!</td>
</tr>
#empty
<tr>
<td colspan="4">There is no existing equipment!</td>
</tr>
#endforelse

I think what you're looking for is with().
Before I get to that though, you actually have a bigger problem there than it seems. Matei Mihai actually touched on this.
When you have something like $Customer->Equipment, you're actually making use of Eloquent's "dynamic properties". What this means is, there's a magic __get() in there somewhere that says if the desired property doesn't exist on the target model, check to see if it has a relation method by that name. And if so, lazy-load it if it hasn't already been eager-loaded via with() or load().
So when you do $Customer->Equipment, it's basically a shortcut for $Customer->Equipment()->get().
Next thing to consider is that the result of get() is an Eloquent\Collection, which is a child-class to Support\Collections. And Support\Collections have their own version of the where() method.
All that to say, $Customer->Equipment->where('SNStatus', 'SHIPPED') does not result in running a query that looks like:
SELECT * FROM Equipment WHERE customerID = ? AND SNStatus = 'SHIPPED'
What you're doing is running this instead:
SELECT * FROM Equipment WHERE customerID = ?
And then asking the Collection class to filter the resulting set by SNStatus='SHIPPED' afterwards. This can be a huge performance hit and even max out your servers RAM depending on how big those tables are. I think what you're really looking for there is this:
$Customer->Equipment()->where('SNStatus', 'SHIPPED')->get()
By calling on the actual Equipment() method rather than the dynamic property, you're telling Eloquent that you're not quite ready for it to execute the query yet, because you're still appending conditions to it.
(Also just as a side-note, your naming-convention hurts my OCD a little bit, methods should always be "camelCased". Only class names have their first letter capitalized.)
So... back to the question you actually asked, and including an understanding of the difference between Model::where() and Collection::where(), what we have is something like this:
$resutls = $Customer->Equipment()->with(['Part'])->where('SNStatus', 'SHIPPED')->get();
Since you wanted to specify a couple fields within the Parts table that you actually care about, you can use a constrained eager-load
$resutls = $Customer->Equipment()->with(['Part' => function (Illuminate\Database\Eloquent\Builder $query) {
$query->select([
'PartNum', //Per Equipment::Part(), This needs to be there for the relation to be mated with its parent
'ClassID',
'PartDescription'
]);
// Since PHP always handles objects by-reference, you don't actually need to return $query after having altered it here.
}])->where('SNStatus', 'SHIPPED')->get();
This will give you a nested Part object with just the fields you care about on each Equipment model element within the Eloquent\Collection results.
As for how to handle these results within your blade file, I'll differ to Matei Mihai on that, I think that answer is pretty good.

Related

I'm getting an error when trying to save a new model with 2 fields referencing different ids in the same table

I'm new to laravel, and I've picked up the basic workflow of creating, updating and deleting database entries using migrations, models and controllers. But now I'm trying to do the same with a subscriptions table that has a subscriberId and a followeeId in it. Both of these fields reference different ids of the same table (users). This kind of task seem to require some finetuning. And I'm stuck.
Here's my code with some comments.
Subscriptions Table
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('subscriberId');
$table->unsignedBigInteger('followeeId');
$table->foreign('subscriberId')->references('id')->on('users');
$table->foreign('followeeId')->references('id')->on('users');
});
Previously, I've used another approach to foreign ids, namely the one with the $table->foreignId('user_id')->constrained() pattern, but in this particular case I need to make sure that the two foreign ids reference different users, so I went for a more verbose option.
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
Here I've added the second parameter. This seems to work.
Subscription Model
class Subscription extends Model
{
use HasFactory;
protected $fillable = [
'subscriberId',
'followeeId'
];
public function subscriberId()
{
return $this->belongsTo(User::class, 'id', 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'id', 'followeeId');
}
}
Here I pass additional parameters, too, although in this case I'm not so sure if these are the correct ones. But this is my best guess. If I'm not mistaken, the second parameter of the belongsTo relation is inferred from the model that is being passed in, not the model of the parent class as is the case with the hasMany relation. So in this case that would be 'id' of the users table, which would be the default here anyway, but I need the third parameter, so I explicitly state the second parameter as well. Again, I'm not sure about this combination, but that's what I was able to make of the docs. I've also used other combinations of additional parameters, and even tried getting rid of these two public functions altogether, but that won't work either.
Now, here's the controller. If I do this:
$user->subscriptions()->get();
I do get the subscriptions I want. But if I do this instead:
$user->subscriptions()->create([
'subscriberId' => 1,
'followeeId' => 2
]);
I get the 500 error. I've also tried another approach:
$newSub = new Subscription;
$newSub->subscriberId = 1;
$newSub->followeeId = 2;
$newSub->save();
return $newSub;
But still no success. I still get the 500 error when I try to save()
Please help me out.
Solution
I should have used
public $timestamps = false
in the Subscription model, and I also misunderstood the docs. The correct combo is
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
and
Subscription Model
public function subscriberId()
{
return $this->belongsTo(User::class, 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'followeeId');
}

Unable to get selective column names on Laravel Eloquent Nested Eager Relationship

I have a Product Model with a following relationship:
public function recommendedPricing()
{
return $this->hasMany(RecommendedPricing::class);
}
The Recommended Pricing Model is:
protected $fillable = [ 'sku_id', 'unit_type_id', 'base_price', 'min_billable_qty', 'max_billable_qty', 'discount_method'];
protected $with = ['bands'];
public function bands()
{
return $this->hasMany('App\Models\RecommendedPricingBand');
}
The Recommended Pricing Band Model is like:
protected $fillable = ['sku_id','recommended_pricing_id','start','end','percent_change','fixed_price'];
Now In my Controller I am doing this:
Product::where('id', $product->id)->with(['recommendedPricing', 'recommendedPricing.bands'])->get();
which gives the entire result. But I want selected columns from both Recommended Pricing and Recommended Pricing Band. So, I tried this:
Product::where('id', $product->id)->with(['recommendedPricing:id, base_price, discount_method', 'recommendedPricing.bands:id, percent_change, fixed_price'])->get();
But this always results in an error.
My question: Is there any better approach or way to fetch the correct result for this kind of a nested relationship scenario?
When eager loading particular fields you can't have spaces. I usually do a seperate with line for each relationship. I don't think you need the array either. And you always have to get the id first, which you have already :) hope this works...
->with('recommendedPricing:id,base_price,discount_method')
->with('recommendedPricing.bands:id,percent_change,fixed_price')

Eloquent With A One to One 'Like' Relationship

I currently have an Eloquent Model that I have tried to simplify for this example and has a similar structure to the below.
class Student extends Model {
public function classes()
{
return $this->hasMany('App\Classes', 'class_code','code');
}
public function events()
{
return $this->hasOne('App\Events', 'event_code', 'code');
}
}
Every student has an assigned code. Hence a student can be matched to classes or event in a one to many / one relationships via this code. The issue is the relationship of the event. The code is slightly different.
In classes, the code will be 11-ABCD.00
For events, the code is: 11-ABCD
The decimal point is missing in the event code but otherwise, the code is the same. The decimal point simply allows for finer sub-divisions. For relationships, it does not matter and may not always exist i.e. A student may not have a class or event related to them.
I can manually retrieve an Event record like this:
class Student extends Model {
public function events($code)
{
$code = explode('.', $code);
if(count($code) > 0)
{
$code = $code[0];
}
return Event::where('code', $code)->first();
}
}
But this isn't in the true spirit of eloquent when I want to retrieve an entire collection e.g.
$results = Student::with('events')->first();
In short, can I design the relationship of the event to automatically take the key 'code' and strip it so that I can retrieve the records that are relevant?
Example Coding:
Student (Model) (10-ABCD.10)
Classes (One to Many) (10-ABCD.10)
Event (One to One) (10-ABCD)

Copying Data from table to another table and applying one to many relationship

This is my Report Model
protected $fillable = [
'site_url',
'reciepients',
'monthly_email_date'
];
public function site()
{
return $this->belongsTo('App\Site');
}
This is my Site Model
public function report()
{
return $this->hasMany('App\Report');
}
This is my ReportController
public function showSpecificSite($site_name)
{
$records = DB::table('reports')
->select('email_date','url','recipient')
->whereHas('sites', function($query){
$query->where('site_name',$site_name);
})
->get();
return view('newsite')->with('records',$records)
->with('site_name',$site_name);
}
My Controller is not yet working as well.
The thing is I would like to copy all the three files from sites table to reports table.
Is it possible in insertInto ?
My code on ReportController shows you that I'm selecting data from reports table but I am the one who puts data to reports table to see the output but it is not yet working because of the it cant reach out the value of site_name even though I already put a relationship between the two tables.
You're not actually using Eloquent in your controller you're just using the Query Builder (DB). This will mean that you don't have access to anything from your Eloquent models.
Try:
$records = \App\Report::whereHas('site', function($query) use($site_name) {
$query->where('site_name', $site_name);
})->get(['id', 'email_date', 'url', 'recipient']);
I've added id to the list of columns as I'm pretty sure you'll need that to use whereHas.
NB to use a variable from the parent scope inside a closure you need to pass it in using use().

Laravel -- Flatten data appended via `with`

I've got two models, User and Seminar. In English, the basic idea is that a bunch of users attend any number of seminars. Additionally, exactly one user may volunteer to speak at each of the seminars.
My implementation consists of a users table, a seminars table, and a seminar_user pivot table.
The seminar_user table has a structure like this:
seminar_id | user_id | speaking
-------------|-----------|---------
int | int | bool
The relationships are defined as follows:
/** On the Seminar model */
public function members()
{
return $this->belongsToMany(User::class);
}
/** On the User model */
public function seminars()
{
return $this->belongsToMany(Seminar::class);
}
I am struggling to figure out how to set up a "relationship" which will help me get a Seminar's speaker. I have currently defined a method like this:
public function speaker()
{
return $this->members()->where('speaking', true);
}
The reason I'd like this is because ultimately, I'd like my API call to look something like this:
public function index()
{
return Seminar::active()
->with(['speaker' => function ($query) {
$query->select('name');
}])
->get()
->toJson();
}
The problem is that since the members relationship is actually a belongsToMany, even though I know there is only to ever be a single User where speaking is true, an array of User's will always be returned.
One workaround would be to post-format the response before sending it off, by first setting a temp $seminars variable, then going through a foreach and setting each $seminar['speaker'] = $seminar['speaker'][0] but that really stinks and I feel like there should be a way to achieve this through Eloquent itself.
How can I flatten the data that is added via the with call? (Or rewrite my relationship methods)
Try changing your speaker function to this
public function speaker()
{
return $this->members()->where('speaking', true)->first();
}
This will always give you an Item as opposed to a Collection that you currently receive.
You can define a new relation on Seminar model as:
public function speaker()
{
return $this->belongsToMany(User::class)->wherePivot('speaking', true);
}
And your query will be as:
Seminar::active()
->with(['speaker' => function ($query) {
$query->select('name');
}])
->get()
->toJson();
Docs scroll down to Filtering Relationships Via Intermediate Table Columns

Categories