implements Hello_Dolly

Image Credit: Ky on Flickr • https://flic.kr/p/8x4bVQ

Jonathan Brinley

Modern Tribe

"Hello, Dolly!"








Image Credit: Classic Film on Flickr • https://flic.kr/p/KD7WWE

"Hello, Dolly!"

Hello, Dolly! in the WordPress plugin admin page

hello.php


/**
 * @package Hello_Dolly
 * @version 1.6
 */
/*
Plugin Name: Hello Dolly
Plugin URI: http://wordpress.org/plugins/hello-dolly/
Description: This is not just a plugin, it symbolizes the hope and enthusiasm of an entire generation summed up in two words sung most famously by Louis Armstrong: Hello, Dolly. When activated you will randomly see a lyric from Hello, Dolly in the upper right of your admin screen on every page.
Author: Matt Mullenweg
Version: 1.6
Author URI: http://ma.tt/
*/

function hello_dolly_get_lyric() {
	/** These are the lyrics to Hello Dolly */
	$lyrics = "Hello, Dolly
Well, hello, Dolly
It's so nice to have you back where you belong
You're lookin' swell, Dolly
I can tell, Dolly
You're still glowin', you're still crowin'
You're still goin' strong
We feel the room swayin'
While the band's playin'
One of your old favourite songs from way back when
So, take her wrap, fellas
Find her an empty lap, fellas
Dolly'll never go away again
Hello, Dolly
Well, hello, Dolly
It's so nice to have you back where you belong
You're lookin' swell, Dolly
I can tell, Dolly
You're still glowin', you're still crowin'
You're still goin' strong
We feel the room swayin'
While the band's playin'
One of your old favourite songs from way back when
Golly, gee, fellas
Find her a vacant knee, fellas
Dolly'll never go away
Dolly'll never go away
Dolly'll never go away again";

	// Here we split it into lines
	$lyrics = explode( "\n", $lyrics );

	// And then randomly choose a line
	return wptexturize( $lyrics[ mt_rand( 0, count( $lyrics ) - 1 ) ] );
}

// This just echoes the chosen line, we'll position it later
function hello_dolly() {
	$chosen = hello_dolly_get_lyric();
	echo "

$chosen

"; } // Now we set that function up to execute when the admin_notices action is called add_action( 'admin_notices', 'hello_dolly' ); // We need some CSS to position the paragraph function dolly_css() { // This makes sure that the positioning is also good for right-to-left languages $x = is_rtl() ? 'left' : 'right'; echo " "; } add_action( 'admin_head', 'dolly_css' );

Refactor!

github.com/flightless/implements-hello-dolly
Image Credit: Classic Film on Flickr • https://flic.kr/p/LxazKW

implements-hello-dolly.php


/*
Plugin Name: implements Hello_Dolly
Description: We start with everyone’s favorite WordPress plugin, “Hello, Dolly”. With a dramatic wave of our hands, we speak the magical word: “Refactor!” Let’s see how far we can go.
Author: Jonathan Brinley
Version: 2.0
Contributors: Matt Mullenweg
*/

namespace Hello_Dolly;

// Start the plugin
add_action( 'plugins_loaded', function () {
	require_once __DIR__ . '/vendor/autoload.php';
	Hello_Dolly_Plugin::init();
	do_action( 'hello_dolly/init' );
}, 1, 0 );
			

Composer

getcomposer.org

  • Autoloading
  • Libraries

Composer

composer init


{
    "name": "flightless/implements-hello-dolly",
    "description": "We start with everyone’s favorite WordPress plugin, “Hello, Dolly”. With a dramatic wave of our hands, we speak the magical word: “Refactor!” Let’s see how far we can go.",
    "type": "wordpress-plugin",
    "license": "GPL-2.0",
    "authors": [
        {
            "name": "Jonathan Brinley",
            "email": "jonathan@tri.be"
        }
    ]
}
			

composer.json


{
    "name": "flightless/implements-hello-dolly",
    "description": "We start with everyone’s favorite WordPress plugin, “Hello, Dolly”. With a dramatic wave of our hands, we speak the magical word: “Refactor!” Let’s see how far we can go.",
    "type": "wordpress-plugin",
    "license": "GPL-2.0",
    "authors": [
        {
            "name": "Jonathan Brinley",
            "email": "jonathan@tri.be"
        }
    ],
    "autoload": {
        "psr-4": {
            "Hello_Dolly\\": "src/"
        }
    }
}
			

Codeception

composer require --dev lucatume/wp-browser

Codeception


{
    "name": "flightless/implements-hello-dolly",
    /* ... */
    "autoload": {
        "psr-4": {
            "Hello_Dolly\\": "src/"
        }
    },
    "require-dev": {
        "lucatume/wp-browser": "^1.16"
    }
}
			

composer install --no-dev

WPLoader

vendor/bin/wpcept bootstrap

https://github.com/lucatume/wp-browser

tests/integration/Hello_Dolly/ Hello_Dolly_Plugin_Test.php


namespace Hello_Dolly;
use Codeception\TestCase\WPTestCase;

class Hello_Dolly_Plugin_Test extends WPTestCase {
	public function test_get_instance() {
		$instance = Hello_Dolly_Plugin::instance();
		$this->assertInstanceOf( 'Hello_Dolly\\Hello_Dolly_Plugin', $instance );
	}
}
			

implements-hello-dolly.php


namespace Hello_Dolly;

// Start the plugin
add_action( 'plugins_loaded', function () {
	require_once __DIR__ . '/vendor/autoload.php';
	Hello_Dolly_Plugin::init();
	do_action( 'hello_dolly/init' );
}, 1, 0 );
			

src/Hello_Dolly_Plugin.php



namespace Hello_Dolly;

/**
 * Initializes the Hello, Dolly plugin
 */
class Hello_Dolly_Plugin {
	/** @var static */
	private static $instance;

	/**
	 * Initialize the plugin
	 */
	public static function init() {
		self::instance();
	}

	/**
	 * Get the global instance of the class
	 * @return static
	 */
	public static function instance() {
		if ( empty( static::$instance ) ) {
			static::$instance = new static();
		}
		return static::$instance;
	}
}
			

First Five Principles

  • Single responsibility principle
  • Open/closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

Single responsibility principle

  • Turn a string of lyrics into an array
  • Get a random line from the lyrics array
  • Texturize a line of lyrics
  • Wrap lyrics in a paragraph tag
  • Build language-appropriate CSS rules
  • Print the lyrics to the browser
  • Print the style rules to the browser
  • Hook into WordPress

Lyrics\Lyric_Collection

Responsibility

Fetch a single line from an array of lyrics

Why would it change?

If the algorithm to fetch a line changed

tests/integration/Hello_Dolly/ Lyrics/Lyric_Collection_Test.php


namespace Hello_Dolly\Lyrics;
use Codeception\TestCase\WPTestCase;

class Lyric_Collection_Test extends WPTestCase {
	public function test_get_lyric() {
		$lyrics = [];
		for ( $i = 0 ; $i < 10 ; $i++ ) {
			$lyrics[] = rand_str( mt_rand( 0, 32 ) ) . ' ' . rand_str( mt_rand( 0, 32 ) ) . ' ' . rand_str( mt_rand( 0, 32 ) );
		}

		$collection = new Lyric_Collection( $lyrics );
		for ( $i = 0 ; $i < 20 ; $i++ ) {
			$this->assertContains( $collection->get_lyric(), $lyrics );
		}
	}
}
			

Lyrics\Lyric_Collection


namespace Hello_Dolly\Lyrics;

class Lyric_Collection implements Lyric_Collection_Interface {
	/** @var string[] */
	protected $lyrics;

	/** @var int */
	protected $count = 0;

	/**
	 * Lyric_Collection constructor.
	 *
	 * @param array $lyrics An array of single-line strings
	 */
	public function __construct( array $lyrics ) {
		$this->lyrics = array_values( $lyrics );
		$this->count = count( $this->lyrics );
	}

	public function get_lyric() {
		return $this->get_random_lyric();
	}

	/**
	 * Get a random item from the list of lyrics
	 *
	 * @return string
	 */
	private function get_random_lyric() {
		return $this->lyrics[ $this->get_random_key() ];
	}

	/**
	 * Get a random integer within the range of keys
	 * for the list of lyrics
	 *
	 * @return int
	 */
	private function get_random_key() {
		return mt_rand( 0, $this->count - 1 );
	}
}
			

Lyrics\Lyric_Collection_Interface


namespace Hello_Dolly\Lyrics;

/**
 * Interface Lyric_Collection_Interface
 *
 * Implementors will return a single line of lyrics
 */
interface Lyric_Collection_Interface {
	/**
	 * @return string A single line of lyrics
	 */
	public function get_lyric();
}
			

Lyrics\Lyric_Collection_Factory

Responsibility

Transform a string of lyrics into a Lyric_Collection

Why would it change?

If the return value should be a different class

tests/integration/Hello_Dolly/ Lyrics/Lyric_Collection_Factory_Test.php


namespace Hello_Dolly\Lyrics;
use Codeception\TestCase\WPTestCase;

class Lyric_Collection_Factory_Test extends WPTestCase {
	public function test_create_collection() {
		$lyrics = [];
		for ( $i = 0 ; $i < 10 ; $i++ ) {
			$lyrics[] = rand_str( mt_rand( 0, 32 ) ) . ' ' . rand_str( mt_rand( 0, 32 ) ) . ' ' . rand_str( mt_rand( 0, 32 ) );
		}
		$string = implode( "\n", $lyrics );
		$factory = new Lyric_Collection_Factory();
		$collection = $factory->create_collection( $string );

		$this->assertInstanceOf( 'Hello_Dolly\\Lyrics\\Lyric_Collection', $collection );
	}
}
			

Lyrics\Lyric_Collection_Factory


namespace Hello_Dolly\Lyrics;

/**
 * Class Lyric_Collection_Factory
 *
 * Creates a Lyric_Collection instance from
 * a string of lyrics.
 */
class Lyric_Collection_Factory implements Lyric_Collection_Factory_Interface {
	/**
	 * @param string $lyrics
	 * @return Lyric_Collection
	 */
	public function create_collection( $lyrics ) {
		$lyrics = explode( "\n", $lyrics );
		return new Lyric_Collection( $lyrics );
	}
}
			

Lyrics\Lyric_Collection_Factory_Interface


namespace Hello_Dolly\Lyrics;

/**
 * Interface Lyric_Collection_Factory_Interface
 *
 * Creates a Lyric_Collection_Interface from a
 * multi-line string of lyrics.
 */
interface Lyric_Collection_Factory_Interface {
	/**
	 * @param string $lyrics A multi-line string of lyrics
	 * @return Lyric_Collection_Interface
	 */
	public function create_collection( $lyrics );
}
			

Formatting

Responsibility

Apply wptexturize() to a string

Wrap a string in a paragraph tag with an ID

Why would it change?

The formatting algorithm changes

The HTML element changes

Decorator

The Decorator design pattern

Formatting\String_Formatter_Interface


namespace Hello_Dolly\Formatting;

/**
 * Interface String_Formatter_Interface
 *
 * Implementors will format and return a string.
 */
interface String_Formatter_Interface {
	/**
	 * @param string $string The string to be formatted
	 * @return string The formatted string
	 */
	public function format( $string );
}
			

Formatting\String_Formatter


namespace Hello_Dolly\Formatting;
use Codeception\TestCase\WPTestCase;

class String_Formatter_Test extends WPTestCase {
	public function test_passthrough() {
		$string = rand_str();
		$passthrough = new String_Formatter();

		$this->assertEquals( $string, $passthrough->format( $string ) );
	}
}
			

Formatting\String_Formatter


namespace Hello_Dolly\Formatting;

/**
 * Class String_Formatter
 *
 * A passthrough formatter that returns the given string
 */
class String_Formatter implements String_Formatter_Interface {
	public function format( $string ) {
		return $string;
	}
}
			

Formatting\String_Formatter_Decorator


namespace Hello_Dolly\Formatting;

/**
 * Class String_Formatter_Decorator
 *
 * Base class for string decorators.
 */
abstract class String_Formatter_Decorator implements String_Formatter_Interface {
	/** @var String_Formatter_Interface */
	protected $formatter;
	public function __construct( String_Formatter_Interface $formatter ) {
		$this->formatter = $formatter;
	}

	public function format( $string ) {
		$string = $this->formatter->format( $string );
		return $this->decorate( $string );
	}

	abstract protected function decorate( $string );
}
			

Formatting\WP_Texturize_Formatter


namespace Hello_Dolly\Formatting;
use Codeception\TestCase\WPTestCase;

class WP_Texturize_Formatter_Test extends WPTestCase {
	public function test_texturization() {
		$string = "You're lookin' swell, Dolly";
		$texturized_string = wptexturize( $string );
		$this->assertNotEquals( $string, $texturized_string );

		$formatter = new WP_Texturize_Formatter( new String_Formatter() );
		$this->assertEquals( $texturized_string, $formatter->format( $string ) );
	}
}
			

Formatting\WP_Texturize_Formatter


namespace Hello_Dolly\Formatting;

/**
 * Class WP_Texturize_Formatter
 *
 * Formats a string with wptexturize()
 * @see wptexturize()
 */
class WP_Texturize_Formatter extends String_Formatter_Decorator {
	protected function decorate( $string ) {
		return wptexturize( $string );
	}
}
			

Formatting\Paragraph_Tag


namespace Hello_Dolly\Formatting;
use Codeception\TestCase\WPTestCase;

class Paragraph_Tag_Test extends WPTestCase {
	public function test_paragraph_tag() {
		$string = rand_str();
		$id = rand_str();

		$formatter = new Paragraph_Tag( new String_Formatter(), $id );

		$expected = sprintf( '

%s

', $id, $string ); $this->assertEquals( $expected, $formatter->format( $string ) ); } public function test_escaped_id() { $string = rand_str(); $id = rand_str(8) . ' ' . rand_str(8); $escaped_id = sanitize_html_class( $id ); $this->assertNotEquals( $id, $escaped_id ); $formatter = new Paragraph_Tag( new String_Formatter(), $id ); $expected = sprintf( '

%s

', $escaped_id, $string ); $this->assertEquals( $expected, $formatter->format( $string ) ); } }

Formatting\Paragraph_Tag


namespace Hello_Dolly\Formatting;

/**
 * Class Paragraph_Tag
 *
 * Wraps a string in a paragraph tag with an ID
 */
class Paragraph_Tag extends String_Formatter_Decorator {
	/** @var string */
	protected $html_id;

	/**
	 * Paragraph_Tag constructor.
	 *
	 * @param String_Formatter_Interface $formatter
	 * @param                            $html_id
	 */
	public function __construct( String_Formatter_Interface $formatter, $html_id ) {
		parent::__construct( $formatter );
		$this->html_id = $html_id;
	}

	protected function decorate( $string ) {
		return sprintf( '

%s

', sanitize_html_class( $this->html_id), $string ); } }

$formatter = new Paragraph_Tag(
                 new WP_Texturize_Formatter(
                     new String_Formatter()
                 ),
                 'dolly'
             );
return $formatter->format( "You're lookin' swell, Dolly" );
			

<p id="dolly">You’re lookin’ swell, Dolly</p>

Styles\Style_Rules_Interface


namespace Hello_Dolly\Styles;

/**
 * Interface Style_Rules_Interface
 *
 * Generates a list of CSS properties
 */
interface Style_Rules_Interface {

	/**
	 * @return string
	 */
	public function get_styles();
}
			

Styles\Directional_Style_Rules


namespace Hello_Dolly\Styles;

/**
 * Class Directional_Style_Rules
 *
 * Generates styles for the given text direction
 */
class Directional_Style_Rules implements Style_Rules_Interface {
	protected $side;

	/**
	 * Directional_Style_Rules constructor.
	 *
	 * @param string $side The side text floats to. 'left' or 'right'
	 */
	public function __construct( $side ) {
		if ( empty( $side ) ) {
			throw new \InvalidArgumentException( __( '$this->side must be set to a non-empty value' ) );
		}
		$this->side = $side;
	}

	public function get_styles() {
		return "
			float: {$this->side};
			padding-{$this->side}: 15px;
			padding-top: 5px;
			margin: 0;
			font-size: 11px;
		";
	}
}
			

Styles\Style_Printer_Interface


namespace Hello_Dolly\Styles;

/**
 * Interface Style_Printer_Interface
 *
 * Prints CSS styles
 */
interface Style_Printer_Interface {
	/**
	 * @param Style_Rules_Interface $styles
	 * @return void
	 */
	public function print_styles( Style_Rules_Interface $styles );
}
			

Styles\Style_Printer


namespace Hello_Dolly\Styles;

/**
 * Class Style_Printer
 *
 * Prints the given style rules for the element with the given ID
 */
class Style_Printer implements Style_Printer_Interface {
	protected $html_id;

	/**
	 * Style_Printer constructor.
	 *
	 * @param string $html_id The ID of the targeted DOM element
	 */
	public function __construct( $html_id ) {
		$this->html_id = $html_id;
	}

	/**
	 * @param Style_Rules_Interface $styles
	 * @return void
	 */
	public function print_styles( Style_Rules_Interface $styles ) {
		$rules = $styles->get_styles();
		if ( ! empty( $rules ) ) {
			printf ("
		
		", $this->html_id, $rules );
		}
	}
}
			

src/Hello_Dolly_Plugin.php


private function hooks() {
	add_action( 'admin_head', [ $this, 'print_admin_css' ] );
	add_action( 'admin_notices', [ $this, 'print_admin_notice' ] );
}

public function print_admin_css() {
	$style_rules = $this->get_style_rules();
	$printer = $this->get_style_printer();
	$printer->print_styles( $style_rules );
}

public function print_admin_notice() {
	$lyrics = $this->get_lyric_collection();
	$printer = $this->get_lyric_printer();
	$printer->render( $lyrics->get_lyric() );
}
			

src/Hello_Dolly_Plugin.php


/**
 * @return Style_Rules_Interface
 */
private function get_style_rules() {
	if ( is_rtl() ) {
		$style_rules = $this->container[ 'style_rules.rtl' ];
	} else {
		$style_rules = $this->container[ 'style_rules.ltr' ];
	}
	return apply_filters( 'hello_dolly/style_rules', $style_rules );
}

/**
 * @return Style_Printer_Interface
 */
private function get_style_printer() {
	$printer = $this->container[ 'style_printer' ];
	return apply_filters( 'hello_dolly/style_printer', $printer );
}

/**
 * @return Lyric_Collection_Interface
 */
private function get_lyric_collection() {
	$collection = $this->container[ 'lyrics.collection' ];
	return apply_filters( 'hello_dolly/lyric_collection', $collection );
}

/**
 * @return Printer_Interface
 */
private function get_lyric_printer() {
	$printer = $this->container[ 'lyrics.printer' ];
	return apply_filters( 'hello_dolly/printer', $printer );
}
			

src/Hello_Dolly_Plugin.php


/**
 * Initialize the plugin
 *
 * @return void
 */
public static function init( Container $container ) {
	$instance = self::instance();
	$instance->container = $container;
	$instance->hooks();
}
			

Dependency Injection Container

  • Aura.Di
  • PHP-DI
  • Pimple
  • Symphony\DependencyInjection
  • Zend\Di

Pimple

composer require pimple/pimple

implements-hello-dolly.php


namespace Hello_Dolly;
use Pimple\Container;

// Start the plugin
add_action( 'plugins_loaded', function () {
	require_once __DIR__ . '/vendor/autoload.php';
	$container = new Container();
	$container->register( new Service_Provider() );
	Hello_Dolly_Plugin::init( $container );
	do_action( 'hello_dolly/init', Hello_Dolly_Plugin::instance(), $container );
}, 1, 0 );
			

src/Service_Provider.php


namespace Hello_Dolly;


use Hello_Dolly\Formatting\Paragraph_Tag;
use Hello_Dolly\Formatting\String_Formatter;
use Hello_Dolly\Formatting\WP_Texturize_Formatter;
use Hello_Dolly\Lyrics\Lyric_Collection_Factory;
use Hello_Dolly\Printing\Formatted_Printer;
use Hello_Dolly\Styles\Directional_Style_Rules;
use Hello_Dolly\Styles\Style_Printer;
use Pimple\Container;
use Pimple\ServiceProviderInterface;

class Service_Provider implements ServiceProviderInterface {
	public function register( Container $container ) {
		$container[ 'html_id' ] = 'dolly';

		$container[ 'lyrics' ] = "Hello, Dolly
Well, hello, Dolly
It's so nice to have you back where you belong
You're lookin' swell, Dolly
I can tell, Dolly
You're still glowin', you're still crowin'
You're still goin' strong
We feel the room swayin'
While the band's playin'
One of your old favourite songs from way back when
So, take her wrap, fellas
Find her an empty lap, fellas
Dolly'll never go away again
Hello, Dolly
Well, hello, Dolly
It's so nice to have you back where you belong
You're lookin' swell, Dolly
I can tell, Dolly
You're still glowin', you're still crowin'
You're still goin' strong
We feel the room swayin'
While the band's playin'
One of your old favourite songs from way back when
Golly, gee, fellas
Find her a vacant knee, fellas
Dolly'll never go away
Dolly'll never go away
Dolly'll never go away again";

		$container[ 'style_rules.ltr' ] = function( Container $container ) {
			return new Directional_Style_Rules( 'right' );
		};
		$container[ 'style_rules.rtl' ] = function( Container $container ) {
			return new Directional_Style_Rules( 'left' );
		};

		$container[ 'style_printer' ] = function( Container $container ) {
			return new Style_Printer( $container[ 'html_id' ] );
		};

		$container[ 'lyrics.formatter' ] = function( Container $container ) {
			$formatter = new String_Formatter();
			$formatter = new WP_Texturize_Formatter( $formatter );
			$formatter = new Paragraph_Tag( $formatter, $container[ 'html_id' ] );
			return $formatter;
		};

		$container[ 'lyrics.printer' ] = function( Container $container ) {
			return new Formatted_Printer( $container[ 'lyrics.formatter' ] );
		};

		$container[ 'lyrics.collection' ] = function( Container $container ) {
			$factory = $container[ 'lyrics.collection.factory' ];
			$lyrics = $container[ 'lyrics' ];
			return $factory->create_collection( $lyrics );
		};

		$container[ 'lyrics.collection.factory' ] = function( Container $container ) {
			return new Lyric_Collection_Factory();
		};

	}
}
			

implements-hello-dolly.php


namespace Hello_Dolly;
use Pimple\Container;

// Start the plugin
add_action( 'plugins_loaded', function () {
	require_once __DIR__ . '/vendor/autoload.php';
	$container = new Container();
	$container->register( new Service_Provider() );
	Hello_Dolly_Plugin::init( $container );
	do_action( 'hello_dolly/init', Hello_Dolly_Plugin::instance(), $container );
}, 1, 0 );
			

Resources

implements Hello_Dolly
github.com/flightless/implements-hello-dolly
The Principles of OOD / Robert C. Martin
butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Inversion of Control Containers and the Dependency Injection pattern / Martin Fowler
martinfowler.com/articles/injection.html
WPBrowser / Luca Tumedei
github.com/lucatume/wp-browser
theaveragedev.com/tag/wp-browser/
Image Credit: New York Sunday News

Jonathan Brinley

@jbrinley • jonathan@tri.be

Modern Tribe