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.
Related
is it possible to disable the loading of relationships, but only in some cases?
Here are my models:
class League extends Model
{
...
public function country()
{
return $this->belongsTo(Country::class)->with('translations');
}
}
class Country extends Model
{
...
public function translations()
{
return $this->hasMany(CountryTranslation::class, 'country_id');
}
}
class CountryTranslation extends Model
{
...
}
In many places, I need to load the translations relationship for Country, but on some pages, I want to display information about the League and its Country only. There I don't want to show the CountryTranslation collection.
Here is the code for that page:
$country = $league->country;
Is it possible only for this line to disable the relations?
So, you're currently finding out one of the reasons for not defining the eager loading inside of the relationship. The first suggestion would be to remove the with() from the relationship definition, and add it in where needed. If desired, you can create another relationship that has the eager loading enabled, and it can use the base relationship to keep it DRY:
public function country()
{
return $this->belongsTo(Country::class);
}
public function countryWithTranslations()
{
return $this->country()->with('translations');
}
If this code change is not feasible, you will need to change how you're accessing the country relationship. When you access the relationship attribute, it lazy loads the relationship, and you don't have the ability to modify the relationship query. So, instead of accessing the relationship attribute, you'd need to call the relationship query so you can modify it.
Therefore, you won't be able to do $country = $league->country;, but you can do:
$country = $league->country()->without('translations')->first();
he with() simply eager loads the translations to avoid additional queries, but you should be able to load the translations with and without it, without with( adds additional queries. https://laravel.com/docs/9.x/eloquent-relationships#eager-loading
You will want to change:
public function country()
{
return $this->belongsTo(Country::class)->with('translations');
}
to
public function country()
{
return $this->belongsTo(Country::class);
}
If you want to load translations, you can do it in the controllers
// if you want translations at some point do this:
$league = League::with('country.translations')
$country = $league->country->translations
// if you do not want translations
$league = League::with('country')
$country = $league->country;
If you do not want to touch:
public function country()
{
return $this->belongsTo(Country::class)->with('translations');
}
you can create another method
public function countryClean()
{
return $this->belongsTo(Country::class);
}
$country = $league->countryClean;
This is the case I have
$user = User_picture::where('is_main','=',1)->where('user_id','=',Auth::user()->id);
This means a user has many pictures but there is only one picture that has is_main value 1.
I tried this.
public function active_picture()
{
return $this->hasMany('App\User_picture')->find($this->is_main);
}
This doesn't return anything but an error that says it should be an object. Can anyone help me out with the case?
HasMany is meant to return a collection since it is a one to many relationship method.
If you only want to return one result, you should use HasOne with conditions. To define conditions, you use where() not find(). Find() is only used to match the model's primary key.
public function active_picture()
{
return $this->hasOne('App\User_picture')
->where('is_main', 1);
}
You could declare an accessor like:
In User model:
// declare relationship:
public function pictures()
{
return $this->hasMany(Picture::class);
}
// declare accessor:
public function getMainPictureAttribute()
{
return $this->pictures()->where('is_main', 1)->first();
}
So you can do this:
auth()->user()->main_picture ...
I have query that I run on my ServiceController
return Service::with('contacts', 'locations.datetimes')->find($service->id);
This works great perfectly, but I need to change it. The relationships are defined as follows:
class Service extends Model
{
public function locations()
{
return $this->belongsToMany('App\Models\Service_location', 'service_location_service');
}
}
class Service_location extends Model
{
public function datetimes()
{
return $this->hasMany('App\Models\Service_detail');
}
}
I need a second constraint on the datetimes where I need the relationship to be along the lines of
return $this->hasMany('App\Models\Service_detail')->where('service_id', $service->id);
The problem is I can't find a way to pass through the $service_id. How do you handle two constraints on a hasMany relationship?
Let try this. Change Service_location to this
class Service_location extends Model
{
public function datetimes()
{
return $this->hasMany('App\Models\Service_detail');
}
public function scopeServiceId($query)
{
return $query->where('service_id', $service->id);
}
}
Now your query will be
Service::with(['contacts', 'locations.datetimes' =>function($q) use($serviceId){
$q->serviceId($serviceId);
}])->find($service->id);
There is no way to pass argument to a relationship function. If you do its highly likely that you will run into the N+1 query problem.
When eager loading, is it possible to avoid doing an extra query when the foreign key in a relationship is null and therefore does not match any related record? In my example I have a Product and User.
A Product is owned by a User but can also be optionally edited by a User. So my model looks like this:
class Product extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function editor()
{
return $this->belongsTo(User::class, 'editor_id');
}
}
When a product has not been edited, it's editor_id attribute is NULL.
If I hadn't been eager loading I know I could do something like the following:
$product = Product::find(1);
if (!is_null($product->editor_id)) {
// Load the relation
}
However, this isn't an option for me and I would ideally like to avoid an extra, unnecessary query being run when eager loading:
Query: select * from `users` where `users`.`id` in (?)
Bindings: [0]
I was wondering if something similar to the following would be possible?
public function editor()
{
if (!is_null($this->editor_id)) {
return $this->belongsTo(User::class, 'editor_id');
}
}
When doing the above I get this error:
Call to a member function addEagerConstraints() on a non-object
I'm guessing that this is because it's not guaranteed that this method returns a Relation object.
Thanks
I solved this by creating a new Relation subclass that implements the required methods but simply returns null when actually obtaining results:
namespace My\App;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class NullRelation extends Relation {
public function __construct() {}
public function addConstraints() {}
public function addEagerConstraints(array $models) {}
public function initRelation(array $models, $relation) {}
public function match(array $models, Collection $results, $relation) {
return [];
}
public function getResults() {
return null;
}
}
Then inside your relation method(s) you can check for null and return an instance of NullRelation instead:
public function editor() {
if ($this->editor_id === null) {
return new NullRelation();
} else {
return $this->belongsTo(User::class, 'editor_id');
}
}
It's a bit ugly and you'd be repeating youself a lot, so if I were using this in more than one place I'd probably create a subclass of the Model, then create versions of the belongsTo, hasOne methods and perform the check there:
public function belongsToIfNotNull($related, $foreignKey = null, $otherKey = null, $relation = null) {
if ($this->$foreignKey === null) {
return new NullRelation();
} else {
return parent::belongsTo($related, $foreignKey, $otherKey, $relation);
}
}
Finally in your modal that inherits the new subclass, your relation method simply becomes
public function editor() {
return $this->belongsToIfNotNull(User::class, 'editor_id');
}
Laravel Docs
Querying Relations When Selecting
When accessing the records for a model, you may wish to limit your results based on the existence of a relationship.
$posts= Post::has('editor')->get();
You may also specify an operator and a count:
$posts = Post::has('editor', '>=', 3)->get();
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');
}