Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid clashing PHP traits used for dependency injection

I'm finally getting around to exploring traits in PHP. The first place I thought I'd try it out is injection of config bits into classes. If I'm using DIC, I might have code like this in any class that needs a config object:

protected function SetConfig($config) {
    $this->config = $config;
}

protected $config;

This seems like a natural fit to traits to avoid having that boilerplate code all over the place, so I might create this:

trait Config {
    protected function SetConfig($config) {
        $this->config = $config;
    }

    protected $config;
}

and then use it like so:

class Foo {
    use Config;

    public function __construct() {
        //can now use $this->config
    }
}

That's great. Now let's say I want to create a second trait, say, for logging:

trait Logger {
    protected function SetLogger($logger) {
        $this->logger = $logger;
    }

    protected $logger;
}

Which I can use like this:

class Foo {
    use Logger;

    public function __construct() {
        //can now use $this->logger
    }
}

Also great. Now the problem comes if those two traits want to use each other. It seems quite reasonable that a logger class would need to have a config object injected, which means doing this:

trait Logger {
    use Config;

    protected function SetLogger($logger) {
        $this->logger = $logger;
    }

    protected $logger;
}

But then things will break when another class uses both of these traits:

class Foo {
    use Config, Logger;

    public function __construct() {
        //want to use $this->config and $this->logger
    }
}

This, of course, doesn't work because the config bits are effectively duplicated in Foo.

I could just leave out the use Config; piece from the Logger trait, knowing that it'll be there in the end. But this feels weird to me as it creates a sort of external dependency. What if I want to use the Logger someplace that didn't already have the config trait? This solution also means I need to suffer my IDE (PhpStorm 8) warning me about unknown methods, and not offering autocompletion. I realize I could fix these problems in turn by using @method, but that's just putting lipstick on a pig, so to speak.

I could also alias the config bits in Logger, but that's also problematic.

All of this has a bit of a smell to it, but I haven't figured out yet whether that's because this is a new pattern for me or if it really is a stinky pattern. Either way, I'm not sure the best way to make this approach actually work.

Any advice on the best way to solve this problem in traits? Or is it better to avoid traits for DIC shortcutting?

like image 670
mr. w Avatar asked Oct 10 '14 20:10

mr. w


People also ask

Is PHP trait good?

PHP Traits are Bad On the surface, there is strong support for PHP traits because using them can help reduce code duplication throughout your application. In addition, they can help improve maintainability and the cleanliness of your code.

Are PHP traits Mixins?

Traits are static access in PHP as explained by Benjamin Eberlei on his blog. They are similar to automated copy-paste of code. This does not mean that traits are absolutely evil, but they most of the time impose coupling issues. Mixin in general is the usual (somewhat safe) use of multiple inheritance.

Can trait implement interface PHP?

Note that PHP does not allow multiple inheritance. So Traits is used to fulfill this gap by allowing us to reuse same functionality in multiple classes. Traits can not implement interfaces. A trait allow both classes to use it for common interface requirement.

Can traits have abstract methods PHP?

Traits can have methods and abstract methods that can be used in multiple classes, and the methods can have any access modifier (public, private, or protected).


1 Answers

The method I've found useful is using getters as well as setters. This then allows you to require that a specific getter exists, without conflicting with other traits.

trait Config {
    protected function SetConfig($config) {
        $this->config = $config;
    }

    protected function GetConfig() {
        return $this->config;
    }

    protected $config;
}

trait Logger {
    abstract protected function GetConfig();

    protected function SetLogger($logger) {
        $this->logger = $logger;
    }

    protected $logger;
}

class Baz {
    use Config, Logger;

    // ...

}

In Baz, the Config trait provides the required abstract method and Baz is composed without error. If you mistakenly only use Logger you will get a Fatal Error: Class Baz contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Baz::GetConfig)

like image 75
Jeff Horton Avatar answered Oct 03 '22 03:10

Jeff Horton