Eager Loading hasMany Relations with Twig
Created 6 years ago by drmonkeyninja

I've been hitting a bit of an issue with eager loading relations and Twig and wondered whether there was a better solution to my problem than the one I've found.

I've got a client model that has many projects; and projects have many tasks. The relationship for the client model is being manually specified like so:-

class ClientModel extends SupportClientsEntryModel implements ClientInterface
{
    /**
     * Has many projects
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function projects()
    {
        return $this->hasMany(ProjectModel::class, 'client_id');
    }
}

The project model has a similar definition for tasks.

I'm wanting to retrieve a client from the database with all the associated projects and tasks that meet certain criteria. So to do this I am eager loading the relations. I'm doing a query something like this:-

$this->model->with(['projects' => $projectsCallback, 'projects.tasks' => $tasksCallback])
    ->whereHas('projects', $projectsCallback)
    ->find($clientId);

This works fine, I can check the returned values and I see exactly what I want and the correct SQL queries are being generated. My problem arises when I output these with Twig:-

<ul>
{% for project in client.projects %}
    {% for task in project.tasks %}
        <li>{{ task.description }}</li>
    {% endfor %}
{% endfor %}
</ul>

Here client.projects is null. It seems Twig (which I am new to) is running through the options available (object property, object method, object getter, etc) and coming up blank.

After much searching around and testing things out I've been able to resolve the issue by defining getter methods for the relations on the relevant models. So for example, on my ClientModel I've got:-

/**
 * Get projects
 * @return ProjectModel
 */
public function getProjects()
{
    return $this->projects;
}

This has solved my problem, but as eager loading is something I can see myself needing to do often this approach is less than ideal.

Is there a better solution to this? Or is there something that could perhaps be added to Pyro to remove the need for adding these getter methods manually?

ryanthompson  —  6 years ago

Hi there!

One thing in general to keep in mind with presenters is you can also be more specific in your methods. So for example client.projects() will return the relation as defined on your model (unless your presenter has an identically named method in which case you would want client.getObject().projects()).

So the projects doesn't translate to projects() unless like you discovered it has a getter. This is designed around how the normal model works via API.

Now on do your question you can access query builder methods with twig too:

client.projects().with(['foo']).get()

And you might benefit from throwing a .cache($minutes) in there too if it's complex. It will automatically bust cache as the model is updated: https://pyrocms.com/help/developer-tools/cache

You can use .with() using the entries plugin function too!

You may want to look into always eager loading these relations by defining $with on your model:

protected $with = [
    'projects',
    'projects.tasks'
];

The above will eager load based on the projects() method and tasks() method on the project model as well.

Hope this answers your questions!

drmonkeyninja  —  6 years ago

Hi Ryan, thanks for your comments. There's more going on than just retrieving the client and its relations than just setting it for the view so we need to keep the logic in the controller; and anyway we prefer to keep the retrieval of data in our controllers as we find it helps keep logic out of the views and aids maintainability in the long term. So your suggestion for accessing the query builder methods with Twig doesn't really work for us.

As for always eager loading relations on the model we'd also try and avoid this as it is only this view that requires this particular eager loading.

After re-evaluating my code a bit I've moved the getter methods to the relevant presenter classes which also resolves the issues I've been getting. This seems like a better fix as the problem was at the view level rather than higher up as everything worked fine in the controller. For example:-

class ClientPresenter extends EntryPresenter
{
    /**
     * Get projects
     * @return ProjectModel
     */
    public function projects()
    {
        return $this->object->projects;
    }

}