Chrome for Android fix.

OOP Plugin Development Basics

Alain Schlesser
Software Engineer & WordPress Consultant

Why OOP ?

Sample Plugin

Sample Plugin

Find it on GitHub:
https://github.com/schlessera/as-speaking

General Plugin Structure


                        /**
                         * Plugin Name: My Plugin
                         * Description: Yep. It's mine.
                         * Text Domain: all-mine
                         * ...rest of plugin header...
                         */

                        namespace Example\Namespace;

                        // Make sure this file is only run from within
                        // WordPress.
                        defined( 'ABSPATH' ) or die();

                        // Load Autoloader class and register plugin
                        // namespace.
                        require_once __DIR__ . '/src/Autoloader.php';
                        ( new Autoloader() )
                            ->add_namespace( __NAMESPACE__, __DIR__ . '/src' )
                            ->register();

                        // Hook plugin into WordPress request lifecycle.
                        PluginFactory::create()
                                     ->register();
                    

Main flow for a basic plugin

  • Plugin header information for WordPress

  • Root namespace

  • Safeguard to not execute outside of WordPress

  • Manually require Autoloader class

  • Add plugin's namespaces to the Autoloader and register them

  • Retrieve a (shared) instance of the plugin

  • Kick off the plugin lifecycle by calling it's register() method

Attaching Hooks

Dynamic Calls


                        class Example {
                            public function test_method() {
                                /* Regular method... */
                            }
                        }

                        $example = new Example();
                        $example->test_method();

                        add_action( 'wp', [ $example, 'test_method' ] );
                    

Static Calls


                        class Example {
                            public static function test_method() {
                                /* Static method... */
                            }
                        }


                        Example::test_method();

                        add_action( 'wp', [ 'Example', 'test_method' ] );
                    

The callback to an instance method is expressed by an array composed of the instance reference, and a string with the exact name of the method.

From within an instance, you can reference that instance through the $this variable.

Note: The method you want to use as a callback needs to be publicly accessible.

Bad


                        final class SomeService {

                            private $data;

                            public function __construct( $data ) {
                                $this->data = $data;
                                add_action(
                                    'init',
                                    [ $this, 'do_stuff' ]
                                );
                            }




                            public function do_stuff() { }
                        }
                    

                        // This "harmless" instantiation will immediately
                        // "do_stuff()". This is an unexpected
                        // side-effect.
                        $service = new SomeService();
                    

Good


                        final class SomeService {

                            private $data;

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

                            public function register() {
                                add_action(
                                    'init',
                                    [ $this, 'do_stuff' ]
                                );
                            }

                            public function do_stuff() { }
                        }
                    

                        // The constructor should only prepare the object,
                        // not immediately use it itself.
                        $service = new SomeService();
                        $service->register();
                    

Services

  • Our Plugin is a collection of Services that work together.

  • Defining the Service as an atomic unit of sorts and giving it an interface allows us to deal with them in bulk.

  • Once the boilerplate code is in place, adding a new service is as easy as adding an element to the array in get_services().

  • get_services() can easily fetch its list from an external config file.


                        final class Plugin {

                            private function get_services() {
                                // We can just treat the services as a
                                // collection of classes or objects.
                                return [
                                    Shortcode\SpeakingPage::class,
                                    CustomPostType\Talk::class,
                                    Metabox\Talk::class,
                                    Widget\Talks::class,
                                ];
                            }

                            public function register_services() {
                                // By unifying the services, we can deal with
                                // them in bulk.
                                foreach ( $this->get_services() as $class ) {
                                    $service = $this->instantiate( $class );
                                    $service->register();
                                }
                            }
                        }
                    

Segregated Interfaces

  • Split up your interface to the smallest logic units.

  • No client should be forced to depend on methods it does not use.

  • These interfaces can be used for type-hinting.

  • Look for recurring mechanisms, and unify them behind one interface, like Registerable, Renderable, ...


                        interface AssetsAware {
                            public function with_assets_handler(
                                AssetsHandler $assets
                            );
                        }
                    

                        final class Plugin {
                            /* ... */
                            private function instantiate_service( $class ) {
                                $service = new $class();

                                // Use setter injection to inject the assets
                                // handler into objects that can deal with it.
                                if ( $service instanceof AssetsAware ) {
                                    $service->with_assets_handler(
                                        $this->assets_handler
                                    );
                                }

                                return $service;
                            }
                        }
                    

Design Patterns

  • provide proven solutions for recurring problems

  • illustrate a principle, not a code snippet

  • improve high-level communication

  • improve high-level reasoning

  • are language-agnostic (!)

Factory Pattern

  • creates and returns an instance of an object

  • can decide what exact object to return

  • can return shared instances

Note: Can be used to get rid of Singletons.
Having the instance-sharing code in a factory leaves the actual object open for standard instantiation, which makes testing much easier.


                        final class PluginFactory {

                            // The Factory decides what exact instance
                            // to return.
                            // In this simple example, it always returns
                            // one shared instance of the Plugin class.
                            public static function create() {
                                static $plugin = null;

                                if ( null === $plugin ) {
                                    $plugin = new Plugin();
                                }

                                return $plugin;
                            }
                        }
                    

A quick note about Singletons

Singleton ≠ shared instance

Singletons:

  • make testing difficult

  • render the constructor useless

  • destroy encapsulation

  • solve the wrong issue

See the following post for more details:
https://www.alainschlesser.com/singletons-shared-instances/

Dequeueing Assets Example


                        // The object we want to instantiate.
                        final class Plugin { /* ... */ }

                        // A factory (with a static method here) that
                        // always returns the same shared instance of
                        // the object we want.
                        final class PluginFactory {
                            public static function create() {
                                static $plugin = null;

                                if ( null === $plugin ) {
                                    $plugin = new Plugin();
                                }

                                return $plugin;
                            }
                        }
                    

                        // Within the bootstrap file, we just
                        // use the factory instead of doing a
                        // "new" call, and then kick off the
                        // plugin.
                        $plugin = PluginFactory::create();
                        $plugin->register();
                    

                        // Within another plugin we can retrieve
                        // the shared instance to work with the
                        // provided public API.
                        $plugin = PluginFactory::create();
                        // Getting the instance of the assets handler
                        // is part of the plugin's public API.
                        $assets = $plugin->get_assets_handler();
                        // Now we can use a clean method to dequeue.
                        // We still have full control over assets.
                        $assets->dequeue( SpeakingPage::CSS_HANDLE );
                    

                        // Get an array of all Talks from the Repository.
                        $repository = new TalkRepository();
                        $talks = $repository->find_all();

                        // Iterate over all the talks and echo their title
                        // and content.
                        foreach ( $talks as $talk ) {
                            /* @var Talk $talk */
                            printf(
                                '<h1>%s</h1>%s',
                                $talk->get_title(),
                                $talk->get_content()
                            );
                        }

                        // Modify a talk and persist the new version.
                        $talk = $repository->find( 42 );
                        $talk->set_title( 'New Title' );
                        $repository->persist( $talk );

                    

Entity

  • Domain object with a unique identity

  • Not tied to a particular persistence mechanism

Repository

  • Abstracts away the persistence mechanism

  • Provides ways to query for entities


                        interface View {
                            public function render();
                        }

                        class BaseView implements View {
                            public function render() {
                                /* ... */
                                return $talk->get_content();
                            }
                        }

                        class PostEscapedView implements View {
                            private $view;
                            public function __construct( View $view ) {
                                $this->view = $view;
                            }
                            // Decorate the actual rendering with an
                            // escaping function.
                            public function render() {
                                return wp_kses_post(
                                    $this->view->render()
                                );
                            }
                        }
                    

Decorator

  • Implements an interface AND accepts an object implementing that same interface as well

  • Acts on all incoming calls as needed, and then forwards them to its inner object


                        // To use decorators, just inject a normal object
                        // into the decorator. Each object behaves like a
                        // View, so you can stack as much as you want.
                        $view = new PostEscapedView( new BaseView() );

                        // The combination of "rendering" and "escaping"
                        // will produce escaped rendering output.
                        echo $view->render();
                    

What's Next ?

Sample Plugin

Learning OOP in PHP

Questions ?

I'm Alain Schlesser.


Follow me on Twitter:

  @schlessera

Or visit my Personal Blog:

   www.alainschlesser.com