Developing a plugin using DI and TDD 04

Adapt and get the job done.

Smarty adapter

The latest post concluded with a shortcode class fully working according to the tests.
Sadly fully working meant having nothing printed to the page.
Unsurprisingly looking at the code below

<?php

class idlikethis_Shortcodes_Simple implements idlikethis_Shortcodes_ShortcodeInterface
{
    /**
     * @var string
     */
    protected $template_slug;

    /**
     * @var array
     */
    protected $template_data;

    /**
     * @var idlikethis_Templates_RenderEngineInterface
     */
    protected $render_engine;

    /**
     * @param idlikethis_Templates_RenderEngineInterface $render_engine
     */
    public function __construct(idlikethis_Templates_RenderEngineInterface $render_engine)
    {
        $this->render_engine = $render_engine;
        $this->template_slug = 'shortcodes/simple';
        $this->template_data = array(
            'text' => __("I'd like this", 'idlikethis'),
        );
    }

    /**
     * Returns the shortcode tag.
     *
     * @return string
     */
    public function get_tag()
    {
        return 'idlikethis';
    }

    /**
     * Returns the shortcode rendered markup code.
     *
     * @return string
     */
    public function render()
    {
        return $this->render_engine->render($this->template_slug, $this->template_data);
    }

    /**
     * @param $template_slug
     */
    public function set_template_slug($template_slug)
    {
        if (!is_string($template_slug) || empty($template_slug)) {
            throw new InvalidArgumentException('Template slug must be a non empty string');
        }
        $this->template_slug = $template_slug;
    }

    /**
     * @param array $data
     */
    public function set_template_data(array $data)
    {
        $this->template_data = $data;
    }
}

The burden of rendering falls on a concrete implementation of the idlikethis_Templates_RenderEngineInterface interface and the one that’s being provided to the class lacks any logic in the render method

<?php

class idlikethis_Adapters_SmartyAdapter implements idlikethis_Templates_RenderEngineInterface
{
    /**
     * Renders a template using the provided data.
     *
     * @param string $template_slug
     * @param array $data
     */
    public function render($template_slug, array $data = array()){
        // implement me
    }
}

So down to a test,

wpcept generate:wpunit wpunit "idlikethis\Adapters\SmartyAdapter"

some test code

<?php
namespace idlikethis\Adapters;

use idlikethis_Adapters_SmartyAdapter as SmartyAdapter;
use Prophecy\Argument;

class SmartyAdapterTest extends \Codeception\TestCase\WPTestCase
{
    /**
     * @var \Smarty
     */
    protected $smarty;

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

        // your set up methods here
        $this->smarty = $this->prophesize('Smarty');
    }

    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_Adapters_SmartyAdapter', $sut);
    }

    /**
     * @test
     * it should assign no template vars if template data is empty
     */
    public function it_should_assign_no_template_vars_if_template_data_is_empty()
    {
        $sut = $this->make_instance();

        $this->smarty->assign(Argument::any())->shouldNotBeCalled();
        $this->smarty->display(Argument::any())->willReturn('foo');

        $sut->render('some-template', []);
    }

    /**
     * @test
     * it should assign template vars
     */
    public function it_should_assign_template_vars()
    {
        $sut = $this->make_instance();

        $this->smarty->assign('key1', 'value1')->shouldBeCalled();
        $this->smarty->assign('key2', 'value2')->shouldBeCalled();
        $this->smarty->display(Argument::any())->willReturn('foo');

        $sut->render('some-template', ['key1' => 'value1', 'key2' => 'value2']);
    }

    /**
     * @test
     * it should call display method on Smarty
     */
    public function it_should_call_display_method_on_smarty()
    {
        $sut = $this->make_instance();

        $this->smarty->assign('key1', 'value1')->shouldBeCalled();
        $this->smarty->assign('key2', 'value2')->shouldBeCalled();
        $this->smarty->display('some-template.tpl')->willReturn('foo');

        $sut->render('some-template', ['key1' => 'value1', 'key2' => 'value2']);
    }


    private function make_instance()
    {
        $sut = new SmartyAdapter($this->smarty->reveal());
        return $sut;

    }
} 

and the class code produced by it

<?php

class idlikethis_Adapters_SmartyAdapter implements idlikethis_Templates_RenderEngineInterface
{
    /**
     * @var Smarty
     */
    protected $smarty;

    /**
     * idlikethis_Adapters_SmartyAdapter constructor.
     * @param Smarty $smarty
     */
    public function __construct(Smarty $smarty)
    {
        $this->smarty = $smarty;
    }

    /**
     * Renders a template using the provided data.
     *
     * @param string $template_slug
     * @param array $data
     */
    public function render($template_slug, array $data = array())
    {
        if (!empty($data)) {
            array_walk($data, array($this, 'assign_template_var'));
        }
        $template_slug = ends_with($template_slug, '.tpl') ? $template_slug : $template_slug . '.tpl';

        return $this->smarty->display($template_slug);
    }

    protected function assign_template_var($value, $key)
    {
        $this->smarty->assign($key, $value);
    }
}

Modifying providers, not implementations

The advantage of this approach is that, with a forethought about it, allows for quick changes to be put in place and echo through the dependency tree.
To allow for the idlikethis_Adapters_SmartyAdapter class to properly instantiate and construct the Shortcodes service provider changed accordingly.

<?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');

        $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');

        $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.
    }
}

It’s easy to see how, sticking to the single responsibility principle, this service provider is now violating its own: as the name, idlikthis_ServiceProviders_Shortcodes, suggests its responsibility should be to set up and provide the shortcode classes and tightly related code.
The lines in charge of setting up the main plugin class and the rendering engine are overreaching; I will stick with this solution for the moment and iterate on it.

The plugin class

The idlikethis_Plugin class is a utility class the responsibility of which is to contextualize the plugin in the current WordPress installation; so pointing to folders and paths and maybe wrapping some more utility methods as I see fit.
Since it has some measure of error prone logic into it it got its own test class as well

wpcept generate:wpunit wpunit "idlikethis\Plugin"

I’m not pasting the code for the sake of brevity.

Maybe some markup?

No, not yet. I’ve repeatedly told this would look like overkill and I know it does but that’s enough code for one article.