Quantcast
Channel: Plugins – WPKrauts
Viewing all articles
Browse latest Browse all 3

Initialize a plugin with a configuration object

$
0
0

The WordPress plugin API, based on actions and filters called in a certain order, forces plugin authors to follow a simple rule to guarantee interoperability: Whatever you do, offer a way to turn it off.

If your plugin uses a code like this …

add_action( 'wp_loaded', 'my_callback' );

… other plugins or a theme can turn it off with …

remove_action( 'wp_loaded', 'my_callback' );

This won’t work with objects:

add_action( 
    'wp_loaded', 
    array ( new Plugin_Object, 'setup' ) 
);

Another plugin has no access to the object, so it will be very difficult to remove this callback.

There are two solutions for this problem: static access to the instance of the main class and a filter for a configuration object.

Static access to plugin instance

An example for the first solution can be found in this gist. Basically, we don’t use new Plugin_Object, but new Plugin_Object::get_instance() to create the object.

public static function get_instance()
{
	NULL === self::$instance and self::$instance = new self;

	return self::$instance;
}

Initialization works like this now:

add_action(
	'wp_loaded',
	array ( Plugin_Object::get_instance(), 'setup' )
);

This works, and it has become quite popular. But I never liked the static method. The only benefit is an on/off option, the ability to stop a plugin. But what if we want to change just some part of the plugin without disabling it completely? We had to add some actions and filters, more and more probably. I think this is not very elegant.
That’s why I prefer a configuration object filter now.

Configuration object filter

In this structure, the main controller object will be created always. But, as the term controller already suggests, it is just a light-weight object with one job: sticking other objects together.

namespace Wpkrauts;

class Controller
{
    protected $plugin_file;

    public function __construct( $file )
    {
        $this->plugin_file = $file;

        \add_action( 'wp_loaded', array ( $this, 'plugin_setup' ) );
    }

    public function plugin_setup()
    {
        $data = new \stdClass;
        $this->set_plugin_data( $data );

        // class names
        $data->model   = __NAMESPACE__ . '\Plugin_Log_Data';
        $data->view    = __NAMESPACE__ . '\Console_Live_Logger';

        /* You can change the class names here, the new classes have just to
         * implement the interfaces.
         * Or set "$data->stop" to TRUE in the filter to stop further processing.
         */
        \apply_filters( 'wpkrauts_base_plugin_data', $data );

        if ( ! empty ( $data->stop ) )
            return;

        $model = new $data->model( $data );
        $view  = new $data->view( $model );

        \add_action(
            'wp_print_footer_scripts',
            array ( $view, 'show' )
        );

        \do_action(
            'wpkrauts_base_plugin_loaded',
            $model,
            $view,
            $data
        );
    }

    protected function set_plugin_data( $data )
    {
        $data->url     = \plugins_url( '', $this->plugin_file );
        $data->dir     = \plugin_dir_path( $this->plugin_file );
        $plugin_info   = \get_file_data(
            $this->plugin_file,
            array (
                'name'    => 'Plugin Name',
                'version' => 'Version'
            )
        );
        $data->name    = $plugin_info['name'];
        $data->version = $plugin_info['version'];
    }
}

As you can see I separated the plugin code by following a MVC structure. Plugin_Log_Data and Console_Live_Logger are the model and the view. Both implement simple interfaces:

interface Log_Data
{
    public function __construct( \stdClass $data );

    public function get_value( $name );
}

interface Live_Logger
{
    public function __construct( Log_Data $model );

    public function show();
}

The actual implementation in my sample case logs the plugin data to the browser’s JavaScript console.

Screenshot of the result in a console

class Plugin_Log_Data implements Log_Data
{
    protected $data;

    public function __construct( \stdClass $data )
    {
        $this->data = $data;
    }

    public function get_value( $name )
    {
        if ( isset ( $this->data->$name ) )
            return $this->data->$name;

        return new \WP_Error(
            '1',
            "Invalid name: $name",
            debug_backtrace()
        );
    }
}

class Console_Live_Logger implements Live_Logger
{
    protected $model;

    public function __construct( Log_Data $model )
    {
        $this->model = $model;
    }

    public function show()
    {
        printf(
            '<script>console.log( \'%1$s\nVersion: %2$s\n%3$s\' );</script>',
            $this->get_js_var( 'name' ),
            $this->get_js_var( 'version' ),
            $this->get_js_var( 'bogus' ) // error demo
        );
    }

    protected function get_js_var( $var )
    {
        $text = $this->model->get_value( $var );

        if ( is_wp_error( $text ) )
            return "ERROR: " . esc_js( $text->get_error_message() );

        return esc_js( $text );
    }
}

This might not be the most useful example, but I hope it is easy to understand.

Now let’s go back to the controller. There is an important line:

\apply_filters( 'wpkrauts_base_plugin_data', $data );

Since object variables are passed as an identifier in PHP we don’t even need a return value from the filter. You can change it in a custom additional function, and these changes will be applied immediately.

To stop further execution of my plugin you can do this:

\add_filter( 'wpkrauts_base_plugin_data', function( $data )
{
    $data->stop = TRUE;
});

Dead simple, almost too trivial.

But you can do much more:

  • You can change $data->model and use a class that implements the Wpkrauts\Log_Data interface. Maybe you want to log WordPress internals or data about a different plugin or a theme?
  • You can change the output handler, again a class implementing an interface, this time Wpkrauts\Live_Logger. Want to show the log content in HTML or send it as HTTP headers? Just replace the class name. In these cases you will probably set $data->stop to TRUE and assign the action for the view in your code.
  • You can also let the plugin do its work and just reuse the classes elsewhere. For example, in your plugin’s controller you use my view class.

All of this and probably more is possible with just one single filter. No static methods are needed anymore.

The configuration object I have used here is a simple stdClass. This is not ideal, and when you need a copy it might become expensive. I will show an alternative in a later article.

You can find all code examples for this article on GitHub.


Viewing all articles
Browse latest Browse all 3

Latest Images

Trending Articles





Latest Images