Laravel Eloquent "siblings" as a relationship? - laravel

class PageRelation extends Eloquent
{
public $incrementing = false;
public $timestamps = false;
protected $table = 'page_relation';
protected $casts = [
'parent' => 'int', // FK to page
'child' => 'int', // FK to page
'lpc' => 'int',
];
protected $fillable = [
'lpc',
];
public function children()
{
return $this->hasMany(Page::class, 'category_id', 'child');
}
public function parents()
{
return $this->hasMany(Page::class, 'category_id', 'parent');
}
public function siblings()
{
// ... return $this->hasMany(Page::class ...
// how do I define this relationship?
}
}
In my design a sibling is (as you might expect) a record that shares the same parent but not itself (exclude current child). How can I achieve this?
This is not a duplicate of Laravel Eloquent Relationships for Siblings because 1) the structure is different, 2) I would like to return a relationship, not a query result, I know how to query this, but I want the power of eager loader.

I don't think you can do that with Laravel's in-built relations. What I would suggest doing is creating your own relation type that extends HasMany and use that.
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class HasManySiblings extends HasMany
{
public function addConstraints()
{
if (static::$constraints) {
if (is_null($foreignKeyValue = $this->getParentKey())) {
$this->query->whereNull($this->foreignKey);
} else {
$this->query->where($this->foreignKey, '=', $foreignKeyValue);
$this->query->whereNotNull($this->foreignKey);
}
$this->query->where($this->localKey, '!=', $this->parent->getAttribute($this->localKey));
}
}
public function getParentKey()
{
return $this->parent->getAttribute($this->foreignKey);
}
}
By extending the HasMany class and providing your own implementation of addConstraints you are able to control what gets added to the query for related models. Usually, what Laravel would do here is add where parent_id = <your model ID> but I've changed it here to add where parent_id = <your model PARENT ID> (if your model's parent_id is null it will instead add where parent_id is null). I've also added an extra clause to ensure that the calling model is not included in the resulting collection: and id != <your model ID>.
You can use it like this in your Page model:
class Page extends Model
{
public function siblings()
{
return new HasManySiblings(
$this->newRelatedInstance(Page::class)->newQuery(), $this, 'parent_id', 'id'
);
}
}
Now you should be able to load the siblings like this:
$page = Page::find(1);
dd($page->siblings);
Please note though, I have only tested this for retrieving related models and it may not work when using the relation for other purposes such as saving related models etc.
Also, please note that in my examples above I've used parent_id instead of parent as in your question. Should be straight swap though.

I am not sure if it works with your model which is kinda marginal because you are relating same objects with a middle table. But,
hasManyThrough()
could be a solution for this.
"... has many siblings through parent."
https://laravel.com/docs/5.6/eloquent-relationships#has-many-through

This is off-topic but bare me with this. I have this suggestion for the way you are handling these relations. You don't need PageRelation model, you can define belongsToMany relation on Page model directly. Moreover, you dont need extra attribute parent, this is kind of inconsistent, defining parent and child both, only children are enough to determine parents. So Instead of two seperate columns, You can reverse the keys when you retrieve the relation. Let me show you with an example what I mean:
pages:
keep this table intact
pages_relation:
- id
- page_id (foreign key to id on page)
- child_id (foreign key to id on page)
And then define two relations in your model:
class Page extends Model
{
public function children()
{
return $this->belongsToMany('App\Page', 'pages_relation', 'page_id', 'child_id');
}
public function parents()
{
return $this->belongsToMany('App\Page', 'pages_relation', 'child_id', 'page_id');
}
}
You can stick to whatever feels good to you. But, I feel this is more consistent. As, there is only single source of truth.
If A is a child of B, then B has to be a parent of A, its obvious, only "A is child of B" is enough to state "B is a parent of A".
I have tested this, it works very well.
EDIT
You can extend BelongsToMany relation to get BelongsToManySiblings realtionship, and just override the addWhereConstraints method.
class BelongsToManySiblings extends BelongsToMany
{
protected function addWhereConstraints()
{
$parentIds = \DB::table($this->table)
->select($this->foreignPivotKey)
->where($this->relatedPivotKey, '=', $this->parent->{$this->parentKey})
->get()->pluck($this->foreignPivotKey)->toArray();
$this->query->whereIn(
$this->getQualifiedForeignPivotKeyName(),
$parentIds
)->where(
$this->getQualifiedRelatedPivotKeyName(),
'<>',
$this->parent->{$this->parentKey}
)->groupBy($this->getQualifiedRelatedPivotKeyName());
return $this;
}
}
Then you can add siblings relationship method on your Page model:
public function siblings()
{
return new BelongsToManySiblings(
$this->newRelatedInstance(Page::class)->newQuery(),
$this,
'pages_relation',
'parent_id',
'child_id',
'id',
'id',
$this->guessBelongsToManyRelation()
);
}
Note: This case does not work for eager loads, eager load needs overriding match and addEagerContraints methods on the BelongsToManySiblings class. You can peek the BelongsToMany class on laravel source to see an example how it eager loads the relations.

Related

Laravel relationship between auto model instance

i'm work on project that require to make all step auto i mean user can add category with items "Regardless of the nature of the names of these category and items" .
and from back i will create new table for each " category and item " with name like user send and link tables with only two model One for Parent and anther for child item .
Now problem with get relationship between instance of Parent & Child Model
My Code
$parent = new ParentModel('product_categories');
$related = new RelatedModel('products');
My ParentModel Like :
`
public function childrens()
{
return $this->hasMany(RelatedModel::class , 'parent_id' ,'id');
}
`
My RelatedModel Like :
`
public function __construct($table = null ,array $attributes = [])
{
parent::__construct($attributes);
$this->setTable($table);
}
public function parent()
{
return $this->belongsTo(ParentModel::class , 'parent_id' ,'id');
}
`
Now when i pass different table name in instance model its work fine and return all table data
$query = $parent->newQuery()->get()
But problem when i try get data with these relationship dynamically
$query = $parent->newQuery()->with('childrens')->get()
laravel always ignore setTable() function and make query from
table name "related_models" not table i'm inject
this is problem show
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'related_models' doesn't exist
and query like that
select * from 'related_models' where 'related_models'.'parent_id' in (1,2)
can i from these model pass only table name and get all data with relation
or can any one help to make this logic sense
Thanks
The point of an ORM and models is to create a logical entrypoint to get data from te database. Going forth with an implementation like your's sounds like its not easy to follow for any new developer who looks at it, and it will eventually run into limitations.
What you could do if writing all the models is just to much work is creating abstracts:
// parent abstract
abstract class Parent extends Model {
abstract protected function childModel();
public function childrens()
{
return $this->hasMany($this->childModel() , 'parent_id' ,'id');
}
}
// parent implementation
class Product extends Parent {
public function childModel()
{
return ProductCategories::class;
}
}
// child abstract
abstract class Child extends Model {
abstract protected function parentModel();
public function parent()
{
return $this->belongsTo($this->parentModel() , 'parent_id' ,'id');
}
}
// child implementation
class ProductCategories extends Child {
public function parentModel()
{
return Product::class;
}
}

How to disable loading of relationships when not needed in Laravel

is it possible to disable the loading of relationships, but only in some cases?
Here are my models:
class League extends Model
{
...
public function country()
{
return $this->belongsTo(Country::class)->with('translations');
}
}
class Country extends Model
{
...
public function translations()
{
return $this->hasMany(CountryTranslation::class, 'country_id');
}
}
class CountryTranslation extends Model
{
...
}
In many places, I need to load the translations relationship for Country, but on some pages, I want to display information about the League and its Country only. There I don't want to show the CountryTranslation collection.
Here is the code for that page:
$country = $league->country;
Is it possible only for this line to disable the relations?
So, you're currently finding out one of the reasons for not defining the eager loading inside of the relationship. The first suggestion would be to remove the with() from the relationship definition, and add it in where needed. If desired, you can create another relationship that has the eager loading enabled, and it can use the base relationship to keep it DRY:
public function country()
{
return $this->belongsTo(Country::class);
}
public function countryWithTranslations()
{
return $this->country()->with('translations');
}
If this code change is not feasible, you will need to change how you're accessing the country relationship. When you access the relationship attribute, it lazy loads the relationship, and you don't have the ability to modify the relationship query. So, instead of accessing the relationship attribute, you'd need to call the relationship query so you can modify it.
Therefore, you won't be able to do $country = $league->country;, but you can do:
$country = $league->country()->without('translations')->first();
he with() simply eager loads the translations to avoid additional queries, but you should be able to load the translations with and without it, without with( adds additional queries. https://laravel.com/docs/9.x/eloquent-relationships#eager-loading
You will want to change:
public function country()
{
return $this->belongsTo(Country::class)->with('translations');
}
to
public function country()
{
return $this->belongsTo(Country::class);
}
If you want to load translations, you can do it in the controllers
// if you want translations at some point do this:
$league = League::with('country.translations')
$country = $league->country->translations
// if you do not want translations
$league = League::with('country')
$country = $league->country;
If you do not want to touch:
public function country()
{
return $this->belongsTo(Country::class)->with('translations');
}
you can create another method
public function countryClean()
{
return $this->belongsTo(Country::class);
}
$country = $league->countryClean;

How can I get all records belonging to the model class as relation on a 1-M polymorphic relation

I am trying to store statuses of multiple models in a polymorphic relationship and use them from there.
The retrieving of the status works but besides that I need to get all (available) the statuses belonging to that specific model. And then eager load them so I can use them in a select form or somewhere else.
Here is a representation:
// Status.php
class Status extends Model
{
public function statusable()
{
return $this->morphTo();
}
}
// Article.php
class Article extends Model
{
public function status()
{
return $this->morphOne(Status::class, 'statusable', 'statusable_type', 'statusable_id', 'status');
}
}
// Post.php
class Post extends Model
{
public function status()
{
return $this->morphOne(Status::class, 'statusable', 'statusable_type', 'statusable_id', 'status');
}
}
Here I can get the status(details) of the selected model like App\Article::first()->status()->get() or eager load it as expected. What I want is to add a method where I can call (all) statuses that belong to that particular model. For example:
$article = App\Article::first()->statuses();
and then return all available statuses to this model. I can (did) of course create a method like:
// Article.php
...
public function statuses()
{
$statuses = Status::where('statusable_type', self::class)->get();
return $statuses;
}
But this way I cant eager load it because I don't return a relationship. So is there a cleaner way where I can use eager loading as well?

Laravel | Using Eloquent hasManyThrough

I have a table called invoiceDetails that has item_id as foreign key from another table called items which has category_id as foreign key from table called categories.
I want to do that following using eloquent:
$result = InvoiceDetail::groupBy('item_id')
->selectRaw('sum(qty) as qty, item_id')->with('item', 'category')->get();
but I am getting error:
Call to undefined relationship [category] on model [App\InvoiceDetail].
Here's my relation inside Category model:
public function invoiceDetail() {
return $this->hasManyThrough('App\InvoiceDetail', 'App\Item', 'category_id', 'item_id');
}
Any suggestions?
Not sure you would even need a hasManyThrough relation here, unless you want to fetch all InvoiceDatail objects belonging to all items which in turn belong to the Category. That part is not clear from your question.
But in your example you are fetching items with their category from distinct item_id.
The reason this is not working is because you are trying to fetch the category relation from the InvoiceDetail object, which does not exist.
->with('item', 'category')
You want to load the Category based on the item relation, not based on the InvoiceDetail, try the dot notation (given that you did define the other relations)
->with('item.category')
Relations should be like this:
class InvoiceDetail extends Model
{
public function item()
{
return $this->belongsTo(\App\Item::class);
}
}
class Item extends Model
{
public function invoiceDetails()
{
return $this->hasMany(\App\InvoiceDetail::class);
}
public function category()
{
return $this->belongsTo(\App\Category::class);
}
}
class Category extends Model
{
public function items()
{
return $this->hasMany(\App\Item::class);
}
public function invoiceDetails()
{
return $this->hasManyThrough(\App\InvoiceDetail::class, \App\Item::class, 'category_id', 'item_id');
}
}
You would want to use the hasManyThrough if, for example, you have a Category and you want to load all the InvoiceDetails directly.
dd($category->invoiceDetails);

Simple Laravel Relationship

I have two models, one is LeadHistory and the other one is Leads.
Leads:
class Leads extends Model
{
public function lead_history()
{
return $this->hasMany('App\LeadHistory');
}
}
LeadHistory:
class LeadHistory extends Model
{
public function lead()
{
return $this->belongsTo('App\Leads', 'lead_id', 'id');
}
}
When I go into php tinker, get the first Lead ($lead = App\Leads::first();), create a new LeadHistory ($leadHistory = new App\LeadHistory;) and ($leadHistory->message = 'second one';) and ($leadHistory->status_id = 11;) then try to save the leadHistory ($leadHistory->lead()->save($lead);). I get this error message:
BadMethodCallException with message 'Call to undefined method Illuminate\Database\Query\Builder::save()'
Can someone point me in the right direction, I feel like I have been following the instructions given in Laracasts but can't seem to get the LeadHistory to save with the associated Lead ID.
You’re trying to call save() on a relation rather than a model I think.
Instead, “attach” your LeadHistory model to your Lead model:
$lead = Lead::create($leadAttributes);
$history = new LeadHistory($leadHistoryAttributes);
$lead->history()->attach($history);
You’ll need to rename your relation if you copy-and-paste the above code:
class Lead extends Model
{
public function history()
{
return $this->hasMany(LeadHistory::class);
}
}
I feel the name “lead history” is superfluous when you’re already working with a Lead model.
Try to save $leadHistory first:
$leadHistory->save();
And then:
$lead->lead_history()->save($leadHistory)
Correct me if I'm wrong, but since you already have a model instance of your target App\Leads, I think you should be able to simply access the id of that instance and inject it into a static create call:
$lead = App\Leads::first();
$leadHistory = App\LeadHistory::create([
'message' => 'second one',
'status_id' => 11,
'lead_id' => $lead->id
]);
Before being able to use the create method you'd have to make the properties you want to assign 'mass assignable', by defining a protected property called $fillable in your model:
class LeadHistory extends Model
{
protected $fillable = [
'message',
'status_id',
'lead_id'
];
public function lead()
{
return $this->belongsTo('App\Leads', 'lead_id', 'id');
}
}
This will effectively associate your new record with that lead, since the only thing the Eloquent model does in this regard is providing another way to describe the same relationships your database exercises.
Some other answers mention the attach() method of an Eloquent model. This method is used to attach two models with a many to many relationship (relationships defined with belongsToMany).

Resources