12 January 2017

Traits in PHP

What are traits?

Image: Pixabay
Let's start by understanding what traits are and how they're useful.  We'll move on to code examples straight after that.

Traits are not unique to PHP and are available in other languages too.  They provide a way to extend the functionality of a class.  A trait will have methods to implement this functionality and make these available as if they had been defined in the class itself.

In other words traits are flattened into a class and it doesn’t matter if a method is defined in the trait or in the class that uses the trait. You could copy and paste the code from the trait into the class and it would be used in the same manner.

The code that is included into a trait is intended to encapsulate reusable properties and methods that
can be applied to multiple classes.  Traits group functionality in a fine-grained and consistent way and allow you to reuse this functionality without requiring inheritance.

I mentioned before in the lecture on inheritance that PHP does not support inheriting from multiple classes.  Traits offer a way to provide similar functionality and offer a way to avoid the so-called "diamond problem" that is introduced with multiple inheritance.

Defining and using traits

Traits are very easy to use and there are lots of places that they can be useful.  I'm going to run through an example that defines a trait that implements singleton functionality.

We define the trait using the `trait` keyword followed by the name we want to use.  We can declare variables in the trait as well as functions.  

If you look at the code example you'll see that my Singleton trait has got all of the implementation requirements for the singleton pattern.

I've defined a private static variable called $instance in the trait.  Although it's private in the trait remember that the code gets flattened when a class uses the trait and so the class would be able to access it.  If that's not clear just imagine copying and pasting the code in the trait straight into your class.

The trait also includes a static function called getInstance that returns a singleton instance of the class.

I'm using the trait in the class called MyClass.  The `use` keyword indicates that we're using the trait.  You might already be familiar with the `use` keyword as a way to import namespaces.  When we import a namespace the `use` keyword is outside of the class, if it's inside the class it indicates that we're using a trait.

Now we're able to call the `getInstance()` static method on the class.  This method is defined in the trait but we're using the trait in the class so it's available to be used as part of the class.

We're able to overwrite the constructor method in the trait.  In this example I've set the constructor to private so that you are not able to instantiate the class but rather have to use the singleton method to get an instance.  Trying to create a new instance of MyClass generates a fatal error because `__construct()` is public.

Now we're able to easily give a class all of the functionality it needs to be turned into a singleton without interfering with the inheritance hierarchy.

Namespacing Traits

So far we've seen how to declare and use a trait.  What happens if we want to use a name for our trait that has already been using elsewhere in our code?  It might happen for example that one of the libraries we use has already defined a trait named Singleton.

Traits, just like classes, can be namespaced.  The name of a trait must be unique within its namespace otherwise PHP will throw a fatal error.

Just as with classes if you're wanting to use a trait in a namespace other than your own then you'll need to provide a full reference to it.

Inheritance and Method precedence

Traits may not inherit from other traits or classes but you can nest one trait inside another.

Okay so inheritance is simple, but what about situations where a class and trait have a method with the same name?  Which method will take precedence and be run when we try to call it?

Methods declared in a class using a trait take precedence over methods declared in the trait. However, methods in a trait will override methods inherited by a class.

Expressed more simply, precedence in traits and classes is as follows:


Conflict resolution in traits

What happens if your class is using two traits and they both declare a method with the same name?  

PHP requires you to explicitly resolve these sort of conflicts and will throw a fatal error if you try to use two traits that each define a method of the same name.

There are two approaches to resolving name conflicts between traits.  You can either tell PHP not to use a method that is defined in one of the traits or you can tell PHP to use an alias for a method in one of the traits.  Put shorter, you can exclude a method or alias a method.

In order to exclude a method you use the `insteadof` operator which tells PHP to use a method from one trait instead of the method from the other.

In order to alias a method you use the `as` operator to provide an alias to use.  The only trick here is that it's not enough to just specify an alias, you must *also* exclude the original method from being used.

Right, so we've got that we can either exclude or alias a method to resolve naming conflicts between traits.  Lets look at some code to see how it works.

First we're going to create two traits.  We're going to define two methods in each trait and name them such that they'll create naming conflicts.

We're going to use the traits in a class which we're calling DomesticPet.  Because the methods in the traits have conflicting names we're going to have to resolve these conflicts.

We resolve the conflicts in a code block that follows the `use` keyword.  We have to resolve every conflict so lets step through this now.

Replacing methods with "insteadof"

For the first method, `makeNoise()`, we're going to simply exclude the method from the Dog class.  We would do this if we only ever want to use the Cat method and expect never to use the Dog method.

In order to do this we're going to tell the class to use the makeNoise function declared in the Cat trait instead of the one in the Dog trait.  

We use the `insteadof` keyword to tell PHP to use the method declared in the Cat trait instead of the one in the dog class.

Whenever the `makeNoise()` function is called PHP will know that you mean the one defined in the Cat trait and you won't be able to access the one from the Dog trait.

Aliasing methods with "as"

For the second method, `wantWalkies()`, we're going to rather alias the method in Cat.  We would do this when we want to be able to use both methods in our class.

You may have already noticed in the code sample that there are three statements in the conflict resolution block but only two functions that need to be resolved.  

Remember that earlier I said the trick with aliasing methods is that it's not enough to alias by itself and that you must also exclude the method you're aliasing?  

We're going to need two statements to alias our method.

First we're going to tell PHP to give the method an alias that we can use to reference it.  Then we're going to tell PHP to exclude the original method.

In order to alias the method we use the `as` keyword.   We're going to give the `wantWalkies()` method an alias of `kittyWalk()`.  By doing this you will be able to call the `kittyWalk()` function in your class.  This isn't a new function, it's just an alias for the function in the Cat trait.

Right after we alias the function we go ahead and tell PHP to use the Dog `wantWalkies()` method instead of the Cat trait.  If we left out this step we would still have a naming conflict.

The end result

We used the Cat `makeNoise()` function instead of the Dog one.  When we call the `makeNoise()` method on the object we are referencing the cat function and so the output is "Purr".

We aliased the Cat `wantWalkies()` method as `kittyWalk()` and so when we call the `kittyWalk()` method on the object we are referencing the aliased Cat method and the output is "No Thanks".

Because we are aliasing the Cat `wantWalkies` method we also had to use the Dog method instead of the Cat method.  Therefore when we call the `wantWalkies` method on the object we are referencing the Dog method and the output is "Yes Please".


You might want to change the visibility of a method that you're using from a trait.  This is done by using the `as` keyword.  

Lets look at a simple example before we take a look at the complications.

We can set an aliased method as protected by inserting the keyword `protected` between `as` and the name we're aliasing it as.  For example we can say,

Dog::wantWalkies as protected doggyWalk;

Changing the visibility of a replaced method is slightly more complicated.

In order to change the visibility of a replaced method we need to use two steps.  First we use `insteadof` to replace the method and then in the next statement we stipulate that it must be protected.

Lets look at this in code.  

In the first line inside the trait use block I'm aliasing a function and also changing its visibility.  The keyword `protected` appears between `as` and the aliased name.  If I were to try and call `doggyWalk()` from outside the class I would receive an error because it is protected.

I've commented out the second line because it's an example of what doesn't work.  The rule of thumb seems to be that you can't use both `as` and `insteadof` in one single statement.  You need to split them into separate statements as I do in the last two lines of the use block.


No comments:

Post a Comment