Experimenting with dependency injection in widget classes.
How the flow works
WordPress will instantiate widgets on the widgets_init
action.
That action is called from the wp_widgets_init
function defined in the widgets.php
file.
Plugins and themes usually hook into such an action to call the register_widget
function, this is the function body
function register_widget($widget_class) {
global $wp_widget_factory;
$wp_widget_factory->register($widget_class);
}
The register_widget
function takes one parameter only: the fully qualified name of a class extending the WP_Widget
that will in turn be used to create a new widget instance by the WP_Widget_Factory::register
method.
The body of that method is this
public function register( $widget_class ) {
$this->widgets[$widget_class] = new $widget_class();
}
It’s not a complicated flow.
What this flow lacks is the possibility to do any kind of injection: the way the widget class __construct
method is called presumes no parameters will be needed by the constructor and no space for constructor or setter method based injection is available.
Why Dependency Injection again?
Dependency Injection means that anything the class instance depends upon to work must be supplied to it from the outside rather than being autonomously instantiated or “taken from the aether”.
Starting from the example widget class implementation shown on the codex I’ve modified it imagining a widget that needs to convey an action link (“cta” stands for “call to action”) associated with some kind of e-commerce plugin.
class myplugin_CTAWidget extends WP_Widget {
/**
* Sets up the widgets name etc
*/
public function __construct() {
$widget_ops = array(
'class_name' => 'my_widget',
'description' => 'My Widget is awesome',
);
parent::__construct( 'my_widget', 'My Widget', $widget_ops );
}
/**
* Outputs the content of the widget
*
* @param array $args
* @param array $instance
*/
public function widget( $args, $instance ) {
if $this->user->is_connected(){
$user_type = $this->user->get_type();
$message = $this->user_messages->get_cta_for($user_type);
$url = $this->user_urls->get_for($user_type);
} elseif ($this->user->is_pending_registration()) {
$message = $this->user_messages->get_pending_registration_cta();
$url = $this->user_urls->get_pending_registration_url();
} else {
$message = $this->user_messages->get_register_cta();
$url = $this->user_urls->get_register_url();
}
echo sprint_f('<a href="%s">%s</a>', $url, $message);
}
/**
* Outputs the options form on admin
*
* @param array $instance The widget options
*/
public function form( $instance ) {
// outputs the options form on admin
}
/**
* Processing widget options on save
*
* @param array $new_instance The new options
* @param array $old_instance The previous options
*/
public function update( $new_instance, $old_instance ) {
// processes widget options to be saved
}
}
The widget
method will call on three dependencies:
user
represents the current whose responsibility is to model the user visiting the pageuser_messages
represents a class whose responsibility is to return the correct user related messages for the siteuser_urls
represents a class whose responsibility is to return the correct user related urls for the site
The code above is not instantiating those instances in any way and will miserably fail; a first approach might be to build those dependencies in the widget __construct
method
public function __construct() {
$this->user = new myplugin_User();
$this->user_messages = new myplugin_UserMessages();
$this->user_urls = new myplugin_UserUrls();
$widget_ops = array(
'class_name' => 'my_widget',
'description' => 'My Widget is awesome',
);
parent::__construct( 'my_widget', 'My Widget', $widget_ops );
}
The Dependency Injection factor of the widget is none: there is no way to write a test for the widget class and control those three dependencies; any test aimed at the class will need to have knowledge of the class and of any of its dependencies.
Say I want to test that a not connected user will get a connect message and url I would need to write a test like this
public function tests_not_connected_user_get_connect_cta_and_url(){
$sut = new myplugin_CTAWidget();
$urls = new myplugin_UserUrls();
$messages = new myplugin_UserMessages();
$url = $urls->get_register_url();
$message = $messages->get_register_cta();
$anchor = $sut->widget();
$expected = sprintf('<a href="%s">%s</a>', $url, $message);
$this->assertEquals($expected, $anchor);
}
What’s wrong in this test is that I’m testing an output, the anchor, and know how it’s made exactly; my test has a strong coupling with the current code and does not test what I’m interested in: how the anchor is built, not the final anchor markup.
From the aether
When I wrote “taken from the aether” I mean this
public function __construct() {
global $user, $user_messages, $user_urls;
$this->user = $user;
$this->user_messages = $user_messages;
$this->user_urls = $user_urls;
$widget_ops = array(
'class_name' => 'my_widget',
'description' => 'My Widget is awesome',
);
parent::__construct( 'my_widget', 'My Widget', $widget_ops );
}
and this
public function __construct() {
$this->user = myplugin_User::instance();
$this->user_messages = myplugin_UserMessages::instance();
$this->user_urls = myplugin_UserUrls::instance();
$widget_ops = array(
'class_name' => 'my_widget',
'description' => 'My Widget is awesome',
);
parent::__construct( 'my_widget', 'My Widget', $widget_ops );
}
and poses the same challenges as above being yet again not controllable dependencies (not easily and reliably).
Plus the widget class constructor tells nothing (as in “type-hints nothing”) about what it will need to work.
Default arguments for the constructor are a middle ground that’s immediately appliable
public function __construct(
myplugin_UserInterface $user = null,
myplugin_UserMessagesInterface $user_messages = null,
myplugin_UserUrlsInterface $user_urls = null
) {
$this->user = $user ? $user : new myplugin_User();
$this->user_messages = $user_messages ? $user_messages : new myplugin_UserMessages();
$this->user_urls = $user_urls ? $user_urls : new myplugin_UserUrls();
$widget_ops = array(
'class_name' => 'my_widget',
'description' => 'My Widget is awesome',
);
parent::__construct( 'my_widget', 'My Widget', $widget_ops );
}
and allows rewriting the test above
public function tests_not_connected_user_get_connect_cta_and_url(){
$user = $this->prophesize('myplugin_User');
$user->is_connected()->willReturn(false);
$messages = $this->prophesize('myplugin_UserMessages');
$messages->get_register_cta()->shouldBeCalled();
$urls = $this->prophesize('myplugin_UserUrls');
$urls->get_register_url()->shouldBeCalled();
$sut = new myplugin_CTAWidget($user->reveal(), $messages->reveal(), $urls->reveal());
$sut->widget();
}
Next
Hacking the widget instantiation process to have it my way.