Home > Enterprise >  Add field from query data to an Eloquent model without using an Attribute
Add field from query data to an Eloquent model without using an Attribute

Time:01-13

I want to add a field to an Eloquent model instance. This field data comes from a query, not a constant or a value.

Yes, it could be done adding a field to $appends and adding an Attribute, but the problem with this approach is that it's not really added to the object as a field but as something like a function call, which makes that every time it's accessed it causes a query to the database, which if the data won't change will be time consuming needlessly.

In this case I could create a PDO and populate it, but I wonder if this could be done with Eloquent's ways.

I tried adding a Scope or something within the boot method, but nothing that worked.

I have searched but haven't found anything, so here I am asking if could it be done with a Scope, or the boot method or in any other Laravel/Eloquent way. Thanks for your time.

CodePudding user response:

So to make some things clear here. Using accessors on a model is something like computed fields, where you are able to call an attribute on a model that doesn't map to a field in the table, something like:

===

A note on Laravel 9 documentation

To make things more clearly, this was the only way to use accessors with a model prior to Laravel 9. Since the release of Laravel 9, this approach isn't mentioned anymore in the documentation and the use of Attribute is introduced.

class User extends Model {
    protected function firstName(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => ucfirst($value),
        );
    }
}

// will search for a method called `firstName` and then `getFirstNameAttribute`.
User::find(1)->first_name; 

To my opinion, this isn't making any improvement at all. This just narrows the models scope for using function names where it will accidentally hit a magic method call unwillingly due to all the magicness happening in Eloquents models already.

===

class User extends Model {

  function getMyFieldAttribute() {
    return "user {$this->id} computed value"  
  }

}

// will result in 'user 1 computed value'
echo User::find(1)->my_field;

The $appends attribute is used to append any none existing field from the table or relation to a processed output like JSON using User::toJson() or User::toArray(). You can also use the $appends field to save the state of any accessor field you define. Now you can use the combination of the two and if you would like to use a query to fill the attribute but this query should be executed once, just check if the attribute exists and if so, skip it.

class User extends Model {
    
  protected $appends = ['my_field'];

  // using $appends
  function getMyFieldAttribute() {
    if(!isset($this->attributes['my_field'])) {
      // perform a query, setting the time as an example
      $this->attributes['my_field'] = now();
    }

    return $this->attributes['my_field'];
  }
  
}

$User = User::find(1);
$index = 0;
while($index < 5) {
  // this query will be executed once
  echo "{$User->my_field}\n";
  $index  ;
  // in this example you will see 5 times the same timestamp.
  sleep(1);
}


As yhiamdan pointed out in his answer, the getter method getMyFieldAttribute first parameter will be filled with the computed attributes value if set. On first call this value will be null, on any subsequent call it will be filled with the computed value if set with setAttribute or $this->attributes.

class User extends Model {

  public function getMyFieldAttribute($value) {
    if(!$value) {
      $value = now()
      $this->setAttribute('my_field', $value);
      // or the same with
      // $this->attributes['my_field'] = $value;
    }
    
    return $value;
  }

}

You cannot mix eloquents attributes with a defined property on the class User, this will never reach the magic methods __get or __set used to magically call the functions getMyFieldAttribute or setMyFieldAttribute.

class User extends Model {
  
  public $my_field = 'overwrite';

  public function getMyFieldAttribute() {
    return 'this will never be reached'
  }  

}

echo User::find(1)->my_field; // will output 'overwrite'

Just remind yourself that this is only the getter method of an accessor field. So the attribute my_field should have a getMyFieldAttribute and setMyFieldAttribute function. In the above setup, the getMyFieldAttribute is used as both where the value is variable and static when assigned.

CodePudding user response:

Yes, it could be done adding a field to $appends and adding an Attribute, but the problem with this approach is that it's not really added to the object but something like a function call, and every time it's accessed it causes a query to the database.

Yes whenever you have an attribute defined on a model and you try to access it, the accessor function will be called every time. Its your job to decide whether to query the database or return the original result during the first query.

Lets say you have an attribute fullname on a user model and for some reasons computing the fullname is expensive and you do not want to recompute everytime the the attribute is accessed, you can set the result after the initial computation to your model's attributes using $this->setAttribute("fullname",$result). This way, whenever you try to access the attribute, laravel will pass the value you assigned/set to the attribute to the accessor function, and you can check and decide whether to recompute or simply return the previous result.

This is an example

    public function getFullnameAttribute($val){
    if($val){
      return $val;
    }
    $fullname = //expensive computation;
    $this->setAttribute("fullname",$fullname);
    return $fullname;
    }

However what you need to consider is situations where the fullname attribute could be stale.

For instance when you retrieve a user model, you retrieve the fullname for the first time, then you change eg. the firstname attribute and then you try to retrieve the fullname attribute again, you will get the original fullname even if its dependent of the firstname attribute.

I hope this helps.

  • Related