Developing a plugin using DI and TDD 10

Almost ready to make a request.

Setting up

The plugin will currently register one endpoint and will expect calls on the /idlikethis/v1/button-click route to register a click on a button.
The PHP side of things is in place but some refinements remain for the front-end to effectively communicate; in a quick abstraction a click on an “I’d like this” button should:

  1. make a call to the right endpoint
  2. provide the required nonce number in the request
  3. specify the post ID the click refers to
  4. provide a comment content

Points 1 and 2 in the list are not specific to a button instance but common to all of them and hence will be part of an array of localized data.

Script localization

The way data should be passed from the PHP side of WordPress to the JavaScript side of it goes through the use of the wp_localize_script function.
The place where this function will be used is the class that’s now responsible for the front-end script queueing

<?php

class idlikethis_Scripts_FrontEndScriptsQ implements idlikethis_Scripts_FrontEndScriptsQInterface
{
    /**
     * @var idlikethis_Plugin
     */
    protected $plugin;

    /**
     * @var idlikethis_Scripts_FrontEndDataProviderInterface
     */
    protected $data_provider;

    /**
     * idlikethis_Scripts_FrontEndScriptsQ constructor.
     * @param idlikethis_Plugin $plugin
     * @param idlikethis_Scripts_FrontEndDataProviderInterface $data_provider
     */
    public function __construct(idlikethis_Plugin $plugin, idlikethis_Scripts_FrontEndDataProviderInterface $data_provider)
    {
        $this->plugin = $plugin;
        $this->data_provider = $data_provider;
    }

    /**
     * Enqueues the needed scripts and styles.
     */
    public function enqueue()
    {
        $bundle_url = $this->plugin->dir_url('assets/js/dist/idlikethis-bundle.js');
        wp_enqueue_script('idlikethis-bundle', $bundle_url, array('backbone'), null, true);
        $data = $this->data_provider->get_data();
        wp_localize_script('idlikethis-bundle', 'idlikethisData', $data);
    }
}

This class is an adapter relying on WordPress provided functions to do its job and while not at all complex now it got its own function-mocker powered tests for regression testing purposes.
After a quick Composer requirement

composer require --dev lucatume/function-mocker

I’ve scaffolded a new test case

wpcept generate:wpunit wpunit "idlikethis\Scripts\FrontEndScriptsQ"

and gone through the little set up function-mocker requires: in the main tests _bootstrap.php file I will call its initialization method

// file tests/_bootstrap.php

<?php
// This is global bootstrap for autoloading
use tad\FunctionMocker\FunctionMocker;

FunctionMocker::init();

This is to allow the Patchwork library to work its magic before all of WordPress code is loaded.
The test class itself has its only particularity being the use of function-mocker

<?php
namespace idlikethis\Scripts;

use tad\FunctionMocker\FunctionMocker as Test;
use idlikethis_Scripts_FrontEndScriptsQ as Q;

class FrontEndScriptsQTest extends \Codeception\TestCase\WPTestCase
{

    /**
     * @var \idlikethis_Plugin
     */
    protected $plugin;

    /**
     * @var \idlikethis_Scripts_FrontEndDataProviderInterface
     */
    protected $data_provider;

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

        // your set up methods here
        Test::setUp();
        $this->plugin = $this->prophesize('idlikethis_Plugin');
        $this->data_provider = $this->prophesize('idlikethis_Scripts_FrontEndDataProviderInterface');
    }

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

        // then
        parent::tearDown();
    }

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

    /**
     * @test
     * it should q the standard version of the script
     */
    public function it_should_q_the_standard_version_of_the_script()
    {
        $sut = $this->make_instance();

        $this->plugin->dir_url('assets/js/dist/idlikethis-bundle.js')->willReturn('foo.js');
        $wp_enqueue_script = Test::replace('wp_enqueue_script');

        $sut->enqueue();

        $wp_enqueue_script->wasCalledWithOnce(['idlikethis-bundle', 'foo.js', ['backbone'], null, true]);
    }

    /**
     * @test
     * it should localize the script data
     */
    public function it_should_localize_the_script_data()
    {
        $sut = $this->make_instance();

        $this->plugin->dir_url('assets/js/dist/idlikethis-bundle.js')->willReturn('foo.js');
        $data = ['some' => 'data'];
        $this->data_provider->get_data()->willReturn($data);
        $wp_localize_script = Test::replace('wp_localize_script');

        $sut->enqueue();

        $wp_localize_script->wasCalledWithOnce(['idlikethis-bundle', 'idlikethisData', $data]);

    }


    private function make_instance()
    {
        return new Q($this->plugin->reveal(), $this->data_provider->reveal());
    }

}

I’ve modified the class constructor to allow for a further dependency to be injected: this will be an instance of a concrete class implementing the idlikethis_Scripts_FrontEndDataProviderInterface interface.
The class itself is little more than a value object and as such I will not cover it with any test; there is a sanity limit to it.

<?php

class idlikethis_Scripts_FrontEndDataProvider implements idlikethis_Scripts_FrontEndDataProviderInterface
{

    /**
     * Returns an array containing the data to be localized.
     *
     * @return array
     */
    public function get_data()
    {
        return array(
            'endpoints' => array(
                'button-click' => array(
                    'url' => home_url('idlikethis/v1/button-click/'),
                    'nonce' => wp_create_nonce('button-click'),
                )
            )
        );
    }
}

This class will take care of providing the data that’s common to any button on the page.

Button particulars

Now that the common data is in place it’s time to take care of points 3 and 4 of the list above; this will have to happen in the class responsible for the rendering of the button markup: idlikethis_Shortcodes_Simple.
The class constructor is building the template data and that is, right now, merely a string of text.
The comment text and the post ID are missing and will have to be provided.
Those cannot be “pulled from the air” and will be injected as any other class dependency.
I will add a dependency to the class that models the “context”: looking ahead the class is supposed to render the shortcode markup code in the loop but might be used elsewhere (think of a widget) and using a injectable context will decouple it from its “surroundings”.

/**
 * @param idlikethis_Templates_RenderEngineInterface $render_engine
 * @param idlikethis_Texts_ProviderInterface $text_provider
 * @param idlikethis_Contexts_ShortcodeContextInterface $context
 */
public function __construct(idlikethis_Templates_RenderEngineInterface $render_engine, idlikethis_Texts_ProviderInterface $text_provider, idlikethis_Contexts_ShortcodeContextInterface $context)
{
    $this->render_engine = $render_engine;
    $this->template_slug = 'shortcodes/simple';
    $this->text_provider = $text_provider;
    $this->template_data = array(
        'text' => $this->text_provider->get_button_text(),
    );
    $this->context = $context;
}

I’ve modified the Shortcodes service provider to provide the class with a context

<?php

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 . '_cache');
        $this->container->singleton('Smarty', $smarty);

        $this->container->singleton('idlikethis_Templates_RenderEngineInterface', 'idlikethis_Adapters_SmartyAdapter');
        $this->container->bind('idlikethis_Shortcodes_ShortcodeInterface', 'idlikethis_Shortcodes_Simple');
        $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.
    }
}

The context class implementation itself is obvious but I will cover with tests in the next post and with its next iteration.

<?php

class idlikethis_Contexts_ShortcodeContext implements idlikethis_Contexts_ShortcodeContextInterface
{

    /**
     * @return string
     */
    public function get_comment_text()
    {
        return esc_attr(__("I'd like this", 'idlikehtis'));
    }

    /**
     * @return int
     */
    public function get_post_id()
    {
        return get_the_ID();
    }
}

This in place it’s just a matter of modifying the template code to use the new data.
The code current to the post is on GitHub.

Next

All should be in place to make and handle the first request on button click.