Laravel relationships avoid query where foreign key is null - laravel

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

Related

Eloquent's "with" doesn't return anything for one relationship, but does for other

I have a Member model in my Laravel application, and in the model:
public function active_memberships() {
return $this->hasMany(MemberMembership::class, 'member_id', 'id')->where(function($query) {
$query->where('ends_at', '>', Carbon::now());
});
}
public function promo_code() {
return $this->hasMany(MemberPromoCode::class, 'member_id', 'id');
}
Both the member_memberships and member_promo_codes tables have member_id as a foreign key. However, if I do this:
$member = Member::with(['active_memberships','promo_code'])->find($id);
I get the active_memberships array in $member, but not promo_code.
So, $member['active_memberships'] prints out an array., but $member['promo_code'] gives an error:
Undefined index: promo_code
What am I missing here?
Try changing your relationship methods to these:
public function active_memberships() {
return $this->hasMany('\App\MemberMembership', 'member_id')->where(function($query) {
$query->where('ends_at', '>', Carbon::now());
});
}
public function promo_code() {
return $this->hasMany('\App\MemberPromoCode', 'member_id');
}
You don't need to pass the third parameter as id because that is by default and try to give the relation using the namespace as that is the most basic way to define relations.
See if Controller query works then?

How to filter a polymorphic relationship to only return a specific model?

I'm having an issue with defining a function to filter a polymorphic relationship and only return a specific model. I'll try to explain below:
Say I have three models: A, B, and C. Model A can belong to either of the other two models. Say we're using the polymorphism field name of recipient, so on our model A database schema we have recipient_type and recipient_id.
On model A, I have a the default function called recipient, defined like so:
public function recipient()
{
return $this->morphTo();
}
However, I want a function called b() which will return a relationship so that it can be used with a query builder using the with() function. The idea being I can call $a->b and it will either return an instance of B or null, depending on whether the instance of A belongs to and instance of B...
Sorry this has been a bit of a mouthful..
Appreciate all the help I can get with this one!
Cheers!
You can define it like this
Model A (define accessor)
public function recipient()
{
return $this->morphTo();
}
public function bRelation()
{
return $this->belongsTo(B::class, 'recipient_id', 'id');
}
public function cRelation()
{
return $this->belongsTo(C::class, 'recipient_id', 'id');
}
public function getBAttribute(){ //define accessor
if($this->recipient_type == 'App\B') return $this->bRelation;
return null;
}
public function getCAttribute(){ //define accessor
if($this->recipient_type == 'App\C') return $this->cRelation;
return null;
}
Now use it with eager loading
$records = A::with('bRelation', 'cRelation')->get();
foreach($records as $a){
dd($a->b); //it will return you either instance of `B` or `null`
}
Works well with accessors:
public function getIssueAttribute()
{
if($this->commentable_type == 'issues') return $this->commentable;
return null;
}
public function getProjectAttribute()
{
if($this->commentable_type == 'projects') return $this->commentable;
return null;
}
public function commentable()
{
return $this->morphTo('commentable');
}

Illuminate Eloquent return null relation on a relation returning different objects

I have an object in Illuminate that has a relation that can return different objets depending on a property of the main object
public function relation(){
switch($this->type){
case "type_1":
return $this->belongsTo('\Models\Type1', 'idElement');
break;
case "type_2":
return $this->belongsTo('\Models\Type2', 'idElement');
break;
default:
return null;
}
}
This produces an error of "Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation" when the "default" section is executed.
Also I can't instantiate a new Illuminate\Database\Eloquent\Relations\Relation() as it is an abstract class.
I could create an empty table and return a relation to this empty table that will always return an empty value, but this is not a solution.
How can I return an empty relation in the default option?
UPDATE:
I have changed it to use polymorphic relations, but now the problem is: how to set a polymorphic relation as an optional relation?
Relation::morphMap([
'type_1' => \App\Models\Type1::class,
'type_2' => \App\Models\Type2::class
]);
....
public function relation(){
return $this->morphTo(null, 'type', 'idElement');
}
I suggest creating a stub relationship class and use it when you need to return always empty (or fixed) collection.
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection;
/**
* Stub relationship
*/
class EmptyRelation 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 $models;
}
public function getResults()
{
return new Collection();
}
}
Then in your default section you do:
return new \App\EmptyRelation();
Well..this is probably not the best option but works
public function relation(){
switch($this->type){
case "type_1":
return $this->belongsTo('\Models\Type1', 'idType1');
break;
case "type_2":
return $this->belongsTo('\Models\Type2', 'idType2');
break;
default:
return $this->hasOne('\Models\ThisModel', 'id','id')->where('id','<',0 );//Assuming you have an id autoincrement field that starts in 1 in your table,
}
}
This will return an empty collection, I'm not sure if this is what you are looking for

Laravel 4 Eloquent relations query

I have a project with main table 'Qsos' and bunch of relations. Now when I try to create advanced search I don't really know how to query all relations at the same time. Qso model has following:
public function band()
{
return $this->belongsTo('Band');
}
public function mode()
{
return $this->belongsTo('Mode');
}
public function prefixes()
{
return $this->belongsToMany('Prefix');
}
public function user()
{
return $this->belongsTo('User');
}
public function customization() {
return $this->hasOne('Customization');
}
Then I have SearchController with following code that has to return collection of all Qsos following required conditions:
$qsos = Qso::withUser($currentUser->id)
->join('prefix_qso','qsos.id','=','prefix_qso.qso_id')
->join('prefixes','prefixes.id','=','prefix_qso.prefix_id')
->where('prefixes.territory','like',$qTerritory)
->withBand($qBand)
->withMode($qMode)
->where('call','like','%'.$input['qCall'].'%')
->orderBy('qsos.id','DESC')
->paginate('20');
And then in view I need to call $qso->prefixes->first() and $qso->prefixes->last() (Qso and Prefix has manyToMany relation) but both return null. What is wrong?
Here is the eloquent code that I found working but taking VERY long time to process:
$qsos = Qso::withUser($currentUser->id)
->with('prefixes')
->withBand($qBand)
->withMode($qMode)
->where('call','like','%'.$input['qCall'].'%')
->whereHas('prefixes', function($q) use ($qTerritory) {
$q->where('territory','like',$qTerritory);
})
->orderBy('qsos.id','DESC')
->paginate('20');

Eloquent relationships for not-existing model class

I would like to have in my applications many models/modules but some of them would be removed for some clients.
Now I have such relation:
public function people()
{
return $this->hasMany('People', 'model_id');
}
and when I run $model = Model::with('people')->get(); it is working fine
But what if the People model doesn't exist?
At the moment I'm getting:
1/1 ErrorException in ClassLoader.php line 386: include(...): failed
to open stream: No such file or directory
I tried with
public function people()
{
try {
return $this->hasMany('People', 'model_id');
}
catch (FatalErrorException $e) {
return null;
}
}
or with:
public function people()
{
return null; // here I could add checking if there is a Model class and if not return null
}
but when using such method $model = Model::with('people')->get(); doesn't work.
I will have a dozens of relations and I cannot have list of them to use in with. The best method for that would be using some empty relation (returning null) just to make Eloquent not to do anything but in this case Eloquent still tries to make it work and I will get:
Whoops, looks like something went wrong.
1/1 FatalErrorException in Builder.php line 430: Call to a member function
addEagerConstraints() on null
Is there any simple solution for that?
The only solution I could come up with is creating your own Eloquent\Builder class.
I've called it MyBuilder. Let's first make sure it gets actually used. In your model (preferably a Base Model) add this newEloquentBuilder method:
public function newEloquentBuilder($query)
{
return new MyBuilder($query);
}
In the custom Builder class we will override the loadRelation method and add an if null check right before addEagerConstraints is called on the relation (or in your case on null)
class MyBuilder extends \Illuminate\Database\Eloquent\Builder {
protected function loadRelation(array $models, $name, Closure $constraints)
{
$relation = $this->getRelation($name);
if($relation == null){
return $models;
}
$relation->addEagerConstraints($models);
call_user_func($constraints, $relation);
$models = $relation->initRelation($models, $name);
$results = $relation->getEager();
return $relation->match($models, $results, $name);
}
}
The rest of the function is basically the identical code from the original builder (Illuminate\Database\Eloquent\Builder)
Now simply add something like this in your relation function and it should all work:
public function people()
{
if(!class_exist('People')){
return null;
}
return $this->hasMany('People', 'model_id');
}
Update: Use it like a relationship
If you want to use it like you can with a relationship it gets a bit more tricky.
You have to override the getRelationshipFromMethod function in Eloquent\Model. So let's create a Base Model (Your model obviously needs to extend it then...)
class BaseModel extends \Illuminate\Database\Eloquent\Model {
protected function getRelationshipFromMethod($key, $camelKey)
{
$relations = $this->$camelKey();
if ( $relations instanceof \Illuminate\Database\Eloquent\Collection){
// "fake" relationship
return $this->relations[$key] = $relations;
}
if ( ! $relations instanceof Relation)
{
throw new LogicException('Relationship method must return an object of type '
. 'Illuminate\Database\Eloquent\Relations\Relation');
}
return $this->relations[$key] = $relations->getResults();
}
}
Now we need to modify the relation to return an empty collection
public function people()
{
if(!class_exist('People')){
return new \Illuminate\Database\Eloquent\Collection();
}
return $this->hasMany('People', 'model_id');
}
And change the loadRelation function in MyBuilder to check for the type collection instead of null
protected function loadRelation(array $models, $name, Closure $constraints)
{
$relation = $this->getRelation($name);
if($relation instanceof \Illuminate\Database\Eloquent\Collection){
return $models;
}
// ...
}

Resources