Many to Many relationships with Laravel / Eloqeunt - php

I think I understand Eloquent for the most part, but I still have some trouble understanding many to many relationships.
I'm designing a sample database, and hopefully you folks can help me out understand the right way to do it.
Table 1: categories
cat_id
cat_name
Table 2: galleries
gallery_id
gallery_name
Table 3: galleryCategories
cat_id
gallery_id
So how would I go about my models? I found this topic, but it didn't seem quite right. maybe because it's a bit more complex relationship than what I need. I think the logic confused me because I'd expect products to have terms whereas terms having products in that example.
If you could please explain it with my sample, it'll help me better understand the belongsToMany() and hasMany() methods and their parameters. I'm also confused with some examples not taking any foreign key parameters.
Thanks already!

To make it work for your schema you need this:
// Category model
protected $primaryKey = 'cat_id';
public function galleries()
{
return $this->belongsToMany('Gallery', 'galleryCategories', 'gallery_id', 'cat_id');
}
// Gallery model
protected $primaryKey = 'gallery_id';
public function categories()
{
return $this->belongsToMany('Category', 'galleryCategories', 'cat_id', 'gallery_id');
}
// then:
$category = Category::first();
$category->galleries; // collection of Gallery models
To make it comply with Eloquent conventions, you would need:
// tables
categories: id, ...
galleries: id, ...
category_gallery: id, category_id, gallery_id [, timestamps]
// models
// Category
public function galleries()
{
return $this->belongsToMany('Gallery');
}
// Gallery
public function categories()
{
return $this->belongsToMany('Category');
}
The 2nd solution is way better if you want to make most of the framework, but if you want to rely on your schema, then you can make Eloquent adjust to it.
Note: hasMany is 1-m relation, belongsTo is its counterpart, while for many-to-many relationship with pivot table, you use belongsToMany on both ends.

Related

Eloquent way of defining a one-to-one relationship using intermediate table to avoid foreign key cycle

Laravel rookie here. I've got titles and reviews. A title can have many reviews, a review belongs to a title.
So far, my tables look unsurprisingly as follows:
titles:
- id (pk)
...
reviews:
- id (pk)
- title_id (fk)
...
Now, I want a title to have zero or one (so-called) top reviews.
My first instinct was to add a nullable top_review_id column to the titles table, but I wanted to avoid a cycle of foreign keys. So instead, I created a third table called top_reviews as follows:
top_reviews:
- id
- title_id (fk, unique)
- review_id (fk, unique)
That way a title is guaranteed to have at most one top review and a review cannot be the top review for multiple titles. (I do realise that it is still possible to have a top review entry where the review actually belongs to a different title, but that's okay.)
My question is how do I wire that up cleanly in Laravel (7.x) ideally using Eloquent ORM relationships and following the framework's best practices?
So far I've got this:
class Title extends Model {
public function reviews() { return $this->hasMany(Review::class); }
public function topReview() { /* ??? */ }
}
class Review extends Model {
public function title() { return $this->belongsTo(Title::class); }
}
I've considered the following:
I could manually build something ugly like return Review::find(DB::table('top_reviews')->select('review_id')->where('title_id', $this->id)->get());, but I suspect there is a nicer Laravelesque way for these trivial relationships.
Simply using hasOne() doesn't seem to be the solution either since it will assume a different table name (namely reviews instead of top_reviews) and there is no way to specify a custom table.
Defining a model TopReview seems clumsy, but perhaps it is my best bet. I suppose that would allow me to define topReview() as hasOneThrough(Review, TopReview).
Feel free to correct me if I'm on the wrong track.
Thanks.
With belongsToMany relationship
class Title extends Model
{
public function reviews()
{
return $this->hasMany(Review::class);
}
public function topReview()
{
return $this->belongsToMany(Review::class, 'top_reviews', 'title_id', 'review_id');
}
}
Anyways you can skip that top_reviews table and just save top_review_id into your titles table and I think that's more efficient
Depend on you requirement
You one only one top reviews so I think you should Has One Through but if you want to have multi top reviewer on 1 title you should use Many To Many.
Has One Through
I will remove title_id if i use it
top_reviews:
- id
- review_id (fk, unique)
class Title extends Model {
public function topReview() {
return $this->hasOneThrough(TopReview::Class, Review::class);
}
}
Many To Many
top_reviews:
- id
- title_id (fk, unique)
- review_id (fk, unique)
class Title extends Model
{
public function topReview()
{
return $this->belongsToMany(Review::class, 'top_reviews', 'title_id', 'review_id');
}
}
but as you describe above. I think I will use has one through.

Get name instead of ID one to one relationship

I have a make table and post table. Make table saves make names as make_code and make_name.
Post table has a column make. While saving a post, it will save make in make_code.
While displaying in blade, I want it to display as make_name. How can I do it?
Currently {{$post->make}} gives me make_code. I need it to show make_name.
I think its a one-to-one relationship that's needed. I tried putting it in model but did not work. How can I achieve it?
MAKE MODEL
class Make extends Model
{
public function make()
{
return $this->belongsTo(App\Post::class);
}
}
POST MODEL:
class Post extends Model
{
protected $table = 'posts';
}
Update
As Tim Lewis noticed:
the relationships can't be named make, as that's a conflict.
Assuming that the your relationship work like this:
a Make has many Post
a Post belongs to a Make object.
| Note: Correct me if I'm wrong.
So, if this is correct, you should define your relationships like this:
Post.php
public function make_rel()
{
return $this->belongsTo(Make::class, 'make', 'make_code');
}
Make.php
public function posts()
{
return $this->hasMany(Post::class, 'make', 'make_code');
}
Check the One-to-Many and One-to-Many (Inverse) relationship sections of the documentation.
So, you could do in your controller (or wherever you want):
$post = Post::find(1);
dd($post->make_rel->make_name); // 'Harley Davidson'
Additionally, you could create a computed property as a shorcout to access this related property in your Post model:
Post.php
// ...
public function getMakeNameAttribute()
{
return $this->make_rel->make_name;
}
Now, you can access it like this:
$post = Post::find(1);
dd($post->make_name); // 'Harley Davidson'
Suggestion
As a suggestion, I strongly advice you to change your foreign key column from make to make_id (in your 'posts' table) to avoid conflicts. Also, you could relate the post to the make primmary key instead of a custom key given the fact that this link is almost invisible and it is handled by Laravel. This would speed up the execution of the query because primmary id's are indexed by default.

Relationship with the same table (Many to Many?) - Laravel

In my database I have to save people and the data of people that have a relationship with the first. For example: father, mother, son, daughter, etc
Then the design of the database, I did it in the following way Is a relation many-to-many because, a person have many people related to it.
But I'm not sure if is ok..
class Person extends Model
{
protected $fillable = [
'name',
'surname',
'profile_picture'
.....
];
public function relationships()
{
return $this -> belongsToMany(Person::class);
}
}
When I create the relation I create a thirth migration table call person_person to save the ids and a description (father, mother, son, daughter)
it's ok describe the relationship this way?
public function relationships()
{
return $this -> belongsToMany(Person::class);
}
What should I add to complete successful this relationship?
You'll need to define the table, primary and foreign key, as Laravel likely can't determine this (it'll try, but it probably won't succeed). Something like:
public function relationships(){
return $this->belongsToMany(Person::class, "person_person", "id_person", "id_person_related")
->withPivot(["parentesco"]);
}
or similar. Table name is a bit awkward, consider something else, like 'relationships' or something that conveys more meaning.
See https://laravel.com/docs/5.8/eloquent-relationships#many-to-many for details on how to define additional parameters for belongsToMany()
Laravel provide a nice convention for manyToMany relationship for two different tables but this convention does not work when we need manyToMany relationship in the same table.
solution steps:
create a pivot table and give it a meaningful name like relatives(person_person is not suitable)and create tow columns in this table persion_id and relative_persion_id.
with classname, pass other three arguments as the Table name (relatives), persion_id and relative_persion_id.
this link has perfect solutuion https://laracasts.com/series/laravel-6-from-scratch/episodes/58
public function relationships() { return $this->belongsToMany(Person::class,'relatives','persion_id','relative_persion_id'); }

Laravel polymorphic relationship with pivot

Assume I have posts and videos that can be seen by multiple users.
- users
- id
- posts
- id
- videos
- id
- user_accessables (pivot)
- id
- user_id
- accessable_id
- accessable_type
In an example like that, I have set my User relationship like so but something feels wrong
class User extends Model {
public function posts() {
return $this->morphedByMany(
Post::class,
'accessable',
'user_accessables'
);
}
public function videos() {
return $this->morphedByMany(
Video::class,
'accessable',
'user_accessables'
);
}
public function allowedEntities() {
return ($this->posts)->merge($this->videos);
}
}
With the allowedEntities() I can get a collection of both models joined together.
However, I think the use of polymorphic relationship is returning a collection of entities through relationship rather than needing a combiner relationship, right?
I am having problems with understanding polymorphic with pivot table (the tag example in documentation doesn't seem like same scenario).
Because now I can't do:
$collection = collect(); // multiple models of Video & Post
$user->allowedEntities()->sync($collection);
As #Jonas Staudenmeir said is not possible to have a relationship that returns all related model, BUT you can define a method on the model that returns a query builder object with all entities you need (search with on the docs).

Applying hasManyThrough to deeper relationships

The Laravel docs seem to indicate that the hasManyThrough declaration can only be used for relationships that are two levels "deep". What about more complex relationships? For example, a User has many Subjects, each of which has many Decks, each of which has many Cards. It's simple to get all Decks belonging to a User using the hasManyThrough declaration, but what about all Cards belonging to a User?
I created a HasManyThrough relationship with unlimited levels: Repository on GitHub
After the installation, you can use it like this:
class User extends Model {
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
public function cards() {
return $this->hasManyDeep(Card::class, [Subject::class, Deck::class]);
}
}
As stated in the comments, hasManyThrough doesn't support this level of specificity. One of the things you can do is return a query builder instance going the opposite direction:
//App\User;
public function cards()
{
Card::whereHas('decks', function($q){
return $q->whereHas('subjects', function($q){
return $q->where('user_id', $this->id);
});
});
}
We're going from Cards -> Decks -> Subjects. The subjects should have a user_id column that we can then latch onto.
When called from the user model, it would be done thussly:
$user->cards()->get();
Well, actually the best solution will be put the extra column to Card table - user_id, if you have so frequent needs to get all cards for the user.
Laravel provides Has-Many-Through relations for 2-depth relation because this is very widely often used relation.
For the relations Laravel does not support, you need to figure out the best table relationship yourself.
Any way, for your purpose, you can use following code snap to grab all cards for the user, with your current relation model.
Assumption
User has hasManyThough relationship to Deck,
So Project model will have following code:
public function decks()
{
return $this->hasManyThrough('Deck', 'Subject');
}
Deck has hasMany relationship to Card
Code
$deck_with_cards = $user->decks()->with("cards")->get();
$cards = [];
foreach($deck_with_cards AS $deck) {
foreach ($deck->cards as $c) {
$cards[] = $c->toArray();
}
}
Now $cards has all cards for the $user.

Categories