oborrero's avatar

Laravel 5 | For Loop w/Multiple Relations

I've setup a few model relationships and I'm trying to feed that data to a page that's displays a table with different columns to show the information. I've seeded 3 Tables (customers, jobs, steps) with Faker dummy data just for testing. How can I pull in the "Steps" table column data? My controller defines both (2) variables for the Job & Step model but I'm unsure how to pull in Step to the display. Do I have to do an array for the the variables called in the controller?

Here's my controller setup

 public function index()
    {
        $jobs = Job::all();
        $steps = Step::all();
        return view('jobs.home', compact ('jobs', 'steps'));
    }

Here's my blade setup

   @foreach ($jobs as $job)
     <tr>
        <td>{{ $job->number }}</td>
        <td>{{ $job->customer->name }}</td>
        <td>NEED TO ADD STEPS COLUMN Here</td>
    </tr>
    @endforeach
0 likes
33 replies
code_chris's avatar

If the steps are related to the jobs then you can just eager load them into jobs

$jobs = Job::with('steps')->get();

Then in your view you can just do:

@foreach ($jobs as $job)
     <tr>
        <td>{{ $job->number }}</td>
        <td>{{ $job->customer->name }}</td>
        <td>{{ $job->step }}</td>
    </tr>
@endforeach

At least I think that should work, not tested it!

bobbybouwmann's avatar

Really easy to do! Let's say you have a one to one relations set up with your Step model called step

public function index()
{
    $jobs = Job::with(['step'])->get();

    return view('my.view', compact('jobs'));
}

Now that we have our jobs we can display them

<table>
    @foreach($jobs as $job)
        <tr>
            <td>{{ $job->id }}</td>
            <td>{{ $job->name }}</td>
            <td>{{ $job->step->id }}</td>
            <td>{{ $job->step->name }}</td>
        </tr>
    @endforeach
</table>    
oborrero's avatar

So when I run tinker Step::with(['job'])->find(1); that works great, but i can't run the inverse of that =/. To give some more details, here are my models:

Job Model

public function customer ()
             {
           return $this->belongsTo('MariasApp\Customer');
                }

     public function steps ()
         {
              return $this->hasMany('MariasApp\Step');
          }

Step Model:

  public function Job ()
                  {
                  return $this->belongsTo('MariasApp\Job');
                    }

And here's the schema so far Schema

code_chris's avatar

Should probably make the function job() rather than Job():

public function job ()
 {
        return $this->belongsTo('MariasApp\Job');
 }

What does it say when you try to look up the reverse?

oborrero's avatar

I get this when running Job::with(['step'])->find(1); BadMethodCallException with message 'Call to undefined method Illuminate\Database\Query\Builder::step()'

code_chris's avatar

Well that's because the function is called steps, try:

Job::with(['steps'])->find(1);
code_chris's avatar

Also, looking at your schema, in your steps table it should be job_id rather than jobs_id, same for the other tables that use the foreign key.

oborrero's avatar

Chris, that was it!!! Ok, please explain why it had to be job and not jobs??

code_chris's avatar

You always use the singular, because you are relating to one job, just like when you use customer_id to reference a customer, you don't use customers_id.

You can actually call it whatever you want and then tell eloquent what you have used, but that's what it looks for by default so unless you have a good reason to use something else there's really no point.

oborrero's avatar

@code_chris how can I call a column from the steps relation? I.e I tried 'Job::with(['steps'])->column_id->find->(1)' but I get a null response

code_chris's avatar

Are you trying to output the column? Do you have multiple job entries related to each job?

Its much easier to help if you make it clear what you are doing so far

oborrero's avatar

@code_chris - Hopefully this make sense. So now that the relationships are fine (ty again) and I can see the output from running the tinker command below, i'm trying to see if there's a way for me to get more granular with what's being displayed from the relation. Using the output below, I'm curious if it's possible to only get the "body" column contents of the steps when using the Job model. For example, i would like to run Job::with('steps')->find(1)->body which would return Eligendi reiciendis ratione labore sed.

>>> Job::with('steps')->find(1);
=> <MariasApp\Job #0000000058b03494000000000b37013c> {
       id: 1,
       number: 59221,
       customer_id: 5,
       user_id: 17,
       created_at: "2015-03-24 01:32:20",
       updated_at: "2015-03-24 01:32:20",
       steps: <Illuminate\Database\Eloquent\Collection #0000000058b03496000000000b37013c> [
           <MariasApp\Step #0000000058b03492000000000b37013c> {
               id: 37,
               job_id: 1,
               body: "Eligendi reiciendis ratione labore sed.",
               created_at: "2015-03-24 01:32:21",
               updated_at: "2015-03-24 01:32:21"
           }
       ]
   }
JarekTkaczyk's avatar

@oborrero You need to follow the Eloquent conventions and name foreing keys just like the models, ie. Job model -> job_id OR provide the columns manually, if you don't:

return $this->hasMany('Model\Name', 'foreign_key');

so in your case all your relations referring to the jobs table need to be changed to:

return $this->hasMany('Model', 'jobs_id');

or just rename the columns.


About granularity - if you want to get only body as an array, instead of collection of models, then you can simply call $job->steps->lists('body'). I would definitely not do that in the relation method.

shawnyv's avatar

@oborrero You can certainly limit what you pull in with the collection, by applying a select statement to your relationship:

return $this->hasMany('Model', 'jobs_id')->select('jobs_id','body');

(You need the jobs_id in there in order for it to correctly link up.

code_chris's avatar

@oborrero If you were going to do it like that you would need to specify steps:

Job::with('steps')->find(1)->steps->body;

But I don't think that can work since you can have multiple steps for each job right?

Hard to see how your controllers/view work but I can see it working like this:

$jobs = Job::with('steps')->find(1);

return view('my.view', compact('jobs'));

View:

@foreach($jobs as $job)
     <tr>
        <td>{{ $job->number }}</td>
        <td>{{ $job->customer->name }}</td>
        @foreach($job->steps as $step)
            <td>{{ $step->body }}</td>
        @endforeach
    </tr>
@endforeach

A couple of questions:

Do you actually intend to have multiple entries in the steps table related to the same job?

How do you want to output them in your view?

oborrero's avatar

@code_chris sorry for the delay, was traveling the last 36 hours ;(. So I was planning on having multiple Step entries per job since there may be additional items that need to be done per job. Maybe to keep things simpler I will just do a 1-to-1, or drop everything into 1 table lol, but I'm really trying to grasp the concept of how this works. I read an article only about how you should create multiple tables for your database so you can easily add more columns to those tables later down the road without suffering horrible DB query performance

So for the table attached you can see the following:

  • There can be many jobs
  • A job can only have one customer
  • A job can have many steps
  • A job can have many poles
  • A job can have many permits

The Table

The numbers shown in the last 2 columns are just placeholders till I can get the data to display. Based on your code, I think my for loop would look like this

@foreach($jobs as $job)
     <tr>
        <td>{{ $job->number }}</td>
        <td>{{ $job->customer->name }}</td>
        @foreach($steps->steps as $step)
            <td>{{ $step->body }}</td>
        @endforeach
        @foreach($poles->poles as $pole)
            <td>{{ $pole->njuns_ticket }}</td>
        @endforeach
  @foreach($permit->permits as $permit)
            <td>{{ $permit->number }}</td>
        @endforeach
    </tr>
@endforeach

Here's what I tested but failed with my controller. There's nothing fancy about this, i'm just trying to get some data from each (relation) table column and show what job it's tied too:

public function index()
    {
        $jobs = Job::all();
        $permits = Permit::all();
        $poles = Pole::all();
        $steps = Step::all();
        return view('jobs.home', compact ('jobs', 'permits', 'poles', 'steps'));
    }

To put things in perspective here's the schema so far

Schema

code_chris's avatar

@oborrero If you set the correct relationships in your models then you don't need to put them in a variable each, you can just eager load them all into the jobs variable:

public function index()
{
     $jobs = Job::with('steps', 'poles', 'permits')->get();
     return view('jobs.home', compact ('jobs'));
}

And then:

@foreach($jobs as $job)
     <tr>
        <td>{{ $job->number }}</td>
        <td>{{ $job->customer->name }}</td>
        
        @foreach($job->steps as $step)
            <td>{{ $step->body }}</td>
        @endforeach

        @foreach($job->poles as $pole)
            <td>{{ $pole->njuns_ticket }}</td>
        @endforeach

        @foreach($job>permits as $permit)
            <td>{{ $permit->number }}</td>
        @endforeach

    </tr>
@endforeach

There may be a better way to do it with less loops, but that should work for now.

code_chris's avatar

Are you just looking to output a number for how many poles/permits there are? If so you don't need a loop you can just output:

$job->poles->count()

I think that should work.

If you get errors can you post them back its hard to figure out what's going wrong without seeing it.

Snapey's avatar

@oborrero just a few thoughts.

  • You can't load all the models individually else there is no relation between them. You will not know which steps belong to which jobs. use the with() statement so that they are nested correctly inside each job

  • You should also load the customer in the with statement else you will be hitting the database again for every job to lookup the customer name.

  • possibly not a problem for now but you shouldn't use td in this way as you will have a variable number of columns in your table for every row.

Hope these ideas help

oborrero's avatar

@code_chris you strike again, that was it! The only thing that's happening and throwing off my table, is that it's showing all the steps, but i'd like those additional steps to be shown when you click on the job. Is there anyway to grab the latest step instead of all of them to feed it into the table?

Table

code_chris's avatar
Level 4

@oborrero Sure, instead of doing this loop:

@foreach($job->steps as $step)
    <td>{{ $step->body }}</td>
@endforeach

Just do:

<td>{{ $job->steps->first() }}</td>

You can use any of the methods of the collection: http://laravel.com/api/5.0/Illuminate/Support/Collection.html

Also, as @Snapey suggested, you should eager load the customers into the jobs in the controller, otherwise your view will be doing lots of queries to find them later:

$jobs = Job::with('steps', 'poles', 'permits', 'customer')->get();
2 likes
oborrero's avatar

@code_chris it spits out the raw JSON for that job, but i'll just play around with it to figure it out. Ty so much, I'm feeling a little more comfortable now

shawnyv's avatar

@oborrero - To get it to spit out just what you want, you have to specify the field.

<td>{{ $job->steps->first() }}</td>

Gets you the first relation (IE everything for the record attached.

If you wanted the "body" property, you'd do

<td>{{ $job->steps->first()->body }}</td>
1 like
oborrero's avatar

Nice, thanks @shawnyv! It's doing something weird where it spits out the latest one X amount of times O.o. I'll take a look and also I think I should eager load my customers, as it's 55 queries for that lol. Also could you explain ($job->steps as $step) , So i know that $job references the variable in the controller, is steps the table in the DB? then of course your just defining the $step variable to eager load the result? Did I capture that correctly?

@foreach ($jobs as $job)
                            <tbody>
                                <tr>
                                    <td>{{ $job->number }}</td>
                                    <td>{{ $job->customer->name }}</td>
                                    <td>Test</td>
                                    <td>test</td>
                                    @foreach($job->steps as $step)
                                        <td> {{ $job->steps->first()->body }} </td>
                                    @endforeach
                            </tbody>
                        </table>
                        @endforeach

Weird Stuff

shawnyv's avatar

@oborrero your problem code comes here:

@foreach($job->steps as $step)
                                        <td> {{ $job->steps->first()->body }} </td>
                                    @endforeach

The reason it's not behaving is because you've given it weird messages in your foreach loop.

The loop will go through every instance of Steps for the job, and treat it as $step. So what you want is:

@foreach($job->steps as $step)
    <td> {{ $step->body }} </td>
@endforeach
1 like
shawnyv's avatar

@oborrero Here's a little more on the foreach loop.

Imagine you have 3 jobs: Job1, Job2, and Job3.

Each job will have 3 steps. Step1, Step2, Step3

So you could say "Show me Job1->Step3" for example, and that would make sense.

When you say:

@foreach($jobs as $job)
    @foreach($job->steps as $step)
        <td> $step->body
    @endforeach
@endforeach

What you're saying is:
Go through every job.
For every job, go through all the steps.
For every step, show me the body.

Hopefully this helps you out a little?

2 likes
code_chris's avatar

@oborrero As I said earlier, you don't need that loop anymore, since you only want to see one step in there right?

@foreach ($jobs as $job)

<tr>
    <td>{{ $job->number }}</td>
    <td>{{ $job->customer->name }}</td>
    <td>Test</td>
    <td>test</td>

    <td> {{ $job->steps->first()->body }} </td>
</tr>

@endforeach

Also, you need to clean up the HTML, you don't want to close the table or the <tbody> within the loop, you should just have a new <tr> in the loop and </tr> close it before the loop ends, then close your <tbody> and anything else after the loop.

I noticed you said you wanted the latest step, so you might want to do last() instead of first(), you could also do a more advanced query in your controller to get the latest step since you don't actually need them all here.

1 like
oborrero's avatar

@code_chris after many variations of $job->steps->first()->body it refuses to show the body, it returns Trying to get property of non-object. I've grown a few grey hairs messing with tinker to see how to get this to display lol. Appreciate you and @shawnyv efforts so far

shawnyv's avatar

That error message indicates that it can't find the relationship.

Do you have your debugger set up? If yes, then you should put a breakpoint in your controller after you run your query, so that you can ensure that each job has all the steps. It may simply be the case that you have a job in your collection that doesn't have any steps.

If you don't have a debugger set up, then try only loading one job where you know you have steps, and see what happens.

Next

Please or to participate in this conversation.