Find it on GitHub:
https://github.com/schlessera/as-speaking
/**
* 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();
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
class Example {
public function test_method() {
/* Regular method... */
}
}
$example = new Example();
$example->test_method();
add_action( 'wp', [ $example, 'test_method' ] );
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.
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();
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();
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();
}
}
}
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;
}
}
provide proven solutions for recurring problems
illustrate a principle, not a code snippet
improve high-level communication
improve high-level reasoning
are language-agnostic (!)
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;
}
}
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/
// 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 );
Domain object with a unique identity
Not tied to a particular persistence mechanism
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()
);
}
}
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();
I'm Alain Schlesser.
Follow me on Twitter:
@schlesseraOr visit my Personal Blog:
www.alainschlesser.com