12

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);
}
Richard
  • 4,341
  • 5
  • 35
  • 55
whoacowboy
  • 6,982
  • 6
  • 44
  • 78
  • @ceejayoz I quote that question and answer in my question. I am looking for another way of doing this. If you see that other way in an answer please point it out. – whoacowboy Jun 18 '15 at 17:51
  • 1
    @ceejayoz I voted to reopen this. The OP isn't having an issue with overloading the constructor in the class. The OP is asking for a way to not have to, and there is a Laravel-specific way to achieve what the user wants. – patricus Jun 18 '15 at 20:46

3 Answers3

13

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.

Community
  • 1
  • 1
IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • 1
    I was going to also suggest a different technique, by over-riding the definition of [`getAttributeValue`](https://github.com/laravel/framework/blob/5.1/src/Illuminate/Database/Eloquent/Model.php#L2543) to be more "hookable", and leaving `$appends` alone, but I spent too long preparing the Reflection version to figure it out right now. – IMSoP Jul 16 '15 at 20:18
  • Thanks for the thoughtful answer. In this case if `ReDecoratedModel` has a `__construct` method, will it conflict with the traits `__construct` methods? – whoacowboy Jul 16 '15 at 20:29
  • @whoacowboy Yes, because the `AppendingGlue` trait gets pasted right into that class, and you can't have two constructors at once, so PHP has to choose one (or choose to error). However, it's reasonably easy to work around based on the answer you linked to. I'll add one more example. :) – IMSoP Jul 16 '15 at 20:41
  • Thanks for the updates! Let me try some of these guys out. – whoacowboy Jul 16 '15 at 20:55
4

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';
  }
}
Kebian
  • 664
  • 6
  • 17
2

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);
Emeka Mbah
  • 16,745
  • 10
  • 77
  • 96
  • Thanks for the answer. I currently am going down the easy path. I was just looking for a way to completely decouple the trait to make it cleaner. By the way, the code I have in my example is currently working with out issue. I think that is a style thing, but I could be mistaken. – whoacowboy Jun 18 '15 at 18:20