Developing a plugin using DI and TDD 07

Starting to handle a click request.

Filling the button click handler

I’ve put in place a trivial code to handle the AJAX request a click on the “I’d like this” button will trigger.
The code is registered in the Endpoints service provider here

<?php

class idlikethis_ServiceProviders_Endpoints extends tad_DI52_ServiceProvider
{

    /**
     * Binds and sets up implementations.
     */
    public function register()
    {
        $this->container->set_var('endpoints-namespace', 'idlikethis/v1');

        $this->container->singleton('idlikethis_Endpoints_ButtonClickHandlerInterface', 'idlikethis_Endpoints_ButtonClickHandler');

        add_action('rest_api_init', array($this, 'register_endpoints'));
    }

    public function register_endpoints()
    {
        $namespace = $this->container->get_var('endpoints-namespace');

        register_rest_route($namespace, '/button-click/', array(
            'methods' => 'POST',
            'callback' => array($this->container->make('idlikethis_Endpoints_ButtonClickHandlerInterface'), 'handle'),
        ));
    }

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

and the concrete ButtonClickHandler class should take care of handling the request and appending a comment to the target post.
What I will need to create a unique and meaningful comment is:

  • a target post ID
  • a comment content

All the rest can be filled up. What’s this handler class responsibility? Handle the comment insertion request. What’s supposed to happen while this request is handled?

  • There should be some verification about the request legitimacy
  • There should be a check of the request content
  • A comment should be inserted in the database associated to the post
  • An exit response should be returned to the user

This implementation is supposed to change in future iterations; this is important to understand the following development and not just tag it as “overkill” and “over-engineering”.
A click-per-day counter could be put in place that will make its job before the last step above; a web socket based real-time communication might give a connected author an instant feedback on the post appreciation; an email notification system might send an email when the count has reached a certain number and so on.
Would all of this make sense in this class? No, it would be a blatant violation of the single responsibility principle.
To put words into code I’ve scaffolded a test for the class

wpcept generate:wprest wpunit "idlikethis\Endpoints\ButtonClickHandler"

and written the following test case

<?php
namespace idlikethis\Endpoints;

use idlikethis_Endpoints_ButtonClickHandler as ButtonClickHandler;
use Prophecy\Argument;

class ButtonClickHandlerTest extends \Codeception\TestCase\WPRestApiTestCase
{

    /**
     * @var \idlikethis_Endpoints_AuthHandlerInterface
     */
    protected $auth_handler;

    /**
     * @var \idlikethis_Repositories_CommentsRepositoryInterface
     */
    protected $comment_repository;


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

        // your set up methods here
        $this->auth_handler = $this->prophesize('idlikethis_Endpoints_AuthHandlerInterface');
        $this->comment_repository = $this->prophesize('idlikethis_Repositories_CommentsRepositoryInterface');
    }

    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();
    }

    /**
     * @test
     * it should return 403 response if verification fails
     */
    public function it_should_return_403_response_if_verification_fails()
    {
        $this->auth_handler->verify_auth(Argument::any(), Argument::any())->willReturn(false);

        $sut = $this->make_instance();

        $out = $sut->handle();

        $this->assertErrorResponse(403, $out);
    }

    /**
     * @test
     * it should return 400 response if request is missing post id
     */
    public function it_should_return_400_response_if_request_is_missing_post_id()
    {
        $this->auth_handler->verify_auth(Argument::any(), Argument::any())->willReturn(true);
        $_POST['content'] = 'foo';

        $sut = $this->make_instance();

        $out = $sut->handle();

        $this->assertErrorResponse(400, $out);
    }

    /**
     * @test
     * it should return 400 status if request is missing content
     */
    public function it_should_return_400_status_if_request_is_missing_content()
    {
        $this->auth_handler->verify_auth(Argument::any(), Argument::any())->willReturn(true);
        $_POST['post_id'] = 123;

        $sut = $this->make_instance();

        $out = $sut->handle();

        $this->assertErrorResponse(400, $out);
    }

    /**
     * @test
     * it should return 400 response if comment insertion fails
     */
    public function it_should_return_400_response_if_comment_insertion_fails()
    {
        $this->auth_handler->verify_auth(Argument::any(), Argument::any())->willReturn(true);
        $this->comment_repository->add_for_post(Argument::any(), Argument::any())->willReturn(false);
        $_POST['post_id'] = 123;
        $_POST['content'] = 'foo';

        $sut = $this->make_instance();

        $out = $sut->handle();

        $this->assertErrorResponse(400, $out);
    }

    /**
     * @test
     * it should return 200 response if comment insertion succeeds
     */
    public function it_should_return_200_response_if_comment_insertion_succeeds()
    {
        $this->auth_handler->verify_auth(Argument::any(), Argument::any())->willReturn(true);
        $this->comment_repository->add_for_post(Argument::any(), Argument::any())->willReturn(112);
        $_POST['post_id'] = 123;
        $_POST['content'] = 'foo';

        $sut = $this->make_instance();

        /** @var \WP_REST_Response $out */
        $out = $sut->handle();

        $this->assertEquals(200, $out->status);
        $this->assertEquals(112, $out->data);
    }

    protected function make_instance()
    {
        return new ButtonClickHandler($this->auth_handler->reveal(), $this->comment_repository->reveal());
    }

}

And the class code fell in line

<?php

class idlikethis_Endpoints_ButtonClickHandler implements idlikethis_Endpoints_ButtonClickHandlerInterface
{
    /**
     * @var idlikethis_Endpoints_AuthHandlerInterface
     */
    protected $auth_handler;

    /**
     * @var idlikethis_Repositories_CommentsRepositoryInterface
     */
    protected $comments_repository;

    public function __construct(idlikethis_Endpoints_AuthHandlerInterface $auth_handler, idlikethis_Repositories_CommentsRepositoryInterface $comments_repository)
    {
        $this->auth_handler = $auth_handler;
        $this->comments_repository = $comments_repository;
    }

    /**
     * Handles a button click request.
     *
     * @return bool `true` if the request was successfully handled, `false` otherwise.
     */
    public function handle()
    {
        $headers = array();

        if (!$this->auth_handler->verify_auth($_POST, 'button-click')) {
            return new WP_REST_Response('Invalid auth', 403, $headers);
        }

        if (empty($_POST['post_id']) || empty($_POST['content'])) {
            return new WP_REST_Response('Missing data', 400, $headers);
        }
        $post_id = $_POST['post_id'];
        $content = $_POST['content'];

        $exit = $this->comments_repository->add_for_post($post_id, $content);

        $message = empty($exit) ? 'Could not register click' : $exit;
        $status = empty($exit) ? 400 : 200;

        return new WP_REST_Response($exit, $status, $headers);
    }
}

Outsourcing responsibilities

Of note here is that I’ve “outsourced” the authorization verification and the comment insertion tasks to distinct auxiliary classes.
This should decouple the class responsibility from the implementation. The Endpoints service provider changed accordingly

<?php

class idlikethis_ServiceProviders_Endpoints extends tad_DI52_ServiceProvider
{

    /**
     * Binds and sets up implementations.
     */
    public function register()
    {
        $this->container->set_var('endpoints-namespace', 'idlikethis/v1');

        $this->container->singleton('idlikethis_Endpoints_AuthHandlerInterface', 'idlikethis_Endpoints_AuthHandler');
        $this->container->singleton('idlikethis_Repositories_CommentsRepositoryInterface', 'idlikethis_Repositories_CommentsRepository');
        $this->container->singleton('idlikethis_Endpoints_ButtonClickHandlerInterface', 'idlikethis_Endpoints_ButtonClickHandler');


        add_action('rest_api_init', array($this, 'register_endpoints'));
    }

    public function register_endpoints()
    {
        $namespace = $this->container->get_var('endpoints-namespace');

        register_rest_route($namespace, '/button-click/', array(
            'methods' => 'POST',
            'callback' => array($this->container->make('idlikethis_Endpoints_ButtonClickHandlerInterface'), 'handle'),
        ));
    }

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

The code current to this post is on GitHub tagged 0.0.2.

Next

I will develop the two auxiliary classes and add some integration testing for the chain responsibility.