Laravel Check If Related Model Exists - php

I have an Eloquent model which has a related model:
public function option() {
return $this->hasOne('RepairOption', 'repair_item_id');
}
public function setOptionArrayAttribute($values)
{
$this->option->update($values);
}
When I create the model, it does not necessarily have a related model. When I update it, I might add an option, or not.
So I need to check if the related model exists, to either update it, or create it, respectively:
$model = RepairItem::find($id);
if (Input::has('option')) {
if (<related_model_exists>) {
$option = new RepairOption(Input::get('option'));
$option->repairItem()->associate($model);
$option->save();
$model->fill(Input::except('option');
} else {
$model->update(Input::all());
}
};
Where <related_model_exists> is the code I am looking for.

In php 7.2+ you can't use count on the relation object, so there's no one-fits-all method for all relations. Use query method instead as #tremby provided below:
$model->relation()->exists()
generic solution working on all the relation types (pre php 7.2):
if (count($model->relation))
{
// exists
}
This will work for every relation since dynamic properties return Model or Collection. Both implement ArrayAccess.
So it goes like this:
single relations: hasOne / belongsTo / morphTo / morphOne
// no related model
$model->relation; // null
count($model->relation); // 0 evaluates to false
// there is one
$model->relation; // Eloquent Model
count($model->relation); // 1 evaluates to true
to-many relations: hasMany / belongsToMany / morphMany / morphToMany / morphedByMany
// no related collection
$model->relation; // Collection with 0 items evaluates to true
count($model->relation); // 0 evaluates to false
// there are related models
$model->relation; // Collection with 1 or more items, evaluates to true as well
count($model->relation); // int > 0 that evaluates to true

A Relation object passes unknown method calls through to an Eloquent query Builder, which is set up to only select the related objects. That Builder in turn passes unknown method calls through to its underlying query Builder.
This means you can use the exists() or count() methods directly from a relation object:
$model->relation()->exists(); // bool: true if there is at least one row
$model->relation()->count(); // int: number of related rows
Note the parentheses after relation: ->relation() is a function call (getting the relation object), as opposed to ->relation which a magic property getter set up for you by Laravel (getting the related object/objects).
Using the count method on the relation object (that is, using the parentheses) will be much faster than doing $model->relation->count() or count($model->relation) (unless the relation has already been eager-loaded) since it runs a count query rather than pulling all of the data for any related objects from the database, just to count them. Likewise, using exists doesn't need to pull model data either.
Both exists() and count() work on all relation types I've tried, so at least belongsTo, hasOne, hasMany, and belongsToMany.

I prefer to use exists method:
RepairItem::find($id)->option()->exists()
to check if related model exists or not. It's working fine on Laravel 5.2

After Php 7.1, The accepted answer won't work for all types of relationships.
Because depending of type the relationship, Eloquent will return a Collection, a Model or Null. And in Php 7.1 count(null) will throw an error.
So, to check if the relation exist you can use:
For relationships single: For example hasOne and belongsTo
if(!is_null($model->relation)) {
....
}
For relationships multiple: For Example: hasMany and belongsToMany
if ($model->relation->isNotEmpty()) {
....
}

I use for single relationships: hasOne, belongsTo and morphs
if($model->relation){
....
}
Because if condition is null, this will be false.
For multiple relationships: hasMany, belongsToMany and morphs
if ($model->relation->isNotEmpty()) {
....
}

You can use the relationLoaded method on the model object. This saved my bacon so hopefully it helps someone else. I was given this suggestion when I asked the same question on Laracasts.

As Hemerson Varela already said in Php 7.1 count(null) will throw an error and hasOne returns null if no row exists. Since you have a hasOnerelation I would use the empty method to check:
$model = RepairItem::find($id);
if (!empty($temp = $request->input('option'))) {
$option = $model->option;
if(empty($option)){
$option = $model->option()->create();
}
$option->someAttribute = temp;
$option->save();
};
But this is superfluous. There is no need to check if the relation exists, to determine if you should do an update or a create call. Simply use the updateOrCreate method. This is equivalent to the above:
$model = RepairItem::find($id);
if (!empty($temp = $request->input('option'))) {
$model->option()
->updateOrCreate(['repair_item_id' => $model->id],
['option' => $temp]);
}

Not sure if this has changed in Laravel 5, but the accepted answer using count($data->$relation) didn't work for me, as the very act of accessing the relation property caused it to be loaded.
In the end, a straightforward isset($data->$relation) did the trick for me.

I had to completely refactor my code when I updated my PHP version to 7.2+ because of bad usage of the count($x) function. This is a real pain and its also extremely scary as there are hundreds usages, in different scenarios and there is no one rules fits all..
Rules I followed to refactor everything, examples:
$x = Auth::user()->posts->find(6); (check if user has a post id=6 using ->find())
[FAILS] if(count($x)) { return 'Found'; }
[GOOD] if($x) { return 'Found'; }
$x = Auth::user()->profile->departments; (check if profile has some departments, there can have many departments)
[FAILS] if(count($x)) { return 'Found'; }
[GOOD] if($x->count()) { return 'Found'; }
$x = Auth::user()->profile->get(); (check if user has a profile after using a ->get())
[FAILS] if(count($x)) { return 'Found'; }
[GOOD] if($x->count()) { return 'Found'; }
Hopes this can help, even 5 years after the question has been asked, this stackoverflow post has helped me a lot!

If you use the model class and use Eloquent ORM, then create a new method and return bool data. like
public function hasPosts(): bool
{
return $this->posts()->exists();
}

RelationLoaded method of Model class could be useful.
if ($this->relationLoaded('prices')) {
return;
}

Related

Evaluate existence of relationship as true or false - Laravel Relationships

I'm trying to create a policy like the following:
public function view(User $user, PersonalStatement $personalStatement)
{
return $user->applicant->id == $personalStatement->applicant_id || $user->mentor->exists();
}
I then test it with a User model where mentor relationship exists, but applicant relationship does not, and I get the error:
Trying to get property 'id' of non-object
I believe this is because the user->applicant returns null in this case.
What's the best way to evaluate if a model has a relationship where the response is either true or false?
Using exists() method returns null if the relationship does not exist.
To handle null case, you may use optional Laravel helper, it allows you to call any property/method even if your source is null, it returns null instead of throwing an exception.
public function view(User $user, PersonalStatement $personalStatement)
{
return optional($user->applicant)->id == $personalStatement->applicant_id || $user->mentor->exists();
}
To check the 'applicant" relationship of 'User' model, try
$user->applicant()->exists()
OR
$user->applicant()->count()
Note: Brackets after the relationship does really matter.

Laravel eloquent conditional relationship

I am trying to set up a conditional relationship in eloquent model based on the value of table column, but it does not works.
This is the code that I am using:
//RELATIONS
public function task_schedule()
{
if ($this->task_schedule_id === 0) {
return $this->belongsTo('TaskSchedule', 'hold_task_schedule_id', 'id');
} else {
return $this->belongsTo('TaskSchedule');
}
}
Basically I want to use different column to get my child model defined in belongs to relationship.
scopeProfile($query) would not work for me because I dont want to retrieve the child in every single query and also I am using Task->with('task_schedule') in a lot parts of code.

Avoid query to check if relation exists in Laravel

In Laravel I've 2 models Man and Car, and the Car model has a foreign key man_id.
Now I would like to check if a Car has an owner, so I can do
if($car->man != null)
I could also do:
if($car->man_id != null)
but I don't like this solution...
But in this way Laravel do a query to check if Man exists, is there a way to avoid this useless query?
You can create a new function on your Car model that checks for its owner.
Car.php
class Car extends Model {
public function hasOwner()
{
return empty($this->man_id); // returns boolean
}
}
Then, you can just do
$car = Car::first();
if ($car->hasOwner()) { ... }
UPDATE:
You can also use exists() method
$car->man()->exists(); // bool: true if there is at least one row
As of Laravel 5.5 or higher, you can also use doesntExist() which will determine if no rows exist for the current query.
$car->man()->doesntExist(); // bool: true if no row exists

Null object pattern with Eloquent relations

There is often the case where an certain eloquent model's relation is unset (i.e. in a books table, author_id is null) and thus calling something like $model->relation returns null.
E.g. say a Book model has an author() (hasOne) relation I might want to do
$author = Book::find(1)->author->name;
If Book 1 has no author set it will throw a "trying to get property of non object" error. Is there a way to avoid this and default to a blank Author so I'll always be able to call name on it regardless of whether the relation has been set for the specific model?
Essentially I want to avoid conditionals to check if $book->author is an actual Author before calling further methods/properties on it. It should default to a new Author instance if the relation isn't set.
I tried something like:
public function getAuthorAttribute($author)
{
return $author ?: new Author;
}
however this doesn't work; $author is being passed in as null, even if it's set on the model. Presumably because it's a relation rather than a direct property of a book. I'd need something like
public function getAuthorAttribute()
{
return $this->author()->first() ?: new Author;
}
which seems pretty inelegant and seems like it would override any eager loading resulting in poor performance.
Update
As of Laravel 5.3.23, there is now a built in way to accomplish this (at least for HasOne relationships). A withDefault() method was added to the HasOne relationship. In the case of your Book/Author example, your code would look like:
public function author() {
return $this->hasOne(Author::class)->withDefault();
}
This relationship will now return a fairly empty (keys are set) Author model if no record is found in the database. Additionally, you can pass in an array of attributes if you'd like to populate your empty model with some extra data, or you can pass in a Closure that returns what you'd like to have your default set to (doesn't have to be an Author model).
Until this makes it into the documentation one day, for more information you can check out the pull requests related to the change: 16198 and 16382.
At the time of this writing, this has only been implemented for the HasOne relationship. It may eventually migrate to the BelongsTo, MorphOne, and MorphTo relationships, but I can't say for sure.
Original
There's no built in way that I know of to do this, but there are a couple workarounds.
Using an Accessor
The problem with using an accessor, as you've found out, is that the $value passed to the accessor will always be null, since it is populated from the array of attributes on the model. This array of attributes does not include relationships, whether they're already loaded or not.
If you want to attempt to solve this with an accessor, you would just ignore whatever value is passed in, and check the relationship yourself.
public function getAuthorAttribute($value)
{
$key = 'author';
/**
* If the relationship is already loaded, get the value. Otherwise, attempt
* to load the value from the relationship method. This will also set the
* key in $this->relations so that subsequent calls will find the key.
*/
if (array_key_exists($key, $this->relations)) {
$value = $this->relations[$key];
} elseif (method_exists($this, $key)) {
$value = $this->getRelationshipFromMethod($key);
}
$value = $value ?: new Author();
/**
* This line is optional. Do you want to set the relationship value to be
* the new Author, or do you want to keep it null? Think of what you'd
* want in your toArray/toJson output...
*/
$this->setRelation($key, $value);
return $value;
}
Now, the problem with doing this in the accessor is that you need to define an accessor for every hasOne/belongsTo relationship on every model.
A second, smaller, issue is that the accessor is only used when accessing the attribute. So, for example, if you were to eager load the relationship, and then dd() or toArray/toJson the model, it would still show null for the relatioinship, instead of an empty Author.
Overriding Model Methods
A second option, instead of using attribute accessors, would be to override some methods on the Model. This solves both of the problems with using an attribute accessor.
You can create your own base Model class that extends the Laravel Model and overrides these methods, and then all of your other models will extend your base Model class, instead of Laravel's Model class.
To handle eager loaded relationships, you would need to override the setRelation() method. If using Laravel >= 5.2.30, this will also handle lazy loaded relationships. If using Laravel < 5.2.30, you will also need to override the getRelationshipFromMethod() method for lazy loaded relationships.
MyModel.php
class MyModel extends Model
{
/**
* Handle eager loaded relationships. Call chain:
* Model::with() => Builder::with(): sets builder eager loads
* Model::get() => Builder::get() => Builder::eagerLoadRelations() => Builder::loadRelation()
* =>Relation::initRelation() => Model::setRelation()
* =>Relation::match() =>Relation::matchOneOrMany() => Model::setRelation()
*/
public function setRelation($relation, $value)
{
/**
* Relationships to many records will always be a Collection, even when empty.
* Relationships to one record will either be a Model or null. When attempting
* to set to null, override with a new instance of the expected model.
*/
if (is_null($value)) {
// set the value to a new instance of the related model
$value = $this->$relation()->getRelated()->newInstance();
}
$this->relations[$relation] = $value;
return $this;
}
/**
* This override is only needed in Laravel < 5.2.30. In Laravel
* >= 5.2.30, this method calls the setRelation method, which
* is already overridden and contains our logic above.
*
* Handle lazy loaded relationships. Call chain:
* Model::__get() => Model::getAttribute() => Model::getRelationshipFromMethod();
*/
protected function getRelationshipFromMethod($method)
{
$results = parent::getRelationshipFromMethod($method);
/**
* Relationships to many records will always be a Collection, even when empty.
* Relationships to one record will either be a Model or null. When the
* result is null, override with a new instance of the related model.
*/
if (is_null($results)) {
$results = $this->$method()->getRelated()->newInstance();
}
return $this->relations[$method] = $results;
}
}
Book.php
class Book extends MyModel
{
//
}
I had the same problem in my project. In my views there's some rows that are accesing to dinamics properties from null relationships, but instead of returning an empty field, the app was thrwoing and exception.
I just added a foreach loop in my controller as a temporal solution that verifies in every value of the collection if the relationship is null. If this case is true, it assigns a new instance of the desire model to that value.
foreach ($shifts as $shift)
{
if (is_null($shift->productivity)) {
$shift->productivity = new Productivity();
}
}
This way when I access to $this->productivity->something in my view when the relationship is unset, I get a empty value instead of an exception without putting any logic in my views nor overriding methods.
Waiting for a better solution to do this automatically.
You can achieve this using model factories.
Define an author factory inside your ModelFactory.php
$factory->define(App\Author::class, function (Faker\Generator $faker) {
return [
'name' => $faker->firstName, //or null
'avatar' => $faker->imageUrl() //or null
];
});
add values for all the needed attributes I am using dummy values from Faker but you can use anything you want.
Then inside your book model you can return an instance of Author like this:
public function getAuthorAttribute($author)
{
return $author ?: factory(App\Author::class)->make();
}

Laravel orWhere not working with hasMany

I have following code:
class Ingredient extends Eloquent
{
public function units()
{
return $this->hasMany('IngredientUnit')
->orWhere('ingredient_id', '=', -1);
}
}
I would expect query like:
select * from `ingredient_units` where `ingredient_id` = '-1' OR `ingredient_units`.`ingredient_id` in (...)
instead I get:
select * from `ingredient_units` where `ingredient_id` = '-1' and `ingredient_units`.`ingredient_id` in (...)
Why it use AND operator instead OR, when I used orWhere()?
Update 1:
And second question is how can I get a query which I was expected?
Update 2:
I want to use eagerloading for that
When you fetch a collection of objects through a relation on a model, the relation constraint is always included, hence the AND. And it makes perfect sense, otherwise you could get $model->units objects that are not related to $model.
I can see what you're trying to achieve here - fetch units related to that $model together with units not related to any models. You can achieve it by adding the following method to your model:
public function getAllUnits() {
$genericUnits = IngredientUnit::whereIngredientId(-1);
return $this->units()->union($genericUnits)->get();
}
OR
public function getAllUnits() {
return IngredientUnit::whereIn('ingredient_id', [$this->id, -1])->get();
}
The only issue here is that it won't be used by eager loading logic but would result in separate query for every model for which you want to return units. But if you always fetch that for a single model anyway, it won't be a problem.
One suggestion: store NULL in ingredient_id instead of -1. This way you'll be able to make use of foreign key constraints.

Categories