My Message model has many MessageAttachment
Following code:
$message = Message::find(1);
return $message->attachments;
Outputs:
[
{
"id": 1,
"message_id": 1,
"attachable_id": 1,
"attachable_type": "Item",
},
{
"id": 2,
"message_id": 1,
"attachable_id": 1,
"attachable_type": "Photo",
}
]
Now, while this is all great, I'd like a way to fetch the model, the MessageAttachment is referring to (i.e. Photo, or Item). Something like:
return $message->attachments[0]->attachable; // outputs the Item 1 model
This is where I get stuck. How would you do that, in a clean and simple way, using above structure, or something similar?
this should work:
$messages = Message::find(1);
foreach($messages as $key => $value){
$model = $value->attachable;
$array[$key] = $model::find($value->attachable_id);
}
dd($array);
Simple. The solution:
class Message extends \Eloquent {
public function attachments() {
return $this->hasMany('MessageAttachment');
}
}
class MessageAttachment extends \Eloquent {
public function attachable() {
return $this->morphTo();
}
}
Doing:
$message = Message::find(1);
$message->attachments[0]->attachable; // returns the model
Best practice is to fetch the items directly with the realtions. You can do this like this:
Messages::with('attachments', 'attachments.photo')->get();
Here photo is a relation of attachments. All relations will be fetched. If you have a single message you can access the relation by:
$message->attachments->first()->photo
Related
I have a relational database whose models look like this:
Restaurant:
class Restaurant extends Authenticatable
{
public function dishes()
{
// return $this->hasMany(Dish::class);
return $this->belongsToMany('App\Models\Dish');
}
}
Dish:
class Dish extends Model
{
public function orders()
{
return $this->belongsToMany('App\Models\Order')->withPivot('quantity');
}
public function restaurant()
{
return $this->belongsToMany('App\Models\Restaurant');
}
}
Order
class Order extends Model
{
public function dishes()
{
return $this->belongsToMany('App\Models\Dish');
}
}
I'm retrieving all orders belonging to the restaurant by id like so:
public function index($id)
{
$user_id = $id;
$dishes = Dish::where('restaurant_id', $user_id)->get();
foreach ($dishes as $dish => $value) {
$orders = $value->orders;
foreach ($orders as $order => $value) {
$response[] = $value;
}
}
return response()->json($response);
}
This works, but I would like to group these results by order_id in some way, so that I get returned something similar to this:
{
"id": 28,
"status": 1,
"address": "Fish Street",
"user_name": "Artyom",
"user_surname": "Pyotrovich",
"phone": "351 351 643 52",
"email": "artyom#restaurant.com",
"total": 35.8,
"created_at": "2021-11-17T10:44:58.000000Z",
"updated_at": "2021-11-17T10:44:58.000000Z",
"dishes": [{
"dish_id": 22,
"quantity": 3
},
{
"dish_id": 23,
"quantity": 1
}]
}
Is this possible by simply changing the Eloquent query or do I need to perform several operations on the result of my current query? Also interested in what is actually the best practice in these cases.
Thanks in advance
You can use the ::with() method to get all the desired relations, this is much cleaner then a foreach rewrite.
Example:
$orders = Order::with('dishes')
->whereHas('dishes', function(Builder $dishes) use ($user_id) {
$dishes->where('restaurant_id', $user_id);
})->get();
return response()->json( $order );
Small note:
Separate the user id from the restaurant id, what if the user has multiple restaurants?
I am creating a Rest API project that I have an Artist that have a Musical Genre object as an attribute, and I created these two classes like this:
Artist:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Artist extends Model
{
public $table = 'artist';
public $timestamps = false;
protected $fillable = [
'name', '...'
];
//...
public function musical_genre() {
return $ this-> belongsTo (MusicalGenre::class, 'musical_genre');
}
}
Musical Genre:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class MusicalGenre extends Model
{
public $table = 'musical_genre';
public $timestamps = false;
protected $fillable = [
'name', '...'
];
// ...
function parent_genre(){
return $this->hasMany(MusicalGenre::class, 'parent_genre');
}
}
As you can see, Genre Musical have an auto relationship, because an Genre Musical can have an Parent Genre. This works nice, but the response I get is like this:
{
"artist": [
{
"id": 14,
"name": "Britney Spears",
"musical_genre": {
"id": 4,
"name": "electropop",
"parent_genre": 1
}
}
]
}
But I wish it could be like this:
{
"artist": [
{
"id": 14,
"name": "Britney Spears",
"musical_genre": {
"id": 4,
"name": "electropop",
"parent_genre": {
"id:" 1,
"name": "pop"
}
}
}
]
}
In controller I have this line to call Artist with Genre:
$artist= Artist::with(['...', 'musical_genre'])->get();
I already tried to call it with musical_genre.parent_genre but I got a empty parent.
There is a way to make the response like I wished in Laravel, with all atributes and not only the id? I didnt found how can I do this in Laravel docs.
EDIT:
I already tried using
$artist->load('parent_genre');
too.
EDIT 2:
My last try was in show() method inside the controller, like this:
public function show($id)
{
$artist = Artist::with(['login', 'musical_genre'])->findOrFail($id);
$musical_genre = MusicalGenre::findOrFail($artist->musical_genre);
$parent_genre = MusicalGenre::findOrFail($musical_genre->parent_genre);
if ($parent_genre){
$musical_genre->parent_genre = $parent_genre;
}
$artist->musical_genre = $musical_genre;
return response()->json(['artist' => $artist], 200);
}
And I got the same response, just with the "parent_genre": 1. I tried to get only $musical_genre->parent_genre and I got this body:
"musical_genre": {
"id": 4,
"name": "electropop",
"parent_genre": {
"id:" 1,
"name": "pop"
"parent_genre": null
}
}
like I wished. Maybe it's some Laravel limitation, not showing artist->musical_genre->parent_genre full body?
Add a $with attribute to the MusicalGenre model like so:
class MusicalGenre extends Model
{
public $table = 'musical_genre';
public $timestamps = false;
protected $with = ['parent_genre:id,name'];
.....
}
See: https://laravel.com/api/5.6/Illuminate/Database/Eloquent/Model.html#property_with
When you call the MusicalGenre model, it loads the parent_genre automatically. The 'id,name' in it limits the retrieved model to the two fields, if you want to to load all, just use only 'parent_genre' only.
Simply call it this way and you have it loaded with the response:
$artist= Artist::with('musical_genre')->get();
Please try this for nested eager loading:
$artist= Artist::with(['musical_genre'=> function ($query) {
$query->with(['parent_genre']);
}])->get();
In my case, I have two table like users table and rating table.
In user table, I'm storing user's personal details like name, email etc,
In rating table, I'm storing user_id and rating(rating will be in numbers like 1,2,3,4 and 5)
I have created relationship two tables
here is the relation
//User Model
public function ratings()
{
return $this->hasMany(Rating::class);
}
//Rating model
public function user()
{
return $this->belongsTo(Consultant::class);
}
I can able to display get data with eager loading
$data = User::with('ratings')->get();
The Response I'll get from eager load is
[
{
"id": 1,
"cunsultant_name": "Quincy Jerde",
"contact_number": "882-904-3379",
"ratings": [
{
"user_id": 1,
"rating_for_user": 3
},
{
"user_id": 1,
"rating_for_user": 5
},
{
"user_id": 2,
"rating_for_user": 3
}
]
},
{
"user_name": "Alene Dicki",
"contact_number": "247.604.8170",
"ratings": [
{
"id": 4,
"user_id": 3,
"rating_for_user": 3
}
]
}
]
So how can I get an average rating for every user with eager loading?
To get the average rating with eager loading you can do
$user->ratings()->avg('rating_for_user');
This will always append average_rating field in product. I use morph relation for ratings but you can use any relation appropriate for your situation.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Product extends Model
{
protected $guarded = [];
protected $appends = ['average_rating'];
public function ratings()
{
return $this->morphMany(Rating::class, 'rateable');
}
public function getAverageRatingAttribute()
{
return $this->ratings()->average('value');
}
}
You can do it like this,
$data = User::with('ratings')
->join('Rating table','user.id','=','Rating table.user_id')
->select('user.*',DB::raw('avg(rating_for_user)'))
->get();
Modify the code as per your need.
I hope it help.
If you want to get ratings of multiple users you can do like this.
$users = User::where('type', 'instructor')->get();
foreach ($users as $user) {
$user['ratings'] = $user->ratings()->avg('rate');
}
return $users;
You can get avg rating like this,
$product=Products::where('id',$productid);
$data=$product->with('ratings')->get();
foreach($data as $d) {
return $d->ratings->avg('rating');
}
I have added code for product avg rating where two model like below:
Product Model:
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\URL;
class Products extends Model {
protected $table = "products";
public $timestamps = false;
public function ratings()
{
return $this->hasMany("App\Model\Reviews","p_id");
}
}
Review Model:
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
class Reviews extends Model
{
protected $table = "product_reviews";
public $timestamps = false;
//Rating model
public function products()
{
return $this->belongsTo("App\Model\Products");
}
}
I have two models, Foo and Bar:
class Foo extends Eloquent {
protected $table = 'foos';
protected $fillable = array('name');
public $timestamps = false;
public function bars() {
return $this->belongsToMany('bar','foos_bars');
}
}
class Bar extends Eloquent {
protected $table = 'bars';
protected $fillable = array('name');
public $timestamps = false;
public function foos() {
return $this->belongsToMany('foo','foos_bars');
}
}
I'm trying to get all the Foo models and their related bars like this and then output it in json:
class FooController extends \BaseController {
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index()
{
$foos = Foo:with('bars')->get();
return Response::json(array(
'foos' => $foos->toArray()
),200);
}
}
The problem I'm having is with the output. This will output something like this:
{
"foos": [
{
"id": 1,
"name": "Foo 1",
"bars": [
{
"id": 1,
"name": "Bar 1",
"pivot": {
"foo_id": 1,
"bar_id": 1
}
},
{
"id": 2,
"name": "Bar 2",
"pivot": {
"foo_id": 1,
"bar_id": 2
}
},
{
"id": 3,
"name": "Bar 3",
"pivot": {
"foo_id": 1,
"bar_id": 3
}
}
]
}
]
}
But all I really want is:
{
"foos":
[{
"id": 1,
"name": "Foo 1",
"bars": [1, 2, 3]
}]
}
Right now, I've been iterating over $foos like this to get the right code:
foreach($foos as &$foo) {
$bar_ids = array();
foreach($foo->bars as $bar) {
$bar_ids[] = $bar->pivot->bar_id;
}
unset($foo->bars);
$foo->bars = $bar_ids;
}
But this seems to complicated and adds a lot of complexity I'm trying to get rid of. Is there a simpler way to get the desired output?
Thanks for any assistance.
Use transformers/presenters for such thing, fractal is one you could consider. However, you can do pretty much the same easily with Eloquent:
// Foo
protected $hidden = ['bars']; // don't show the collection in json
protected $appends = ['barsIds']; // but show additional info you need via accessor
public function getBarsIdsAttribute()
{
return $this->bars->lists('id');
}
Now you can use the accessor directly:
$foo->barsIds; // [1,5,10]
and it is automaticly appended to the toArray/toJson output:
{
"foos": [
{
"id": 1,
"name": "Foo 1",
"barsIds": [1,5,10]
}
]
}
IMPORTANT When you want to output collection of foos, then remember to eager load bars relation, otherwise you will experience N+1 query issue.
You can use the lists method.
$foo->bars->lists('id');
Will give you an array of foo's bar ids.
Available on:
Illuminate\Database\Eloquent\Builder::lists
Illuminate\Database\Query\Builder::lists
Illuminate\Support\Collection::lists
some additional advice:
To return the data in the format you want, it might be nice to use transformers, like from the fractal package (or make a simpler version on your own, depending on your requirements).
The idea is to have something in the middle to format your data instead of dumping your models to a response in your controllers. It could directly take care of your issue and have added benefits like casting properties to types (like boolean and integers) as well as insulating your controllers from database changes.
A transformer could look something like this:
class FooTransformer extends TransformerAbstract
{
/**
* Transform this item object into a generic array.
*
* #param Foo $foo
* #return array
*/
public function transform(Foo $foo)
{
return [
'id' => (int) $foo->id,
'name' => $foo->name,
'bars' => $foo->bars->lists('id'),
];
}
}
It sounds like you want a GROUP CONCAT in the foo query, for which you'd need to use DB::raw().
There's also the lists() method, e.g.
public function barsIds() {
return $this->belongsToMany('bar','foos_bars')
->lists('id');
}
I'm building a client application that needs to have the ids of related models in my server API response.
In my example I've got two models, a Post and a Tag model. The relationship between them is many-to-many, so a pivot table is required.
class Post extends Eloquent {
protected $fillable = [ 'title', 'body' ];
public function tags()
{
return $this->hasMany('Tag');
}
}
class Tag extends Eloquent {
protected $fillable = [ 'title' ];
public function posts()
{
return $this->belongsToMany('Post');
}
}
I've got a resourcefull controller set up on the /api/posts route like this:
class PostsController extends \BaseController {
public function index()
{
$posts = Post::all();
return Response::json([ 'posts' => $posts->toArray() ]);
}
}
This will return a response much like this one:
{
"posts": [
{
"title": "Laravel is awesome",
"body": "Lorem Ipsum..."
},
{
"title": "Did I mention how awesome Laravel is?",
"body": "Lorem Ipsum..."
}
]
}
What I'm looking for is an easy way to include the ids of the related Tags model in the response like this:
{
"posts": [
{
"title": "Laravel is awesome",
"body": "Lorem Ipsum...",
"tags": [ 1, 2, 3 ]
},
{
"title": "Did I mention how awesome Laravel is?",
"body": "Lorem Ipsum...",
"tags": [ 1, 2, 4 ]
}
]
}
This isn't the most elegant solution, but it may work like you want (code not tested)
public function index()
{
$posts = Post::all();
$postsArray = array();
foreach ($posts as $post)
{
$postArray = $post->toArray();
$postArray['tags'] = array_values($post->tags->lists('id'));
$postsArray[] = $postArray;
}
return Response::json([ 'posts' => $postsArray]);
}
Add the following code to your Model/BaseModel:
/**
* Set additional attributes as hidden on the current Model
*
* #return instanceof Model
*/
public function addHidden($attribute)
{
$hidden = $this->getHidden();
array_push($hidden, $attribute);
$this->setHidden($hidden);
// Make method chainable
return $this;
}
/**
* Convert appended collections into a list of attributes
*
* #param object $data Model OR Collection
* #param string|array $levels Levels to iterate over
* #param string $attribute The attribute we want to get listified
* #param boolean $hideOrigin Hide the original relationship data from the result set
* #return Model
*/
public function listAttributes($data, $levels, $attribute = 'id', $hideOrigin = true)
{
// Set some defaults on first call of this function (because this function is recursive)
if (! is_array($levels))
$levels = explode('.', $levels);
if ($data instanceof Illuminate\Database\Eloquent\Collection) // Collection of Model objects
{
// We are dealing with an array here, so iterate over its contents and use recursion to look deeper:
foreach ($data as $row)
{
$this->listAttributes($row, $levels, $attribute, $hideOrigin);
}
}
else
{
// Fetch the name of the current level we are looking at
$curLevel = array_shift($levels);
if (is_object($data->{$curLevel}))
{
if (! empty($levels))
{
// We are traversing the right section, but are not at the level of the list yet... Let's use recursion to look deeper:
$this->listAttributes($data->{$curLevel}, $levels, $attribute, $hideOrigin);
}
else
{
// Hide the appended collection itself from the result set, if the user didn't request it
if ($hideOrigin)
$data->addHidden($curLevel);
// Convert Collection to Eloquent lists()
if (is_array($attribute)) // Use specific attributes as key and value
$data->{$curLevel . '_' . $attribute[0]} = $data->{$curLevel}->lists($attribute[0], $attribute[1]);
else // Use specific attribute as value (= numeric keys)
$data->{$curLevel . '_' . $attribute} = $data->{$curLevel}->lists($attribute);
}
}
}
return $data;
}
You can use it on your Model/Collection Object like this:
// Fetch posts data
$data = Post::with('tags')->get(); // or use ->first()
// Convert relationship data to list of id's
$data->listAttributes($data, 'tags');
$data will now contain the following object store:
{
"posts": [
{
"title": "Laravel is awesome",
"body": "Lorem Ipsum...",
"tags_id": [ 1, 2, 3 ]
},
{
"title": "Did I mention how awesome Laravel is?",
"body": "Lorem Ipsum...",
"tags_id": [ 1, 2, 4 ]
}
]
}
It also supports nested relationships:
// Fetch posts data
$data = Post::with('comments', 'comments.tags')->get(); // or use ->first()
// Convert relationship data to list of id's
$data->listAttributes($data, 'comments.tags');
Eloquent relationship returns a collection object which you can filter or even modify, say if you need only an array of the id's you can do this:
$posts->pluck('id');
$posts->pluck('id')->toArray();
I think you need to use the $appends property. Take a look at the docs here http://laravel.com/docs/eloquent#converting-to-arrays-or-json.
This question also is relevant as he encountered the same issue as you (see the edit on the accepted answer).
Add a custom attribute to a Laravel / Eloquent model on load?