Main issue: WordPress minimum PHP version
Stuck at 5.2+ for ages
Current Plan to improve the situation
April 2019 => PHP 5.6+
December 2019 =gt; PHP 7+
Plugin can now state minimum version
Requires PHP:
header information
WordPress will incentivize updates
Nag screen in admin dashboard
Plugin installations/updates blocked
Old PHP versions and outdated tutorials
Hard to find up-to-date reference material
Simple mechanisms fail at higher complexity/scale
Hard to grow from single-file plugins to a proper architecture
Avoid starting from scratch
Get a head start and just dive in
Learning by doing
Fiddling with the code lets you engage with the applied best practices
Over-engineered for very simple plugins
If all you need is a filter, don't bother
PHP 7.2+ might be too aggressive
Especially for public plugins, you might want to adapt as needed
Keep with OOP only for full benefits
If you mix & match with procedural code too much, you cannot fully rely on the contracts & conventions put in place
Use PSR-4, a standard recommendation for class names to file names
Prefer Composer generated autoloaders over home-grown solutions
Composer is the de-facto standard and provides mature checks & optimizations out of the box
Use bundled Autoloader for direct deploys
With a bundled autoloader, you can just deploy the source code as-is without requiring a composer install
step *
Unexpected input is one of the most common source of bugs
Relying on always receiving the data you expect is not a strategy
Weak typing and automatic type coercion obfuscate these bugs
Bad data can travel through many places in the code before it triggers an actual error... if it does at all
Strict typing lets you detect data integrity issues earlier
Turns hard-to-debug runtime errors into logical errors that require you to fix them immediately
Value Objects let you extend the typing system
Enforce precise business logic for conceptual types (i.e. an Email
instead of a string
)
Exceptions are a transport mechanism
They route information from the place where an error condition occurs to the earliest place that can handle this error condition
Prefer Exceptions over WP_Error
WP_Error
needs to be routed through normal execution, so you need to add special handling for it at every step in-between, even if they are totally unrelated and cannot actually handle the error
Exceptions can encapsulate all logic and message handling through named constructors
Throwing the exception is a single, descriptive method call
throw FailedToLoadView::for_view_file( $file );
Single bootstrap file
The only file with side-effects on inclusion, to limit attack surface
Classes regrouped in src/
folder
Everything in that folder is handled through autoloading
Use recommendations in pds/skeleton
<?php
/**
* Plugin Name: My Basic Plugin
*/
namespace MyCompany\MyPlugin;
if ( ! defined( 'ABSPATH' ) ) {
die();
}
$autoloader = __DIR__ . '/vendor/autoload.php';
if ( is_readable( $autoloader ) ) {
require $autoloader;
}
PluginFactory::create()
->register();
Plugin header information for WordPress
Root namespace
Safeguard to not execute outside of WordPress
Register the autoloader with the PHP process
Retrieve a (shared) instance of the plugin
Register the plugin with the WordPress lifecycle
Bootstrap
[ Activate plugin ]
Load plugin
Initialize plugin
Run regular execution of plugin
Clean up after plugin
[ Deactivate plugin ]
Shutdown
Modular
Adapt granularity and count to your plugin's scope
Unified
Simple and elegant way of assembling the entire system
Flexible
Services can be provided by other plugins/third-party packages
Future-proof
Extract services out into external packages as needed
Semantic
Marker interfaces give meaning that the logic can rely on
The 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_service_classes()
get_service_classes()
can easily fetch its list from an external config file or from a filter
final class Plugin {
private function get_service_classes(): array {
// We can just treat the services as a
// collection of classes or objects.
return \apply_filters( self::SERVICES_FILTER, [
// Add services as FQCNs here.
ViewFactory::class,
SampleService::class,
] );
}
public function register_services() {
// By unifying the services, we can deal with
// them in bulk.
foreach ( $this->get_service_classes() as $class ) {
$service = $this->instantiate_service( $class );
if ( $service instanceof Registerable ) {
$service->register();
}
$this->services[] = $service;
}
}
}
The concept of passing in dependencies from the outside code, instead of fetching them from within the code that needs them.
interface Database {
public function get( string $key ): string;
}
final class MySQLDatabase {
public function get( string $key ): string { /* [...] */ };
}
final class SomeService {
public function do_something( string $key ) {
$database = new MySQLDatabase();
$data = $database->get( 'lyrics.hello_dolly' );
// Do something with the data.
}
}
interface Database {
public function get( string $key ): string;
}
final class MySQLDatabase {
public function get( string $key ): string { /* [...] */ };
}
final class SomeService {
/** @var MySQLDatabase */
private $database;
public function __construct( MySQLDatabase $database ) {
$this->database = $database;
}
public function do_something( string $key ) {
$data = $this->database->get( 'lyrics.hello_dolly' );
// Do something with the data.
}
}
interface Database {
public function get( string $key ): string;
}
final class MySQLDatabase implements Database {
public function get( string $key ): string { /* [...] */ };
}
final class SomeService {
/** @var Database */
private $database;
public function __construct( Database $database ) {
$this->database = $database;
}
public function do_something( string $key ) {
$data = $this->database->get( 'lyrics.hello_dolly' );
// Do something with the data.
}
}
Technical decisions, like which implementation to use, are "deferred"
Code that consumes a given interface does not need to decide which implementation of that interface to use
Deferred decisions make for very flexible code that can easily be adapted when requirements change, because all of the consuming code was never directly tied to a specific implementation anyway
Dependency Injection moves instantiation from inside a class to its surrounding code, so one level up...
...recursively, so the instantiations move up in the object tree all the way to the top
The topmost point of the object tree, or the tree's root, is called the "Composition Root"
An ideal code base will instantiate the entire object tree in one go from within the Composition Root
Separate presentation from logic
Improves flexibility and maintainability
Provide "theme-ability" out-of-the-box
Just copy the view files to your theme and adapt as needed
Default to escaping
Outputting an unsafe raw value requires an extra effort
Several means of scaffolding a plugin from the boilerplate reference *
create-project mwpd/plugin <folder>
scaffold mwpd-plugin <slug>
https://modernwp.dev/plugin-boilerplate/
Customization to shape the plugin according to your requirements
* Still a work-in-progress at the time of this talk. The exact syntax might change.
Simple base mechanism
Add a class marked as Service
, include its FQCN in the plugin's get_service_classes()
method.
/**
* Get the list of services to register.
*
* The services array contains a map of <identifier> => <service class name>
* associations.
*
* @return array<string> Associative array of identifiers mapped to fully
* qualified class names.
*/
protected function get_service_classes(): array {
return [
self::SAMPLE_BACKEND_SERVICE_ID => SampleSubsystem\SampleBackendService::class,
self::SAMPLE_LOOP_SERVICE_ID => SampleSubsystem\SampleLoopService::class,
// Add your service definitions here.
];
}
final class SalaryCalculator implements Service {
/**
* Calculate the salary of a given person.
*
* @var string $name Name of person to calculate salary for.
* @return int Calculated salary.
*/
public function calculate( string $name ): int {
return PHP_INT_MAX;
}
}
final class SalarySheet implements Service {
/** @var SalaryCalculator */
private $salary_calculator;
public function __construct( SalaryCalculator $salary_calculator ) {
$this->salary_calculator = $salary_calculator;
}
public function get_salary_sheet( array $employees ): array {
foreach ( $employees as $employee ) {
$salary = $this->salary_calculator->calculate( $employee );
// [...]
}
}
}
final class SalarySheetShortcode implements Service, Registerable, Conditional, Renderable {
/** @var SalarySheet */
private $salary_sheet;
/** @var ViewFactory */
private $view_factory;
public static function is_needed(): bool {
return ! is_admin() && ! wp_is_doing_ajax();
}
public function __construct( SalarySheet $salary_sheet, ViewFactory $view_factory ) {
$this->salary_sheet = $salary_sheet;
$this->view_factory = $view_factory;
}
public function register(): void {
add_shortcode( 'salary_sheet', [ $this, 'render' ] );
}
public function render( array $context = [] ): string {
return $this->view_factory->create( 'views/test-service' )
->render( [ 'plugin' => 'MWPD Boilerplate' ] );
}
}
Website will be up soon *
Be notified of updates (no spam *)
For the curious folks who can't wait
(Still WIP, no tooling or docs yet)
* No, really.
I'm Alain Schlesser.
Follow me on Twitter:
@schlesseraOr visit my Personal Blog:
www.alainschlesser.com