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();
Related
I'm writing a REST API using Lumen. I have for my example 2 models User and Post. Post model use the method belongsTo to get the User model which created this post. My goal was to define an accessor so I can get the author's username of the post just like that Post::find($id)->author. So according to the doc I do this:
Post.php :
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $table = 'posts';
protected $appends = ['author'];
protected $fillable = [
'title',
'description'
];
protected $hidden = [
'user_id',
'created_at',
'updated_at'
];
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
public function getAuthorAttribute()
{
return $this->user->username;
}
}
Now the getter works well and I can easily get the author of the given Post.
But if I tried to return the Post in a JSON response, it's also return me weird attributes like user that seems to come from my user() method that call a belongsTo():
return response()->json(Post::find(2), 200);
{
"id": 2,
"title": "Amazing Post",
"description": "Nice post",
"author": "FooBar",
"user": {
"id": 4,
"username": "FooBar"
}
}
If I use the attributesToArray() it's work as expected:
return response()->json(Post::find(2)->attributesToArray(), 200);
{
"id": 2,
"title": "Amazing Post",
"description": "Nice post",
"author": "FooBar"
}
Moreover if I remove the getter getAuthorAttribute() and the $appends declaration, I don't get the unexpected user attribute.
But I don't want to use this method each time and it doesn't make it work if I want to return all my Post using:
return response()->json(Post::all(), 200);
Have someone an idea why I get this additional attribute using belongsTo?
This behavior is because of performance.
When you call $post->user for first time, The Laravel read it from the database and keep it in $post->relation[] for next usage. So next time Laravel can read it from the array and prevent from executing a query again(it will be useful if you use it in multiple places).
Plus, the user is also an attribute and the Laravel merges
$attributes and $relations array together when you call $model->toJson() or $model->toArray()
The Laravel's Model source code:
public function toArray()
{
return array_merge($this->attributesToArray(), $this->relationsToArray());
}
public function jsonSerialize()
{
return $this->toArray();
}
Your first approach was good, you just need to add 'user' into the $hidden array
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $table = 'posts';
protected $appends = ['author'];
protected $fillable = [
'title',
'description'
];
protected $hidden = [
'user_id',
'created_at',
'updated_at',
'user', // <-- add 'user' here
];
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
public function getAuthorAttribute()
{
return $this->user->username;
}
}
And your resulting model would be:
{
"id": 2,
"title": "Amazing Post",
"description": "Nice post",
"author": "FooBar"
}
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");
}
}
Hello i'm trying to change how to access to a database column name without change the name, for example, my column name is resourceType but I want to call it name and also I want the response json appears name instead resourceType. Looking around internet found I should use protected $maps = ['oldName' => 'newName']; but doesn't work. I want to change resourceType because I think doesn't look good the table name should be equal than a column resourceType->resourceType
This is my model
<?php
namespace Knotion;
use Illuminate\Database\Eloquent\Model;
use Mappable, Mutable;
class CTL_ResourceType extends Model {
public $timestamps = false;
protected $table = "CTL_ResourceType";
protected $primaryKey = "idResourceType";
public $incrementing = false;
public static $snakeAttributes = false;
protected $hidden = ['idCountry', 'idCompany', 'initials', 'thumbnail', 'icon', 'status', 'createTime', 'updateTime'];
protected $fillable = ['name'];
protected $maps = ['resourceType' => 'name'];
protected $appends = ['name'];
public function resource() {
return $this->hasMany('Knotion\CTL_Resource', 'idResource' );
}
public function country() {
return $this->belongsTo('Knotion\CTL_Country', 'idCountry', 'idCountry');
}
public function company() {
return $this->belongsTo('Knotion\CTL_Company', 'idCompany', 'idCompany');
}
}
and this is the response JSON I'm receiving. As you see resourceType stills there instead name
{
"total": 16,
"per_page": 15,
"current_page": 1,
"last_page": 2,
"next_page_url": "http://localhost:8000/krb/api/resources?page=2",
"prev_page_url": null,
"from": 1,
"to": 15,
"data": [
{
"idResource": "4e8f1ece-f666-11e5-8137-0f7932903a75",
"productionKey": "238493ujjsl",
"title": "ElTitle16",
"description": "ElDescription16",
"minimumAge": "4",
"maximumAge": "15",
"fileName": "ElFileName16",
"extension": ".png",
"URL": "ElURL16",
"createTime": "2016-03-30 04:58:16",
"creatorUser": {
"idUser": "85cf125c-f5ff-11e5-8137-0f7932903a75",
"name": "Roberto"
},
"creationCountry": {
"idCountry": "f03a75a0-f5ff-11e5-8137-0f7932903a75",
"country": "Estados Unidos"
},
"resourceType": {
"idResourceType": "5c902028-f601-11e5-8137-0f7932903a75",
"resourceType": "TípodeRecurso3"
},
"tags": [
{
"idTag": "40c6a114-f520-11e5-8137-0f7932903a75",
"name": "ElTag1"
}
],
"quickTags": [
{
"idQuickTag": "679bc8f0-f520-11e5-8137-0f7932903a75",
"name": "ElQuickTag4"
}
],
"relatedTo": [
{
"idRelatedTo": "7beddc6c-f520-11e5-8137-0f7932903a75",
"name": "ElRelatedTo3"
}
]
}
I hadn't heard of the $maps property or Mappable before, so I did a quick search. It looks like they (as well as Mutable) are part of the jarektkaczyk/eloquence package.
In this case, both Mappable and Mutable are traits that are supposed to be added to the class. Additionally, in order for them to work properly, you need to add in the Eloquence trait, as well.
Your use statements at the top of your file need to be changed to properly address the class names in the correct namespace, and then you need to add the traits to your class:
<?php
namespace Knotion;
// import the class names
use Sofa\Eloquence\Mutable;
use Sofa\Eloquence\Mappable;
use Sofa\Eloquence\Eloquence;
use Illuminate\Database\Eloquent\Model;
class CTL_ResourceType extends Model {
// add the traits to the class
use Eloquence, Mappable, Mutable;
// code...
}
Edit
If you wanted to do this without the package, you need to do three things:
You need to add resourceType to your $hidden array, so that it won't show up in your toArray()/toJson() results.
protected $hidden = ['idCountry', 'idCompany', 'initials', 'thumbnail', 'icon', 'status', 'createTime', 'updateTime', 'resourceType'];
You need to create a getNameAttribute() accessor method, which will be called whenever you attempt to access the name attribute.
public function getNameAttribute() {
return $this->resourceType;
}
You need to add name to your $appends array, so that it will be included in your toArray()/toJson() results.
protected $appends = ['name'];
Optionally, if that feels like too much work, you could always just override the toArray() method (called by toJson()) to force your naming convention, as well:
public function toArray() {
// call parent method to get initial array results
$array = parent::toArray();
// set the new key with data
$array['name'] = $array['resourceType'];
// unset the old key
unset($array['resourceType']);
// return the array
return $array;
}
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');
}
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