Richard Craddock
Richard Craddock

Reputation: 153

Eloquent Relationship in Loop

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><a href="{{ route('Part.show',$Equipment->PartNum)}}">{{$Equipment->PartNum}}</a></td>
        <td><a href="{{ route('Equipment.show',$Equipment->SerialNumber)}}">{{$Equipment->SerialNumber}}</a></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

Upvotes: 2

Views: 3464

Answers (2)

kmuenkel
kmuenkel

Reputation: 2789

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.

Upvotes: 1

Mihai Matei
Mihai Matei

Reputation: 24276

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><a href="{{ route('Part.show',$Equipment->PartNum)}}">{{$Equipment->PartNum}}</a></td>
            <td><a href="{{ route('Equipment.show',$Equipment->SerialNumber)}}">{{$Equipment->SerialNumber}}</a></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><a href="{{ route('Part.show',$Equipment->PartNum)}}">{{$Equipment->PartNum}}</a></td>
            <td><a href="{{ route('Equipment.show',$Equipment->SerialNumber)}}">{{$Equipment->SerialNumber}}</a></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

Upvotes: 2

Related Questions