Traits with PHP and Laravel

17.5k views Asked by At

I am using Laravel 5.1 and would like to access an array on the Model from the Trait when the Model before the model uses the appends array.

I would like to add certain items to the appends array if it exists from my trait. I don't want to edit the model in order to achieve this. Are traits actually usable in this scenario or should I use inheritance?

array_push($this->appends, 'saucedByCurrentUser');

Here is how my current setup works.

Trait

<?php namespace App;

trait AwesomeSauceTrait {

  /**
   * Collection of the sauce on this record
   */
  public function awesomeSauced()
  {
    return $this->morphMany('App\AwesomeSauce', 'sauceable')->latest();
  }
  public function getSaucedByCurrentUserAttribute()
  {
    if(\Auth::guest()){
        return false;
    }
    $i = $this->awesomeSauced()->whereUserId(\Auth::user()->id)->count();
    if ($i > 0){
        return true;
    }
    return false;
  }
}

Model

<?php namespace App;

use App\AwesomeSauceTrait;
use Illuminate\Database\Eloquent\Model;

class FairlyBlandModel extends Model {
    use AwesomeSauceTrait;

    protected $appends = array('age','saucedByCurrentUser');

}

What I would like to do is something to achieve the same effect as extending a class. I have a few similar traits, so using inheritance gets somewhat ugly.

trait AwesomeSauceTrait {
 function __construct() {
     parent::__construct();
     array_push($this->appends, 'saucedByCurrentUser');
 }
}

I have seen some workarounds for this, but none of them seem better/cleaner than just adding the item to the array manually. Any ideas are appreciated.

Update


I discovered this way of accomplishing what I need for one trait, but it only works for one trait and I don't see an advantage of using this over inheritance.

trait

protected $awesomeSauceAppends = ['sauced_by_current_user'];

protected function getArrayableAppends()
{
    array_merge($this->appends, $this->awesomeSauceAppends);
    parent::getArrayableAppends();
}

How I am currently handling my Model, for what it is worth.

model

public function __construct()
{
    array_merge($this->appends, $this->awesomeSauceAppends);
}
3

There are 3 answers

4
IMSoP On BEST ANSWER

Traits are sometimes described as "compiler-assisted copy-and-paste"; the result of using a Trait can always be written out as a valid class in its own right. There is therefore no notion of parent in a Trait, because once the Trait has been applied, its methods are indistinguishable from those defined in the class itself, or imported from other Traits at the same time.

Similarly, as the PHP docs say:

If two Traits insert a method with the same name, a fatal error is produced, if the conflict is not explicitly resolved.

As such, they are not very suitable for situations where you want to mix in multiple variants of the same piece of behaviour, because there is no way for base functionality and mixed in functionality to talk to each other in a generic way.

In my understanding the problem you're actually trying to solve is this:

  • add custom Accessors and Mutators to an Eloquent model class
  • add additional items to the protected $appends array matching these methods

One approach would be to continue to use Traits, and use Reflection to dynamically discover which methods have been added. However, beware that Reflection has a reputation for being rather slow.

To do this, we first implement a constructor with a loop which we can hook into just by naming a method in a particular way. This can be placed into a Trait of its own (alternatively, you could sub-class the Eloquent Model class with your own enhanced version):

trait AppendingGlue {
  public function __construct() {
    // parent refers not to the class being mixed into, but its parent
    parent::__construct();

    // Find and execute all methods beginning 'extraConstruct'
    $mirror = new ReflectionClass($this);
    foreach ( $mirror->getMethods() as $method ) {
      if ( strpos($method->getName(), 'extraConstruct') === 0 ) {
        $method->invoke($this);
      }
    }
  }
}

Then any number of Traits implementing differently named extraConstruct methods:

trait AwesomeSauce {
  public function extraConstructAwesomeSauce() {
    $this->appends[] = 'awesome_sauce';
  }

  public function doAwesomeSauceStuff() {
  }
}

trait ChocolateSprinkles {
  public function extraConstructChocolateSprinkles() {
    $this->appends[] = 'chocolate_sprinkles';
  }

  public function doChocolateSprinklesStuff() {
  }
}

Finally, we mix in all the traits into a plain model, and check the result:

class BaseModel {
  protected $appends = array('base');

  public function __construct() {
    echo "Base constructor run OK.\n";
  }

  public function getAppends() {
    return $this->appends;
  }
}

class DecoratedModel extends BaseModel {
  use AppendingGlue, AwesomeSauce, ChocolateSprinkles;
}

$dm = new DecoratedModel;
print_r($dm->getAppends());

We can set the initial content of $appends inside the decorated model itself, and it will replace the BaseModel definition, but not interrupt the other Traits:

class ReDecoratedModel extends BaseModel {
  use AppendingGlue, AwesomeSauce, ChocolateSprinkles;

  protected $appends = ['switched_base'];
}

However, if you over-ride the constructor at the same time as mixing in the AppendingGlue, you do need to do a bit of extra work, as discussed in this previous answer. It's similar to calling parent::__construct in an inheritance situation, but you have to alias the trait's constructor in order to access it:

class ReConstructedModel extends BaseModel {
  use AppendingGlue { __construct as private appendingGlueConstructor; }
  use AwesomeSauce, ChocolateSprinkles;

  public function __construct() {
    // Call the mixed-in constructor explicitly, like you would the parent
    // Note that it will call the real parent as well, as though it was a grand-parent
    $this->appendingGlueConstructor();

    echo "New constructor executed!\n";
  }
}

This can be avoided by inheriting from a class which either exists instead of the AppendingGlue trait, or already uses it:

class GluedModel extends BaseModel {
  use AppendingGlue;
}
class ReConstructedGluedModel extends GluedModel {
  use AwesomeSauce, ChocolateSprinkles;

  public function __construct() {
    // Standard call to the parent constructor
    parent::__construct();
    echo "New constructor executed!\n";
  }
}

Here's a live demo of all of that put together.

1
Emeka Mbah On

KISS:

I don't see any reason why you should use trait when your are simply appending attributes.

I would only recommend using trait without a constructor like you were doing, only if you model is getting pretty bulky and you wish to slim down things.

Please also note this not the correct way of appending attribute

    protected $appends = array('age','saucedByCurrentUser');

You could do this:

    protected $appends = array('age','sauced_by_current_user');

Appends attribute names should the snake_case of its method Name

Edited:

The idea behind appends is to dynamically add fields that doesn't exist in your database table to your model so after you can do like:

  $model = FairlyBlandModel ::find(1); 
  dd($model->sauced_by_current_user);
0
Kebian On

I thought I'd add an update for 2019 since this was one of the first discussions that popped up when trying to do a similar thing. I'm using Laravel 5.7 and nowadays Laravel will do the reflection that IMSoP mentioned.

After the trait has been booted, Laravel will then call initializeTraitName() on the constructed object (where TraitName is the full name of the trait).

To add extra items to $appends from a trait, you could simply do this...

trait AwesomeSauceTrait {

  public function initializeAwesomeSauceTrait()
  {
    $this->appends[] = 'sauced_by_current_user';
  }

  public function getSaucedByCurrentUserAttribute()
  {
    return 'whatever';
  }
}