how retrieve relation between two model that other models have polymorphic relation with both of them - laravel

I have these two models: Tag and Translate, I have these tables: tags, translates, translatables, Tag model uses translatable.
my table columns is like this:
translates: translatables: tags:
id name lang | translate_id translatable_id translatable_type | id
-- ---- ---- | ------------- --------------- ----------------- | --
| |
I want to write a function to get current translation of tag based on app locale which means I should query on translates tables based on translatables table data and using translateble_id and translatable_type find id in the translate table and get the name based on lang.
do you know how i can do this?
this is translatable trait:
trait Translatable{
public function translates(){
return $this->morphToMany(Translate::class,'translatable','translatables','translatable_id','translate_id');
}
}
and this is how I get locale:
$locale=app()->getLocale();
I get tags translates with querybuilder like:
$locale=app()->getLocale();
$tags=DB::table('translates as T')
->where('lang','=',$locale)
->join('translatables as R','R.translate_id','=','T.id')
->where('R.translatable_type','=','App\Models\Tag')
->join('tags as M','R.translatable_id','=','M.id')
->get();
but i want to use a function in model and eloquent.

Related

Load model one to many relation eloquent way without primary key but on multiple overlapping fields

I'm working on an older project that I've been tasked to speed up certain parts of while we work on a complete re-write since the code is just badly maintained, poorly written and outdated for what it's suppose to do.
I stumbled into an issue to the core of the project and because of this I can't change it without breaking almost everything else. So I need to load a "relation" the eloquent way (using Planning:with('availability') but there isn't a real foreign ID, it rather laps with multiple fields.
Would there be a way to load it all in one query with the overlapping fields rather than have it load separately creating an n+1 problem?
+--------------+-----------------+
| Planning | Availability |
+--------------+-----------------+
| planning_id | availability_id |
| date | date |
| startHour | startHour |
| stopHour | stopHour |
| candidate_id | candidate_id |
| section_id | section_id |
+--------------+-----------------+
From the above example you can see the overlapping fields are date, startHour, stopHour, candidate_id and section_id.
I tried get...attribute but that still loads with n+1, I tried including it with ->with(['availabilities']) but that doesn't work since I ask for the
model and not the relation:
Edit for more clarity:
Planning Model:
public function availabilities()
{
return Availability::where('section_id', $this->section_id)
->where('candidate_id', $this->candidate_id)
->where('planningDate', $this->planningDate)
->where('startHour', $this->startHour)
->where('stopHour', $this->stopHour)
->get();
}
public function availabilities2()
{
return $this->hasMany('App\Models\Availability', 'candidate_id', 'candidate_id')
}
Controller:
$plannings = Planning::with(['availabilities'])->get();
$plannings = Planning::with(['availabilities2' => function ($query) {
// $this is suppose to be Planning model but doesn't work
$query->where('section_id', $this->section_id)
->where('planningDate', $this->planningDate)
->where('startHour', $this->startHour)
->where('stopHour', $this->stopHour);
// ---- OR ---- //
// Don't have access to planning table here
$query->where('section_id', 'planning.section_id')
->where('planningDate', 'planning.planningDate')
->where('startHour', 'planning.startHour')
->where('stopHour', 'planning.stopHour');
}])->get();
First of all to be able to load my relation I took one of the keys that matched and took the one which had the least matches which in my case was section_id.
So on my Planning model I have a function:
public function availabilities()
{
return $this->hasMany('App\Models\Availability', 'section_id', 'section_id');
}
This way I can load the data when needed with: Planning:with('availability').
However since I had a few other keys that needed to match I found a way to limit this relation by adding a subquery to it:
$planning = Planning::with([
'availabilities' => function ($query) {
$query->where('candidate_id', $this->candidate_id)
->where('startHour', $this->startHour)
->where('stopHour', $this->stopHour);
},
// Any other relations could be added here
])
->get();
It's not the best way but it is the only way I found it not getting too much extra data, while also respecting my relationship
When you want to use multiple fields in where() method you most insert a array in the where() method:
This document can help you
change your code to this:
return Availability::where([
['section_id', $this->section_id],
['candidate_id', $this->candidate_id],
['planningDate', $this->planningDate],
['startHour', $this->startHour],
['stopHour', $this->stopHour]
])->firstOrFail();

Confused with Laravel's hasOne Eloquent Relationships

I have a new Laravel 5.8 application. I started playing with the Eloquent ORM and its relationships.
There is a problem right away that I encountered.
I have the following tables. (this is just an example, for testing reasons, not going to be an actual application)
Login table:
--------------------------
| id | user | data_id |
--------------------------
| 1 | admin | 1 |
| 2 | admin | 2 |
| 3 | admin | 3 |
--------------------------
Data table:
--------------
| id | ip_id |
--------------
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
--------------
IP table:
----------------------
| id | ip |
----------------------
| 1 | 192.168.1.1 |
| 2 | 192.168.1.2 |
| 3 | 192.168.1.3 |
----------------------
What I wanted is to get the IP belonging to the actual login.
So I added a hasOne relationship to the Login table that has a foreign key for the Data table:
public function data()
{
return $this->hasOne('App\Models\Data');
}
Then I added a hasOne relationship to the Data table that has a foreign key for the IP table:
public function ip()
{
return $this->hasOne('App\Models\Ip');
}
Once I was done, I wanted to retrieve the IP address for the first record of the Login table:
Login::find(1)->data()->ip()->get();
But I get this error:
Call to undefined method Illuminate\Database\Eloquent\Relations\HasOne::ip()
What am I missing here and how can I get the IP of that login in the correct way? Do I need a belongsTo somewhere?
1st error: Wrong relationship definition
Laravel relationships are bi-directional. On one-to-one relationships, you can define the direct relationship (HasOne) and the inverse relationship (BelongsTo)
The direct relationship should be:
HasOne HasOne
[ Login ] <----------- [ Data ] <----------- [ IP ]
And the inverse relationship should be:
BelongsTo BelongsTo
[ Login ] -----------> [ Data ] -----------> [ IP ]
See Eloquent: Relationships - One-to-One docs for details on how defining it.
Note that you don't need to define both directions for a relationship unless you need it. In your case, I think you just need to define the belongsTo direction.
2nd error: You are calling the relationship method, not the relationship itself
When you do:
Login::find(1)->data()->ip()->get();
You are calling the method data that defines your relationship, not the related model. This is useful in some cases, but not on your case.
The correct is call the relationship magic property instead:
Login::find(1)->data->ip;
Note that we don't use the () and we do not need the get() here. Laravel takes care of loading it for us.
Use Eager Loading
Laravel Eloquent have a Eager Loading for relationships that's very useful in some cases because it pre-loads your relationships, and reduce the quantity of queries you do.
In the situation that you described (loading a single Login model) it doesn't make any performance improvement, but also it doesn't slow down.
It's useful when you load many models, so it reduces your database query count from N+1 to 2.
Imagine that you are loading 100 Login models, without eager loading, you will do 1 query to get your Login models, 100 queries to get your Data models, and more 100 queries to get your Ip models.
With eager loading, it will do only 3 queries, causing a big performance increase.
With your database structure:
Login belongsTo Data
Data hasOne Login
Data belongsTo IP
IP hasOne Data
After fixed your methods you can use of your relations like this
$login = Login::with(['data.ip'])->find(1);
You can try like this:
Login
public function data()
{
return $this->belongsTo('App\Models\Data', 'data_id');
}
Data
public function ip()
{
return $this->belongsTo('App\Models\Ip', 'ip_id');
}
$login = Login::with(['data.ip'])->find(1);
And inside data you will have ip like $login->data->ip.

Laravel | Polymorphic relationship from morphed model

I am creating a polymorphic relationship which defaults to null, as it doesn't have an association originally.
I then create the association and I want to be able to update the polymorphic table to have a link between the original and the morphed class.
post_images:
| id | post_id | width | height | created_at | updated_at
files:
| id | fileable_type | fileable_id | file_disk_id | created_at | updated_at
So basically, inside of the polymorphic table the type and the id they default to null as it has to be processed first and converted to a standard format etc.
I then create the associated row inside of the morphed table, however I am unsure how to update the polymorphic to have the type and the id from the morphed table.
I have tried to do something like $post->images()->create(...); and then try and do something like $file->where('id', $fileId)->firstOrFail()->associate($post); but this just seems to return null.
So the current process is:
Upload the file -> creates a row in the files table which contains the polymorphic relationships but columns are defaulted to null.
Standardize the file -> the file then gets converted and compressed into a standard format that is used for the site.
Create post_images row -> The site then creates a row for post_images table which then needs to link the update the files row to contain the new polymorphic relation.
Eloquent relationships:
File.php
public function fileable()
{
return $this->morphTo();
}
PostImage.php
public function file()
{
return $this->morphMany(File::class, 'fileable');
}
Post.php
public function images()
{
return $this->hasMany(PostImage::class);
}
I think you should just be able to call the save method on the relationship. For example:
$post->file()->save($file)

Doctrine inner join three tables

I'm trying to get Posts by Tags in Symfony 4 with Doctrine. I have three tables like this:
Post
------------------------------
| id | title | content | ... |
------------------------------
Tag
-------------
| id | name |
-------------
TagPost (which makes the association between tags and posts)
--------------------
| tag_id | post_id |
--------------------
There can be several tags by posts and a tag can be used for several posts, that's why I use an association table.
I already succeeded to get it but only with raw sql, I have tried multiple times with the query builder and no way to get it. Any advices ?
The query (working):
"SELECT post.id, post.title, post.author_id, post.content, post.datetime,
post.tile FROM post
INNER JOIN tag_post ON post.id = tag_post.post_id
INNER JOIN tag ON tag_post.tag_id = tag.id
WHERE tag.id = " . $tag_id;
Assuming you're writing this inside a method in your PostRepository (https://symfony.com/doc/3.3/doctrine/repository.html), you'd write:
$qb = $this->createQueryBuilder('post')
->select('post.id, post.title, post.author_id, post.content, post.datetime, post.tile')
->innerJoin('post.tag', 't')
->where('t.id = :tagid')
->setParameter('tagid', $tag_id)
;
$result = $qb->getQuery()->getResult();
Several things to note:
The createQueryBuilder method is not exactly the same in a Repository and in an EntityManager. The EntityManager one needs a ->from() method, while the Repository one guesses the 'from' table and takes instead just a constructor argument for the alias.
The field names, like 'id', 'author_id', 'datetime' etc should not be the names of the fields in your database, but the names of the properties of your doctrine entities. They might be the same, but they're probably camelCase instead of snake_case (e.g., 'authorId'), or they could be completely different. Check your entities to make sure.
Similarly, I'm assuming that the $post entities have a $tag field properly defined via doctrine as a ManyToMany relation. If that's the case, Doctrine will know on its own how to join that property by its name, so that the ->innerJoin method will only need one additional parameter: the alias. If you could include your entities definitions, that would help troubleshoot any additional problems.
Is post.tile (at the end of the SELECT clause) on purpose or is it a misspelling of post.title?

Joining an additional table with belongsToMany()?

This question is best illustrated by an example:
users
id
name
roles
id
name
role_user
user_id
role_id
rank_id
group_id
...
ranks
id
name
groups
id
name
I can easily eager load a users table by specifying the following relationship in my User.php model:
public function roles() {
return $this->belongsToMany('Role');
}
Which will output the table below when calling User::with('roles'):
User | Role
-------------
Jon | Admin
Jan | Mod
However I have no idea how to extend this to include:
User | Role | Rank | Group
-----------------------------
Jon | Admin | Boss | Blue
Jan | Mod | Minion | Red
What I've tried doing User::with('roles', 'ranks', 'groups') but that is certainly wrong since I'm telling Laravel there are rank_user and group_user intermediate tables too but there aren't. What is the correct way?
PS: I know it's better to separate the ranks and groups into their own relationship/pivot tables, this is simply an example.
EDIT: Closest example I can find for this: https://github.com/laravel/framework/issues/2619#issuecomment-38015154
You can just treat your model's relations methods as ordinary queries and build upon them:
public function roles() {
return $this->belongsToMany('Role')
->join('role_user', 'role_user.role_id', '=', 'roles.id')
->join('ranks', 'ranks.id', '=', 'role_user.rank_id')
->join('groups', 'groups.id', '=', 'role_user.group_id');
}
Relations queries like the above are not so intuitive to understand when they get too complex, so it may be better to rethink database design, but in theory it's possible to manipulate them.

Resources