Laravel belongsToMany to return specific columns only - php

So I have a many to many relationship between Users and Photos via the pivot table user_photo. I use belongsToMany('Photo') in my User model. However the trouble here is that I have a dozen columns in my Photo table most I don't need (especially during a json response). So an example would be:
//Grab user #987's photos:
User::with('photos')->find(987);
//json output:
{
id: 987,
name: "John Appleseed",
photos: {
id: 5435433,
date: ...,
name: 'feelsgoodman.jpg',
....// other columns that I don't need
}
}
Is it possible to modify this method such that Photos model will only return the accepted columns (say specified by an array ['name', 'date'])?
User.php
public function photos()
{
return $this->belongsToMany('Photo');
}
Note: I only want to select specific columns when doing a User->belongsToMany->Photo only. When doing something like Photo::all(), yes I would want all the columns as normal.
EDIT: I've tried Get specific columns using "with()" function in Laravel Eloquent but the columns are still being selected. Also https://github.com/laravel/laravel/issues/2306

You can use belongsToMany with select operation using laravel relationship.
public function photos()
{
return $this->belongsToMany('Photo')->select(array('name', 'date'));
}

Im assuming you have a column named user_id. Then you should be able to do the following:
public function photos()
{
return $this->belongsToMany('Photo')->select(['id', 'user_id', 'date', 'name']);
}
You have to select, the foreign key, else it will have no way of joining it.

Specifying the exact columns you want for the Photos relationship will likely end up biting you in the butt in the future, should your application's needs ever change. A better solution would be to only specify the data you want to return in that particular instance, i.e. the specific JSON response you're delivering.
Option 1: extend/overwrite the toArray() Eloquent function (which is called by toJson()) and change the information returned by it. This will affect every call to these methods, though, so it may end up giving you the same problems as doing select() in the original query.
Option 2: Create a specific method for your JSON response and call it instead of the general toJson(). That method could then do any data building / array modifications necessary to achieve the specific output you need.
Option 3: If you're working with an API or ajax calls in general that need a specific format, consider using a library such as League/Fractal, which is built for just such an occasion. (Phil is also working on a book on building APIs, and it doesn't suck.)

Related

Retrieve Parent Model Through Pivot Table Laravel

I'm currently struggling with retrieving data towards a parent model. I'll drop my database, classes, and things I've tried before.
I have 4 tables: sales_orders, products, work_orders, and product_sales_order (pivot table between sales_orders and products).
SalesOrder.php
class SalesOrder extends Model
{
public function products()
{
return $this->belongsToMany(Product::class)
->using(ProductSalesOrder::class)
->withPivot(['qty', 'price']);
}
}
ProductSalesOrder.php
class ProductSalesOrder extends Pivot
{
public function work_orders()
{
return $this->hasMany(WorkOrder::class);
}
public function getSubTotalAttribute()
{
return $this->qty* $this->price;
}
}
WorkOrder.php
class WorkOrder extends Model
{
public function product_sales_order()
{
return $this->belongsTo(ProductSalesOrder::class);
}
public function sales_order()
{
return $this->hasManyThrough(
ProductSalesOrder::class,
SalesOrder::class
);
}
}
So, what I want to retrieve sales order data from work order since both tables don't have direct relationship and have to go through pivot table and that is product sales order. I've tried hasOneThrough and hasManyThrough but it cast an error unknown column. I understand that error and not possible to use that eloquent function.
Is it possible to retrieve that sales order data using eloquent function from WorkOrder.php ?
You cannot achieve what you want using hasOneThrough as it goes from a table that has no ID related to the intermediate model.
In your example you are doing "the inverse" of hasOneThrough, as you are going from a model that has the ID of the intermediate model in itself, and the intermediate model has the ID of your final model. The documentation shows clearly that hasOneThrough is used exactly for the inverse.
So you still should be able to fix this, and use a normal relation as you have the sales_orders_id in your model SuratPerintahKerja, so you can use a normal relation like belongsTo to get just one SalesOrder and define it like this:
public function salesOrder()
{
return $this->belongsTo(SalesOrder::class, 'sale_orders_id');
}
If you want to get many SalesOrders (if that makes sense for your logic), then you should just run a simple query like:
public function salesOrders()
{
return $this->query()
->where('sale_orders_id', $this->sale_orders_id)
->get();
}
Have in mind that:
I have renamed your method from sales_order to salesOrder (follow camel case as that is the Laravel standard...).
I have renamed your method from sales_order to salesOrders for the second code as it will return more than 1, hence a collection, but the first one just works with one model at a time.
I see you use sale_orders_id, but it should be sales_order_id, have that in mind, because any relation will try to use sales_order_id instead of sale_orders_id, again, stick to the standards... (this is why the first code needs more parameters instead of just the model).
All pivot tables would still need to have id as primary and auto incremental, instead of having the id of each related model as primary... Because in SuratPerintahKerja you want to reference the pivot table ProdukSalesOrder but it has to use both produks_id (should have been produk_id singular) and sale_orders_id (should have been sales_order_id). So if you were able to use something like produk_sales_order_id, you could be able to have better references for relations.
You can see that I am using $this->query(), I am just doing this to only return a new query and not use anything it has as filters on itself. I you still want to use current filters (like where and stuff), remove ->query() and directly use the first where. If you also want to add ->where('produks_id', $this->produks_id) that is valid and doesn't matter the order. But if you do so, I am not sure if you would get just one result, so ->get() makes no sense, it should be ->first() and also the method's name should be salesOrder.
Sorry for this 6 tip/step, but super personal recommendation, always write code in English and do not write both languages at the same time like produks and sales orders, stick to one language, preferrably English as everyone will understand it out of the box. I had to translate some things so I can understand what is the purpose of each table.
If you have any questions or some of my code does not work, please tell me in the comments of this answer so I can help you work it out.
Edit:
After you have followed my steps and changed everything to English and modified the database, this is my new code:
First, edit ProductSalesOrder and add this method:
public function sales_order()
{
return $this->belongsTo(SalesOrder::class);
}
This will allow us to use relations of relations.
Then, have WorkOrder as my code:
public function sales_order()
{
return $this->query()->with('product_sales_order.sales_order')->first();
}
first should get you a ProductSalesOrder, but then you can access ->sales_order and that will be a model.
Remember that if any of this does not work, change all the names to camelCase instead of kebab_case.

Call method inside of other model

I got 2 main tables users and pictures. Each user can have same picture(and vice versa) so it have got "hasMany"(and pivot table in between of them) relationship in Elaquent and it works well. Also, I do have as separate table pictures_details. I wonder, is it possible to get the details of pictures_details when accessing from user to picture model?
public function getImages($id, Images $images)
{
return $users->with('images')->find($id);
}
So when I have call like that, can I also get data of pictures_details table? I have following method in my Image model
public function imageDetails(): BelongsTo
{
return $this->belongsTo(ImageDetails::class, 'image_details_id', 'id');
}
So I thought something like
return $users->with('images')->find($id)->with('imagedetails');
Will work but its not. Can you tell me how to achieve this? Or it is wrong approach?
I want to edit ArSeN's answer. May be it would be more correct if
$users->with(['images.imagedetails'])->find($id);
find($id) returns a single model, not the eloquent query, you cant do joins there.
Switch them around:
$users->with('images')->with('imagedetails')->find($id);

One To Many Relationship in Eloquent with JSON field

I'm trying to create a connection between a JSON field in my database and a table which stores music by ID. So, I have a table called "playlists" which has a field called "songs". In this "songs" field I have a array[] of song ID's e.g. [1,2]. I tried the following code to make a relationship between these two tables:
class Playlist extends Model
{
protected $table = 'playlists';
public function songs()
{
return $this->hasMany('App\Music', 'id');
}
}
I used the foreign_key id because of the songs table which has a id field.
The code I used to retrieve the playlist from the controller is as follows:
$playlist = Playlist::find($id)->songs;
print_r($playlist);
Which outputs:
[1,2]
I most probably did something wrong, not understanding the relationships correctly. Could someone explain how this works? I looked up the documentation but did not get any wiser.
Laravel has no native support for JSON relationships.
I created a package for this: https://github.com/staudenmeir/eloquent-json-relations
If you rename the songs column to song_ids, you can define a many-to-many relationship like this:
class Playlist extends Model
{
use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
protected $casts = [
'song_ids' => 'json',
];
public function songs()
{
return $this->belongsToJson('App\Music', 'song_ids');
}
}
class Music extends Model
{
use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
public function playlists()
{
return $this->hasManyJson('App\Playlist', 'song_ids');
}
}
Although this is a very old post but I will go ahead and drop my own opinion for my future self and fellow googlers.....
So, If I got this question correctly, you are trying to use a JSON field for a relationship query. This issue I have stumbled across a couple of times, at different occasions for different use-cases. With the most recent being for the purpose of saving a couple of Ids belonging to different tables, in a single JSON field on a given table (While I keep pondering on why the Laravel guy won't just add this functionality already! I Know Pivots, Data Normalization etc....But I'm pleading for the 1%). Until I came across this post on Laracast that worked like a charm.
Apologies for the long intro, let me get right into it....
On your Playlist model (in Laravel 8.0 and a few older versions I can't really keep track of) you can do something like so;
public function songs()
{
$related = $this->hasMany(Song::class);
$related->setQuery(
Song::whereIn('id', $this->song_ids)->getQuery()
);
return $related;
}
I have the really good solution for keeping data in column on json format. It help me on previous project online shop
https://scotch.io/tutorials/working-with-json-in-mysql

Laravel Eloquent query unexpected result

I've found some query result really unexpected.
It's Laravel 5.2
We have following entity:
User with method:
public function roles() : BelongsToMany
{
return $this->belongsToMany(Role::class)->withPivot('timestamp');
}
Each User can have many roles, so we have also Role entity (but it doesn't matter much in my question) and pivot table user_role with timestamp field (and ids of course), because we hold information about time, when User achieved specific role.
I want to get all Users with theirs last assigned Role
When I create query (in User context in some repository):
$this->with(['roles' => function($query) {
$query->orderBy('timestamp', 'desc');
}])->all();
the result will contain Users with Roles entities inside itself ordered by timestamp - it's ok. But I want to retrieve only one last role inside each User entity not all ordered.
So...
$this->with(['roles' => function($query) {
$query->orderBy('timestamp', 'desc')->limit(1);
}])->all();
And then I retrieve Users but only User which achieved some Role for the very last time contains it! All the other Users have their roles field containing empty array.
Why ordering was performed on each Users relation separately, but when I added limit it behaved like a global limit for all.
It drives me crazy...
Thanks for advices.
EDIT
I've created lastRoles() method to get all Roles ordered desc. But all, retrieving one is impossible.
public function lastRoles() : BelongsToMany
{
return $this->BelongsToMany(Roles::class)->withPivot('timestamp')->latest('timestamp');
}
And for testing:
$users = (new User())->with('lastRoles')->get();
But now I must iterate over Users and invoke lastRoles() on each one:
foreach ($users as $user) {
var_dump($user->lastRoles()->get()->first()->name);
}
Then I retrieve names of latest Roles assigned to each User.
So... There is no way to do it in one query? This is the only way?
For this to work, you would need a helper function:
public function latestRole()
{
return $this->hasOne(Role::class)->withPivot('timestamp')->orderBy('timestamp', 'DESC');
}
And then:
$this->with('latestRole')->get();
Credits to this awesome article.
When you eager load a relationship with query constraint(s), the query will be run once to load all relationships, not each one individually. This is the expected behavior. Think about it, eager loading exists to turn many queries into one query in order to optimize performance. There is only one query executed, so your limit constraint will limit the entire result set, rather than on a per model basis.
To circumvent this, you could try creating another belongsToMany method that adds the desired limit constraint. The following code is untested:
public function lastRole() : BelongstoMany
{
return $this->belongsToMany(Role::class)
->withPivot('timestamp')
->orderBy('timestamp', 'desc')
->limit(1);
}
Assuming this works, you can then simply change the relationship method from roles to lastRole and remove your query constraint:
$this->with('lastRole')->all();

Laravel nested relationship not working

I have a user model which stores basic user information such as username, password etc.
There are also 3 types of user, Student, Staff and Parent. Each type also has a seperate model. For example, there is a Student model which belongs to a User model.
I also have a relationships table, which stores relationships between students and parents. This relationship is stored in the User model.
If I do something like:
App\Student::first()->user->relations;
It happily returns a collection of related parents.
In my Students model, I have a method called hasParent() which accepts a given user ID, and checks to ensure the student has a parent with that id. In that method, I have the following:
public function hasParent($parent)
{
return $this->user->relations->where('id', $parent)->count() === 1;
}
However, this returns an error Cannot call 'where' on a non-object. If I debug further, $this->user->relations returns an empty array.
The problem is, like above, if I call the methods separately, I get the results I want.
So to clarify, if I run:
App\Student::first()->user->relations;
This returns a collection of users just fine.
In my Student model however, if I call:
$this->user
Then I get the correct student
If I call
$this->user->relations
I get an empty array. Which doesn't make sense! Can anyone shed any light on this, or what I might be doing wrong? If you need any further info, please let me know.
You need to call where on the relation like below.
public function hasParent($parent)
{
return $this->user->relations()->where('id', $parent)->count() === 1;
}
See the parenthesis after the relations. If you call the relation without the parenthesis Laravel returns you a collection. To get the builder you need to call the relation with the parenthesis.
I'd suggest - to avoid creating a huge query overhead (which you'll do by calling where and count on the Query builder, not the collection) - to do what you're doing already, except using Illuminate Collections filter-method:
public function hasParent($parent)
{
return $this->user->relations->filter(function($relation) use ($parent){return $entity->id === $parent;})->count() === 1;
}

Categories