Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Traits with PHP and Laravel

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);
}
like image 335
whoacowboy Avatar asked Jun 18 '15 17:06

whoacowboy


People also ask

What is difference between traits and helpers in Laravel?

Laravel traits vs helpersHelpers functions are used by the framework itself and also, you can use them in your own applications as per your convenience. Also, you can create custom helper functions. Traits are commonly utilized code. You can write a trait and can utilize it anywhere you desire.

Where are traits stored in Laravel?

Creating our Trait Creating the Trait is easy! I chose to store all my traits in folder called Traits (usually located at App/Traits), using the namespace App\Traits, this way all my traits are easy to find. You can organise them in subfolders too.

What are Laravel services?

The Laravel service container is a powerful tool for managing class dependencies and performing dependency injection. Dependency injection is a fancy phrase that essentially means this: class dependencies are "injected" into the class via the constructor or, in some cases, "setter" methods.

What is namespace in PHP Laravel?

Namespaces are qualifiers that solve two different problems: They allow for better organization by grouping classes that work together to perform a task. They allow the same name to be used for more than one class.


2 Answers

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.

like image 52
IMSoP Avatar answered Oct 08 '22 14:10

IMSoP


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';
  }
}
like image 35
Kebian Avatar answered Oct 08 '22 15:10

Kebian