Laravel 4 Eloquent get ids of relations - php

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?

Related

get all atributes in nested object that have auto relationship

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();

Laravel query builder get custom attribute

I'm trying to get a custom attribute (https://laravel.com/docs/5.5/eloquent-mutators#defining-an-accessor)
from a query.
Right now I have:
User.php
public function getViewUrlAttribute()
{
return route('admin.users.view', ['id' => $this->id]);
}
public function role()
{
return $this->belongsTo('App\Role')->withDefault([
'name' => 'User'
]);
}
UserController.php
public function dataTable(Request $request)
{
$length = $request->has('length') ? $request->input('length') : 10;
$orderDirection = $request->input('orderDirection');
$searchValue = $request->input('search');
$users = User::select('id', 'name', 'email', 'created_at')->with('role:name')->limit($length);
if ($request->has('orderBy')) {
if ($request->has('orderDirection')) {
$users = $users->orderBy($request->input('orderBy'), $request->input('orderDirection') > 0 ? 'asc' : 'desc');
} else {
$users = $users->orderBy($request->input('orderBy'), 'desc');
}
}
return $users->get();
}
Returns
[
{
"id": 1,
"name": "User",
"email": "user#test.com",
"created_at": "2018-04-24 14:14:12",
"role": {
"name": "User"
}
}
]
So the thing is: there's any way to also get the view_url attribute? (I tried inside the with() but it fails)
Also can I return only the role name and not the whole object as you can see in the "Return" code? (I would like something like: "role": "User").
(Of course I'm trying to avoid running raw sql)
Thanks!
You're almost done...
1- To add a custom attribute you need to append it on the Model with $appends attribute:
protected $appends = ['view_url'];
And define your attribute method:
public function getViewUrlAttribute()
{
return route('admin.users.view', ['id' => $this->id]);
}
2- To add attribute to a model from another related model, I think you should try :
// to add them as attribute automatically
protected $appends = ['view_url', 'role_name'];
// to hide attributes or relations from json/array
protected $hidden = ['role']; // hide the role relation
public function getRoleNameAttribute()
{
// if relation is not loaded yet, load it first in case you don't use eager loading
if ( ! array_key_exists('role', $this->relations))
$this->load('role');
$role = $this->getRelation('role');
// then return the name directly
return $role->name;
}
Then you might not event need ->with('role') eager loading.

Is there a way to mass assign relations in laravel 5?

I have two Model classes with a oneToMany relation like this
App\Car
class Car extends Model
{
public $timestamps = true;
protected $fillable = [
'name',
'price'
];
public function parts ()
{
return $this->hasMany('App\Part');
}
}
App\Part
class Part extends Model
{
public $timestamps = false;
protected $fillable = [
'name',
'price'
];
public function car ()
{
return $this->belongsTo('App\Car');
}
}
The client makes a POST request with a JSON representing a Car with a nested Array of Parts
{
"name": "Fiat Punto",
"price": 15000,
"parts": [
{
"name": "wheel",
"price": 300
},
{
"name": "engine",
"price": 5000
}
]
}
Is there a way to save the Car model and create the relations in just one hit?
I tried to do this in my controller but it doesn't work:
...
public function store (Request $request) {
$input = $request->all();
Car::create($input);
}
...
PS: I already know how to do the task with a foreach or array_reduce, just wondering if laravel could do that for me
-- Edit --
Here is how i implemented the controller right now:
...
public function store (Request $request) {
$input = $request->all();
$car = Car::create($input);
$parts = array_reduce(
$input['parts'],
function ($carry, $item) {
array_push($carry, new Part($item));
return $carry;
},
[]
);
$car->parts()->saveMany($parts);
}
...
Any improvement is welcome
I don´t think there is a way of getting around the fact that you need to create each of the App\Part model instances that are defined in the request object. So, at some point you have to iterate over those items in the request object, meaning you have (at least) two options. Those are both described here.
On a side note I think in this case it is better to use a foreach loop for the first option:
foreach ($request->input('parts') as $item) {
$car->parts()->save(new Part($item));
}
If you'd go for the second option of storing them in an array first, then I think array_map is the more appropriate method to use (because there is no actual "reducing" going on):
$parts = array_map(function($part) {
return new App\Part($part);
}, $request->input('parts'));
$car->parts()-saveMany($parts);
Edit: As per suggestion from #TomasKim, with only 2 queries - one for the Car-model and another for the Part-models:
$parts = array_map(function($part) {
return [...$part, 'car_id' => $car->id];
}, $input['parts'])
DB::table('parts')->insert($parts);
While there isn't a way to technically do this entirely automatically, you can make the eloquent api for your model act this way by just tweaking the fillable property and adding a method:
class Car extends Model
{
public $timestamps = true;
protected $fillable = [
'name',
'price',
'parts',
];
public function parts ()
{
return $this->hasMany('App\Part');
}
public function setPartsAttribute($parts)
{
foreach ($parts as $part) {
Part::updateOrCreate(
['id' => array_get($parts, 'id')],
array_merge($part, ['car_id' => $this->id])
);
}
// ... delete parts not in the list if you want
}
}
Notice we just added parts to the fillable property, and then used Eloquent's attribute setting hook set[Field]Attribute() to perform the assignment. This makes it possible to have the Car api look about right:
Car::find(1)->update($carWithParts);
Note: This won't work when creating the Car model since the there is no id. If you want to automate that too, you can queue these updates and then use the model's event hooks to perform this action.

How to get the ids of many to many relation?

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');
}

Most efficient way to fetch model data from a relationship

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

Categories