I have the following code in my DesignsController:
public function show($id)
{
return Design::find($id)->with('variables')->get();
}
When I GET /designs/1 I should get back json of just the design with id=1, but I get back all the current designs.
In the design model:
/* Define relationship between designs and variables */
public function variables()
{
return $this->hasMany('Variable');
}
The routes.php:
Route::resource('designs', 'DesignsController');
What am I doing wrong here?
Edit: a bit more information. I get all the results back as long as I hit an id of an actual design, so it seems to be finding the result according to the id, but then returning all results.
If I remove ->with('variables')->get(); then this works, but I need the variables too. Here's the model for Design:
class Design extends Eloquent {
/* Define relationship between designs and variables */
public function variables()
{
return $this->hasMany('Variable');
}
/* Define relationship between designs and variables */
public function user()
{
return $this->belongsTo('User');
}
}
Variable model:
class Variable extends Eloquent {
public $timestamps = false;
}
You're doing your "with" statement incorrectly:
Eager load:
public function show($id)
{
return Design::with('variables')->find($id);
}
Actually I think you're problem was calling get() after find() since find already returns a model. Find should be called at the end of a query you build because it essentially calls get() inside of it.
Lazy-Eager alternative:
public function show($id)
{
return Design::find($id)->load('variables');
}
Related
let's say that I want to get the author's name inside the book model
// app/Book.php
protected $appends = ['author_name'];
public function author() {
return belongsTo(Author::class, 'author_id');
}
public function getAuthorNameAttribute() {
return $this->author->name;
}
but this would append the whole author collection to the final book collection as well, that would bump up the loading time when trying to load like 100 books, right now I work it around by removing the author after getting the name like this
// app/Book.php
protected $appends = ['author_name'];
public function author() {
return belongsTo(Author::class, 'author_id');
}
public function getAuthorNameAttribute() {
$authorName = $this->author->name;
unset($this->author);
return $authorName;
}
is there a better way to do it? or did I miss any function from eloquent?
Cheers
Try to add following code in your app/Book.php
// app/Book.php
protected $hidden = ['author'];
I have the following route:
Route::get('/api/products/{product}', 'ProductController#get');
My Product model looks like this:
class Product extends Model
{
public function ingredients()
{
return $this->belongsToMany(Ingredient::class)->withPivot('value');
}
}
In my controller, the method is:
public function get(Product $product)
{
return $product;
}
This only returns the attributes of the Product object as a JSON. I would also like to return the related ingredients and pivot table values (as it would with the with method), and possibly other related models.
return $product->with('ingredients') creates a collection of all Products, so that doesn't really work, I have to filter it again by the product ID. I can obviously construct the JSON myself, but that becomes tedious if I want multiple related models included. Is there an easy way to accomplish this?
You have three options:
Using $with in model
class Product extends Model
{
protected $with = ['ingredients'];
public function ingredients()
{
return $this->belongsToMany(Ingredient::class)->withPivot('value');
}
}
Load the relation and return product:
public function get(Product $product)
{
$product->ingredients;
return $product;
}
Use the load method on the product:
public function get(Product $product)
{
return $product->load('ingredients');
}
Normal relationship methods don't usually have a condition, and tend to look like this:
class StripeCustomer extends Model
{
public function user()
{
return $this->belongsTo(User::class, 'stripe_customer_id');
}
}
In my model I have a condition in the relationship method like so:
class StripeCustomer extends Model
{
public function user()
{
if ($this->type === 'normal') {
return $this->hasOne(User::class, 'stripe_customer_id');
} else {
return $this->hasOne(User::class, 'stripe_customer_charity_id');
}
}
}
Does Laravel support conditional relationships in Eloquent like above. A lot of the usual methods still work like so:
StripeCustomer::get()->first()->user;
StripeCustomer::get()->first()->user()->get();
But would the following work predictably:
Foo::with('user')->get();
The issue here is that I am unsure in how the "with" operator works in Eloquent internally.
A reason I believe it also doesn't work is that the user() method needs to be executed for every model. However, when I added a dump(...) at the start of the method, I found it was only run once, indicating that with() does not work.
No, it won't work with with(). What do you think will happen when you try to execute the following code:
Foo::with('user')->get();
The answer is Laravel will create new instance of Foo and try to call user() to get the relationship object. This new instance doesn't have any type ((new Foo)->type will be null), therefore your method user() will always return $this->hasOne(Bar::class, 'b_id') and this relationship object will be used to construct a query.
As you can see this is clearly not what you wanted since only type B users will be eager loaded for all Foo rows. What you need to do in this case is create two relationships (one for each type) and accessors (get/set) for user:
class Foo extends Model
{
public function userA()
{
return $this->hasOne(Bar::class, 'a_id');
}
public function userB()
{
return $this->hasOne(Bar::class, 'b_id');
}
public function getUserAttribute()
{
if ($this->type === 'a') {
return $this->userA;
} else {
return $this->userB;
}
}
public function setUserAttribute($user)
{
if ($this->type === 'a') {
$this->userA()->associate($user);
} else {
$this->userB()->associate($user);
}
}
}
Then you can use with() for both relations to utilize eager loading:
$fooRows = Foo::with('userA', 'userB')->get();
...
foreach ($fooRows as $row) {
$row->user;
}
edit:
Since you've edited code in your question the example code in my answer no longer represents your case, but I hope you get the overall idea.
Yep, with() works. It runs a subquery on any relation your user() method returns. Since your relation already has a constraint, it applies said constraint to the subquery as you'd expect.
In my Image model I have two relationships with my Article model, one many-to-many and one one-to-many:
public function articlesAlbums()
{
return $this->belongsToMany('Article', 'article_image', 'image_id', 'article_id')->publishedFilter();
}
public function articleThumb()
{
return $this->hasMany('Article')->publishedFilter();
}
I merge the results from these to get all images used by Article:
public function getArticlesAllAttribute()
{
return $this->articlesAlbums->merge($this->articleThumb);
}
In my Article model I have two relationships with my Image model:
public function images()
{
return $this->belongsToMany('Image', 'article_image', 'article_id', 'image_id');
}
public function thumbnail()
{
return $this->belongsTo('Image', 'image_id');
}
I would like to merge these as well, same way I do in my Image model:
public function getImagesAllAttribute()
{
return $this->images->merge($this->thumbnail);
}
But that doesn't work, it seems to be because my thumbnail relationship is belongsTo, not hasMany. So maybe it's not a collection. When I try I get an exception:
Call to a member function getKey() on a non-object
I've tried converting it to a collection with:
new Collection($this->thumbnail)
but get an error saying:
__construct() must be of the type array, object given
How can I merge $this->images and $this->thumbnail in my Article model to get the same results that I do in my Image model? Meaning that the results are merged with no duplicates.
Many thanks.
Update:
Since Razor made me realized that $this->thumbnail did not in fact return a collection, but rather a single object it made me rethink if merge was really the proper function to use.
return $this->images->merge($this->thumbnail()->get());
This seemed to cause a lot of unnecessary queries, even though I was eager loading.
So I ended up doing this instead:
public function getImagesAllAttribute()
{
$imagesAll = $this->images;
if ( ! is_null($this->thumbnail)) $imagesAll->add($this->thumbnail);
return $imagesAll->unique();
}
It drastically reduced the number of queries, and the result is the same.
$this->thumbnail is the same as $this->thumbnail()->first(), to get a collection, you may try:
public function getImagesAllAttribute()
{
return $this->images->merge($this->thumbnail()->get());
}
Just migrated to 4.1 to take advantage of this powerful feature.
everything seems to work correctly when retrieving individual 'morphedByXxxx' relations, however when trying to retrieve all models that a particular tag belongs to -- i get an error or no results.
$tag = Tag::find(45); //Tag model name = 'awesome'
//returns an Illuminate\Database\Eloquent\Collection of zero length
$tag->taggable;
//returns Illuminate\Database\Eloquent\Relations\MorphToMany Builder class
$tag->taggable();
//returns a populated Collection of Video models
$tag->videos()->get();
//returns a populated Collection of Post models
$tag->posts()->get();
My Tag Model class loooks like this:
class Tag extends Eloquent
{
protected $table = 'tags';
public $timestamps = true;
public function taggable()
{
//none of these seem to function as expected,
//both return an instance of MorphToMany
//return $this->morphedByMany('Tag', 'taggable');
return $this->morphToMany('Tag', 'taggable');
//this throws an error about missing argument 1
//return $this->morphToMany();
}
public function posts()
{
return $this->morphedByMany('Post', 'taggable');
}
public function videos()
{
return $this->morphedByMany('Video', 'taggable');
}
}
And the Post and Video models look like this:
class Post extends Eloquent
{
protected $table = 'posts';
public $timestamps = true;
public function tags()
{
return $this->morphToMany('Tag', 'taggable');
}
}
I am able to add/remove Tags to Posts and Videos as well as retrieve the related Posts, and Videos for any Tag -- however -- what is the proper way to retrieve all Models having the Tag name 'awesome'?
Was able to figure it out, would love to hear comments on this implementation.
in Tag.php
public function taggable()
{
return $this->morphToMany('Tag', 'taggable', 'taggables', 'tag_id')->orWhereRaw('taggables.taggable_type IS NOT NULL');
}
in calling code:
$allItemsHavingThisTag = $tag->taggable()
->with('videos')
->with('posts')
->get();
I just used this on Laravel 5.2 (not sure if it is a good strategy though):
Tag model:
public function related()
{
return $this->hasMany(Taggable::class, 'tag_id');
}
Taggable model:
public function model()
{
return $this->belongsTo( $this->taggable_type, 'taggable_id');
}
To retrieve all the inverse relations (all the entities attached to the requested tag):
#foreach ($tag->related as $related)
{{ $related->model }}
#endforeach
... sadly this technique doesn't offer eager load functionalities and feels like a hack. At least it makes it simple to check the related model class and show the desired model attributes without much fear to look for the right attributes on the right model.
I posted a similar question in this other thread as I am looking for relations not known in advance.