Laravel retrieving eloquent nested eager loading - php

In my application i have 4 models that relate to each other.
Forms->categories->fields->triggers
What I am trying to do is get the Triggers that refer to the current Form.
Upon researching i found nested eager loading, which would require my code to look like this
Form::with('categories.fields.triggers')->get();
Looking through the response of this i can clearly see the relations all the way down to my desired triggers.
Now the part I'm struggling with is only getting the triggers, without looping through each model.
The code i know works:
$form = Form::findOrFail($id);
$categories = $form->categories;
foreach ($categories as $category) {
$fields = $category->fields;
foreach ($fields as $field) {
$triggers[] = $field->triggers;
}
}
I know this works, but can it be simplified? Is it possible to write:
$form = Form::with('categories.fields.triggers')->get()
$triggers = $form->categories->fields->triggers;
To get the triggers related? Doing this as of right now results in:
Undefined property: Illuminate\Database\Eloquent\Collection::$categories
Since it is trying to run the $form->categories on a collection.
How would i go about doing this? Do i need to use the HasManyThrough relation on my models?
My models
class Form extends Model
{
public function categories()
{
return $this->hasMany('App\Category');
}
}
class Category extends Model
{
public function form()
{
return $this->belongsTo('App\Form');
}
public function fields()
{
return $this->hasMany('App\Field');
}
}
class Field extends Model
{
public function category()
{
return $this->belongsTo('App\Category');
}
public function triggers()
{
return $this->belongsToMany('App\Trigger');
}
}
class Trigger extends Model
{
public function fields()
{
return $this->belongsToMany('App\Field');
}
}
The triggers run through a pivot table, but should be reachable with the same method?

I created a HasManyThrough relationship with unlimited levels and support for BelongsToMany:
Repository on GitHub
After the installation, you can use it like this:
class Form extends Model {
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
public function triggers() {
return $this->hasManyDeep(Trigger::class, [Category::class, Field::class, 'field_trigger']);
}
}
Form::with('triggers')->get();
Form::findOrFail($id)->triggers;

Related

How to check if a Laravel eloquest relation is eagerLoaded on the retrieved event of a model?

I will try to simplify the problem as much as I can.
I want to disable a relation load from a trait on a resource that happens at the retrieved event.
There is a model we will name Post that uses a trait named HasComments.
class Post extends Model
{
use HasComments;
...
}
The trait listenes for the retrieved event on the model and loads the comments relation.
trait HasComment
{
public static function bootHasComment(): void
{
self::retrieved(function ($model) {
$model->load('comments');
});
}
public function comments(): BelongsTo
{
return $this->belongsTo(Comment::class);
}
}
I want to be able to check if the comments relation was eager loaded and NOT load the relation again.
I tried to check if the relation is loaded but failed.
ex:
self::retrieved(function ($model) {
if (!isset($model->relations['comments'])) {
dd('still loads!');
$model->load('comments');
}
});
or
self::retrieved(function ($model) {
if (!$model->relationLoaded('comments')) {
dd('still loads!');
$model->load('comments');
}
});
I was also thinking of maybe there is a way to disable this behavior when constructin the query but failed again.
ex:
trait HasComment
{
public bool $load = true;
public static function bootHasComment(): void
{
self::retrieved(function ($model) {
if (!$this->load) {
dd('still loads!');
$model->load('comments');
}
});
}
public function comments(): BelongsTo
{
return $this->belongsTo(Comment::class);
}
public function disableRetrievedLoad()
{
$this->load = false;
}
}
Has someone encountered something similar and can give me some help?
There is a method named relationLoaded that returns a boolean. Seems like the right fit for your needs.
Alternatively, if you want to load a relationship if it's not been loaded yet, there's loadMissing.
$post = Post::first();
$post->relationLoaded('comments'); // false
$post->loadMissing('comments'); // makes the queries to load the relationship
$post->relationLoaded('comments'); // true
$post->loadMissing('comments'); // does nothing. comments is already loaded

Is there a common naming convention for custom trait/attribute model in Laravel?

I am designing an app where user can define traits/attributes for objects.
I was planning to go like this
class AttributeName extends Model
{
//
}
class Attribute extends Model
{
public function attributeName()
{
return $this->belongsTo('AttributeName');
}
public function job()
{
return $this->belongsTo('Job');
}
}
class Job extends Model
{
public function attributes()
{
return $this->hasMany('Attribute');
}
public function getAttributeValue($attributeNameId)
{
return $this->attributes->where('attribute_name_id', $attributeNameId)->first()->value ?? ;;
}
}
Unfourtunately, this gives me hard time as $this->attributes has another meaning for Laravel models. So Attribute is not a very good name for these things.
Naming it trait won't go well in PHP either. So what should I name this? Is there any common name that reflects semantics as well as attribute or trait does but doesn't conflict with other uses in Laravel and PHP?

eager load of relations of a specific model in laravel

I have a model named Test like this:
class Test extends Model
{
public $primaryKey = 'test_id';
public function questions ()
{
return $this->belongsToMany('App\Question', 'question_test', 'test_id', 'question_id');
}
}
And a Question model like this:
class Question extends Model
{
public function tests ()
{
return $this->belongsToMany('App\Test', 'question_test', 'question_id', 'test_id');
}
}
As you see there is a ManyToMany relation between this two model.
Now in a controller function, I want to get an specific Test(by id) and send it to a view. then eager load all it's questions related models and send it to another view. like this :
public function beginTest ($course_id, $lesson_id, $test_id)
{
$test = Test::find($test_id);
if ($test->onebyone) {
return view('main/pages/test/test-onebyone', compact('test'));
} else {
$test = $test->with('questions.options')->get();
return view('main/pages/test/test-onepage', compact('test', 'done_test_id'));
}
}
}
Problem is that when I use with() laravel method to eager load relations, it return all Test models with their Question relations while I want to get relations of selected Test model only.
what is your solution to solve that?
You can use 'lazy eager loading'.
$test->load('questions.options');
Using with off the model instance will make it use a new builder and cause a new query to be executed.

Laravel get related models of related models

I have a RepairRequest model, which is associated with a Vehicle.
class RepairRequest extends \Eloquent {
public function vehicle() {
return $this->belongsTo('Vehicle');
}
}
class Vehicle extends \Eloquent {
public function requests() {
return $this->hasMany('RepairRequest');
}
}
I would like to get all RepairRequests for the vehicle associated with a given RepairRequest, so I do
return RepairRequests::find($id)->vehicle->requests;
This works fine.
However, RepairRequests have RepairItems:
// RepairRequest class
public function repairItems() {
return $this->hasMany('RepairItem', 'request_id');
}
// RepairItem class
public function request() {
return $this->belongsTo('RepairRequest', 'request_id');
}
which I would like to return too, so I do
return RepairRequests::find($id)->vehicle->requests->with('repairItems');
but I get the following exception:
Call to undefined method Illuminate\Database\Eloquent\Collection::with()
How can I write this so that the returned json includes the RepairItems in the RepairRequest json?
Load related models using load method on the Collection:
return RepairRequests::find($id)->vehicle->requests->load('repairItems');
which is basically the same as:
$repairRequest = RepairRequests::with('vehicle.requests.repairItems')->find($id);
return $repairRequest->vehicle->requests;
I'd suggest eager loading everything.
return RepairRequests::with('vehicle.requests.repaireItems')->find($id);

"Three way" many-to-many relationship using Eloquent

I have a simple database setup: Users, Groups, Pages - each are many to many.
See diagram: http://i.imgur.com/oFVsniH.png
Now I have a variable user id ($id), and with this I want to get back a list of the pages the user has access to, distinctly, since it's many-to-many on all tables.
I've setup my main models like so:
class User extends Eloquent {
protected $table = 'ssms_users';
public function groups()
{
return $this->belongsToMany('Group', 'ssms_groups_users', 'user_id','group_id');
}
}
class Group extends Eloquent {
protected $table = 'ssms_groups';
public function users()
{
return $this->belongsToMany('User', 'ssms_groups_users', 'user_id','group_id');
}
public function pages()
{
return $this->belongsToMany('Page', 'ssms_groups_pages', 'group_id','page_id');
}
}
class Page extends Eloquent {
protected $table = 'ssms_pages';
public function groups()
{
return $this->belongsToMany('Group', 'ssms_groups_pages', 'group_id','page_id');
}
}
I can get the groups the user belongs to by simply doing:
User::with('groups')->first(); // just the first user for now
However I'm totally lost on how to get the pages the user has access to (distinctly) with one query?
I believe the SQL would be something like:
select DISTINCT GP.page_id
from GroupUser GU
join GroupPage GP on GU.group_id = GP.group_id
where GU.user_id = $id
Can anyone help?
Thanks
TL;DR:
The fetchAll method below, in the MyCollection class, does the work. Simply call fetchAll($user->groups, 'pages');
Ok, assuming you managed to load the data (which should be done by eager-loading it, as mentioned in the other answer), you should loop through the Groups the User has, then loop through its Pages and add it to a new collection. Since I've had this problem already, I figured it would be easier to simply extend Laravel's own Collection class and add a generic method to do that.
To keep it simple, simply create a app/libraries folder and add it to your composer.json, under autoload -> classmap, which will take care of loading the class for us. Then put your extended Collection class in the folder.
app/libraries/MyCollection.php
use Illuminate\Database\Eloquent\Collection as IlluminateCollection;
class MyCollection extends IlluminateCollection {
public function fetchAll($allProps, &$newCollection = null) {
$allProps = explode('.', $allProps);
$curProp = array_shift($allProps);
// If this is the initial call, $newCollection should most likely be
// null and we'll have to instantiate it here
if ($newCollection === null) {
$newCollection = new self();
}
if (count($allProps) === 0) {
// If this is the last property we want, then do gather it, checking
// for duplicates using the model's key
foreach ($this as $item) {
foreach ($item->$curProp as $prop) {
if (! $newCollection->contains($prop->getKey())) {
$newCollection->push($prop);
}
}
}
} else {
// If we do have nested properties to gather, then pass we do it
// recursively, passing the $newCollection object by reference
foreach ($this as $item) {
foreach ($item->$curProp as $prop) {
static::make($prop)->fetchAll(implode('.', $allProps), $newCollection);
}
}
}
return $newCollection;
}
}
But then, to make sure your models will be using this class, and not the original Illuminate\Database\Eloquent\Collection, you'll have to create a base model from which you'll extend all your models, and overwrite the newCollection method.
app/models/BaseModel.php
abstract class BaseModel extends Eloquent {
public function newCollection(array $models = array()) {
return new MyCollection($models);
}
}
Don't forget that your models should now extend BaseModel, instead of Eloquent. After all that is done, to get all your User's Pages, having only its ID, do:
$user = User::with(array('groups', 'groups.pages'))
->find($id);
$pages = $user->groups->fetchAll('pages');
Have you tried something like this before?
$pages = User::with(array('groups', 'groups.pages'))->get();
Eager loading might be the solution to your problem: eager loading

Categories