Asked  7 Months ago    Answers:  5   Viewed   34 times

I've been trying to study up on PHP lately, and I find myself getting hung up on traits. I understand the concept of horizontal code reuse and not wanting to necessarily inherit from an abstract class. What I don't understand is: What is the crucial difference between using traits versus interfaces?

I've tried searching for a decent blog post or article explaining when to use one or the other, but the examples I've found so far seem so similar as to be identical.

 Answers

67

An interface defines a set of methods that the implementing class must implement.

When a trait is use'd the implementations of the methods come along too--which doesn't happen in an Interface.

That is the biggest difference.

From the Horizontal Reuse for PHP RFC:

Traits is a mechanism for code reuse in single inheritance languages such as PHP. A Trait is intended to reduce some limitations of single inheritance by enabling a developer to reuse sets of methods freely in several independent classes living in different class hierarchies.

Wednesday, March 31, 2021
 
Gordnfreeman
answered 7 Months ago
43

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.

Wednesday, March 31, 2021
 
laurent
answered 7 Months ago
68

When mocking you need to use the full qualified class path as the mock functionality is not taking the namespace of the calling code or any "use" statements into consideration.

Try

->getMock('\Symfony\Component\Routing\RouterInterface'); 

and leave out the second parameter. Usually specifying the methods does a lot more worse than good. Only if you want all the other methods to work like before than you should need the second parameter.

Example

<?php

namespace bar;

class MyClass {}

namespace foo;

use barMyClass;

class MockingTest extends PHPUnit_Framework_TestCase {

    public function testMock() {
        var_dump($this->getMock('MyClass') instanceOf MyClass);
        var_dump($this->getMock('\bar\MyClass') instanceOf MyClass);
    }   
}

Produces:

/phpunit.sh --debug fiddleTestThree.php 
PHPUnit @package_version@ by Sebastian Bergmann.


Starting test 'fooMockingTest::testMock'.
.bool(false)
bool(true)
Wednesday, March 31, 2021
 
Angolao
answered 7 Months ago
12
function Trait (methods) {
  this.traits = [methods];
};

Trait.prototype = {
    constructor: Trait

  , uses: function (trait) {
      this.traits = this.traits.concat (trait.traits);
      return this;
    }

  , useBy: function (obj) {
      for (var i = 0; i < this.traits.length; ++i) {
        var methods = this.traits [i];
        for (var prop in methods) {
          if (methods.hasOwnProperty (prop)) {
            obj [prop] = obj [prop] || methods [prop];
          }
        }
      }
    }
};

Trait.unimplemented = function (obj, traitName) {
  if (obj === undefined || traitName === undefined) {
    throw new Error ("Unimplemented trait property.");
  }
  throw new Error (traitName + " is not implemented for " + obj);
};

Example:

var TEq = new Trait ({
    equalTo: function (x) {
      Trait.unimplemented (this, "equalTo");
    }

  , notEqualTo: function (x) {
      return !this.equalTo (x);
    }
});

var TOrd = new Trait ({
    lessThan: function (x) {
      Trait.unimplemented (this, "lessThan");
    }

  , greaterThan: function (x) {
      return !this.lessThanOrEqualTo (x);
    }

  , lessThanOrEqualTo: function (x) {
      return this.lessThan (x) || this.equalTo (x);
    }

  , greaterThanOrEqualTo: function (x) {
      return !this.lessThan (x);
    }
}).uses (TEq);


function Rational (numerator, denominator) {
  if (denominator < 0) {
    numerator *= -1;
    denominator *= -1;
  }
  this.numerator = numerator;
  this.denominator = denominator;
}

Rational.prototype = {
    constructor: Rational

  , equalTo: function (q) {
      return this.numerator * q.numerator === this.denominator * q.denominator;
    }

  , lessThan: function (q) {
      return this.numerator * q.denominator < q.numerator * this.denominator;
    }
};

TOrd.useBy (Rational.prototype);

var x = new Rational (1, 5);
var y = new Rational (1, 2);

[x.notEqualTo (y), x.lessThan (y)]; // [true, true]
Sunday, June 13, 2021
 
Santi
answered 5 Months ago
53

self does not refer to the instance, it refers to the current class. There is no way for an interface to specify that the same instance must be returned - using self in the manner you're attempting would only enforce that the returned instance be of the same class.

That said, return type declarations in PHP must be invariant while what you're attempting is covariant.

Your use of self is equivalent to:

interface iFoo
{
    public function bar (string $baz) : iFoo;
}

class Foo implements iFoo
{

    public function bar (string $baz) : Foo  {...}
}

which is not allowed.


The Return Type Declarations RFC has this to say:

The enforcement of the declared return type during inheritance is invariant; this means that when a sub-type overrides a parent method then the return type of the child must exactly match the parent and may not be omitted. If the parent does not declare a return type then the child is allowed to declare one.

...

This RFC originally proposed covariant return types but was changed to invariant because of a few issues. It is possible to add covariant return types at some point in the future.


For the time being at least the best you can do is:

interface iFoo
{
    public function bar (string $baz) : iFoo;
}

class Foo implements iFoo
{

    public function bar (string $baz) : iFoo  {...}
}
Wednesday, July 28, 2021
 
fret
answered 3 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :