Developing a plugin using DI and TDD 11

Taking some user input and the dilemma of class modification or extension.

REST calls are a go

I’ve taken care of the calls to the REST API infrastructure in digressing post.
That’s kinda out of the scope for the moment but still interesting: leveraging the REST API infrastructure should replace the use of calls to the WordPress AJAX endpoint while allowing for a sweeter interaction with the backend and I could not refrain myself from using it.

Modify or extend?

The idlikethis_Shortcodes_Simple class is the one in charge of rendering the shortcode on the front-end.
In particular the render method does it relying on a rendering engine and a context provider to “assemble” the data it needs

/**
 * Returns the shortcode rendered markup code.
 *
 * @param string|array $attributes An array of shortcode attributes.
 * @param string $content The shortcode content.
 *
 * @return string
 */
public function render($attributes = array(), $content = '')
{
    $data = $this->template_data;
    $data['comment_text'] = $this->context->get_comment_text();
    $data['post_id'] = $this->context->get_post_id();

    return $this->render_engine->render($this->template_slug, $data);
}

The comment text is being passed from the context and from that class being defaulted to “I’d like this”.
On the backend this will make impossible understanding which button, among the possibly many a post or page body contains, was clicked.
Furthermore a user writing [idlikethis]my good idea[/idlikethis] will see his/her text ignored in the output.
That comment text should be read from the user input and more specifically from the $content argument.
So, why not simply modify the render method to something like this?

/**
 * Returns the shortcode rendered markup code.
 *
 * @param string|array $attributes An array of shortcode attributes.
 * @param string $content The shortcode content.
 *
 * @return string
 */
public function render($attributes = array(), $content = '')
{
    $data = $this->template_data;
    $content = empty($content) ? $this->context->get_comment_text() : $content;
    $data['comment_text'] = esc_attr($content);
    $data['post_id'] = $this->context->get_post_id();

    return $this->render_engine->render($this->template_slug, $data);
}

Dependency injection works

The benefit of using a DI container is that switching from one interface implementation to another is a bliss.
In this specific case the idlikethis_Shortcodes_Simple class is a concrete implementation of the idlikethis_Shortcodes_ShortcodeInterface interface and I can simply change the implementation to interface binding in the Shortcodes service provider from this

$this->container->bind('idlikethis_Shortcodes_ShortcodeInterface', 'idlikethis_Shortcodes_Simple');

to this

$this->container->bind('idlikethis_Shortcodes_ShortcodeInterface', 'idlikethis_Shortcodes_UserContentShortcode');

and extend the Simple class after a little refactoring of the render method code

public function render($attributes = array(), $content = '')
{
    $data = $this->template_data;
    $data['comment_text'] = $this->get_comment_text($attributes, $content);
    $data['post_id'] = $this->context->get_post_id();

    return $this->render_engine->render($this->template_slug, $data);
}

protected function get_comment_text($attributes = array(), $content = '') {
    return $this->context->get_comment_text();
}

Running the tests will confirm nothing has broken.

Testing the new implementation

I will add a test case for the new class

wpcept generate:wpunit wpunit "idlikethis\Shortcodes\UserContentShortcode"

and have it specify the extending class expected behaviour

<?php
namespace idlikethis\Shortcodes;

use idlikethis_Shortcodes_UserContentShortcode as UserContentShortcode;
use Prophecy\Argument;

class UserContentShortcodeTest extends \Codeception\TestCase\WPTestCase
{
    /**
     *  idlikethis_Templates_RenderEngineInterface
     */
    protected $render_engine;

    /**
     * @var \idlikethis_Texts_ProviderInterface
     */
    protected $text_provider;

    /**
     * @var \idlikethis_Contexts_ShortcodeContextInterface
     */
    protected $context;

    public function setUp()
    {
        // before
        parent::setUp();

        // your set up methods here
        $this->render_engine = $this->prophesize('idlikethis_Templates_RenderEngineInterface');
        $this->text_provider = $this->prophesize('idlikethis_Texts_ProviderInterface');
        $this->context = $this->prophesize('idlikethis_Contexts_ShortcodeContextInterface');
    }

    public function tearDown()
    {
        // your tear down methods here

        // then
        parent::tearDown();
    }

    /**
     * @test
     * it should be instantiatable
     */
    public function it_should_be_instantiatable()
    {
        $sut = $this->make_instance();

        $this->assertInstanceOf('idlikethis_Shortcodes_UserContentShortcode', $sut);
    }

    /**
     * @test
     * it should return the context default text if the content is not provided
     */
    public function it_should_return_the_context_default_text_if_the_content_is_not_provided()
    {
        $sut = $this->make_instance();
        $this->context->get_post_id()->willReturn(23);
        $this->context->get_comment_text()->willReturn('default text');

        $this->render_engine->render(Argument::any(), Argument::withEntry('comment_text', 'default text'))->shouldBeCalled();

        $out = $sut->render([]);
    }

    /**
     * @test
     * it should return the default context text if the content is empty string
     */
    public function it_should_return_the_default_context_text_if_the_content_is_not_a_string()
    {
        $sut = $this->make_instance();
        $content = '';
        $this->context->get_post_id()->willReturn(23);
        $this->context->get_comment_text()->willReturn('default text');

        $this->render_engine->render(Argument::any(), Argument::withEntry('comment_text', 'default text'))->shouldBeCalled();

        $out = $sut->render([], $content);
    }

    /**
     * @test
     * it should return the escaped content if provided
     */
    public function it_should_return_the_escaped_content_if_provided()
    {
        $sut = $this->make_instance();
        $this->context->get_post_id()->willReturn(23);
        $this->context->get_comment_text()->willReturn('default text');
        $content = 'some&content$$<>';

        $this->render_engine->render(Argument::any(), Argument::withEntry('comment_text', esc_attr($content)))->shouldBeCalled();

        $out = $sut->render([], $content);
    }

    protected function make_instance()
    {
        return new UserContentShortcode($this->render_engine->reveal(), $this->text_provider->reveal(), $this->context->reveal());
    }

}

Hot swap

A quick codecept run confirms the class is still working as expected.
Now that a shortcode handling class is in place and capable of using the user provided shortcode content it’s time to use that to render the shortcode in the Shortcodes provider

class idlikethis_ServiceProviders_Shortcodes extends tad_DI52_ServiceProvider
{

    /**
     * Binds and sets up implementations.
     */
    public function register()
    {
        $this->container->singleton('idlikethis_Plugin', new idlikethis_Plugin());
        $templates_dir = $this->container->resolve('idlikethis_Plugin')->dir_path('templates');

        $this->container->singleton('idlikethis_Texts_ProviderInterface', 'idlikethis_Texts_Provider');

        $smarty = new Smarty();
        $smarty->setTemplateDir($templates_dir);
        $smarty->setCacheDir($templates_dir . DIRECTORY_SEPARATOR . '_cache');
        $this->container->singleton('Smarty', $smarty);

        $this->container->singleton('idlikethis_Templates_RenderEngineInterface', 'idlikethis_Adapters_SmartyAdapter');
        $this->container->bind('idlikethis_Shortcodes_ShortcodeInterface', 'idlikethis_Shortcodes_UserContentShortcode');
        $this->container->bind('idlikethis_Contexts_ShortcodeContextInterface', 'idlikethis_Contexts_ShortcodeContext');

        $simple_shortcode = $this->container->resolve('idlikethis_Shortcodes_ShortcodeInterface');

        add_shortcode($simple_shortcode->get_tag(), array($simple_shortcode, 'render'));
    }

    /**
     * Binds and sets up implementations at boot time.
     */
    public function boot()
    {
        // TODO: Implement boot() method.
    }
}

On GitHub

Code current to this iteration is on GitHub.

Next

The button clicks, the shortcode will accept user input: time to user the collected information to allow a post editor to know who’d like what.

I appreciate your input