Etsy sync WordPress plugin 08

Stepping forward in the Etsy WordPress plugin development.

Sync Steps

The core of the Etsy Little Helper WordPress plugin will be its ability to sync content one-way with Etsy; I might extend its functionalities to go both ways since Etsy API allows for that but will keep aim low in this initial process.
The synchronization will happen in steps:

  • the plugin will get hold of a user shops
  • for each shop the plugin will fetch the listings

the content mirroring in WordPress terms will be, first thoughts about it, to have a custom type for the shops and one for the listings.
But before any of that can happen I have to make sure the sync steps will properly request their piece of content; with the request classes in place, user shops and shop listings, and the request compiler in place I only need some class to make requests and consume the data.
To keep class specialization to a minimum the first step in a synchronization process will be getting a user shops.

Interface segregation

The class taking care of the first step is the ELH_ShopRetriever one and it’s just stubbed out to the point where no logic is involved

class ELH_ShopRetriever extends ELH_AbstractSyncStep implements ELH_ApiRequestClientInterface {

    /**
     * @var ELH_ApiRequestInterface
     */
    protected $request;

    /**
     * @var ELH_RequestCompilerInterface
     */
    protected $request_compiler;

    public static function instance( ELH_KeychainInterface $keychain, ELH_ApiInterface $api, ELH_ApiRequestInterface $request, ELH_RequestCompilerInterface $request_compiler ) {
        $instance                   = new self();
        $instance->keychain         = $keychain;
        $instance->api              = $api;
        $instance->request          = $request;
        $instance->request_compiler = $request_compiler;

        return $instance;
    }

    public function set_request( ELH_ApiRequestInterface $request ) {
        $this->request = $request;
    }

    public function set_request_compiler( ELH_RequestCompilerInterface $request_compiler ) {
        $this->request_compiler = $request_compiler;
    }

    public function run() {
        // TODO: Implement run() method.
    }
}

And the core method, common to any class implementing the ELH_StepInterface interface, run is still a no op.

What’s to do?

In plain english the class should cover the following steps:

  • check for its requirements
  • compile the request for shops
  • send the request for shops
  • raise an exception if the request goes somewhat wrong OR call the next step (a class) in the chain and let it handle the shop data

The test code below covers the above specs (I’ve cut it to fit, see full code here)

 use tad\FunctionMocker\FunctionMocker as Test;

class ELH_ShopRetrieverTest extends \PHPUnit_Framework_TestCase {

    protected function setUp() {
        Test::setUp();
        Test::replace( 'get_option' );
        Test::replace( 'wp_remote_get' );
    }

    protected function tearDown() {
        Test::tearDown();
    }

    /**
     * @test
     * it should raise an exception if keychain is not set on run
     */
    public function it_should_raise_an_exception_if_keychain_is_not_set_on_run() {
        $this->setExpectedException( 'ELH_MissingStepRequirement' );

        $sut = new ELH_ShopRetriever();

        $sut->set_api( Test::replace( 'ELH_ApiInterface' )->get() );
        $sut->set_request( Test::replace( 'ELH_ApiRequestInterface' )->get() );
        $sut->set_request_compiler( Test::replace( 'ELH_RequestCompilerInterface' )->get() );

        $sut->run();
    }

    /**
     * @test
     * it should raise an exception if api is not set on run
     */
    public function it_should_raise_an_exception_if_api_is_not_set_on_run() {
        $this->setExpectedException( 'ELH_MissingStepRequirement' );

        $sut = new ELH_ShopRetriever();

        $sut->set_keychain( test::replace( 'ELH_KeychainInterface' )->get() );
        $sut->set_request( Test::replace( 'ELH_ApiRequestInterface' )->get() );
        $sut->set_request_compiler( Test::replace( 'ELH_RequestCompilerInterface' )->get() );

        $sut->run();
    }

    // more tests...

    /**
     * @test
     * it should raise an exception if the result is not a success
     */
    public function it_should_raise_an_exception_if_the_result_is_not_a_success() {
        $this->setExpectedException( 'ELH_SyncException' );

        $sut = new ELH_ShopRetriever();

        $fake_api_key = 'foo';
        $api          = Test::replace( 'ELH_ApiInterface' )->method( 'get_api_key', $fake_api_key )->get();
        $sut->set_api( $api );
        $sut->set_keychain( test::replace( 'ELH_KeychainInterface' )->get() );
        $request = Test::replace( 'ELH_ApiRequestInterface' )->get();
        $sut->set_request( $request );
        $request_compiler = Test::replace( 'ELH_RequestCompilerInterface' )->method( 'set_request' )
                                ->method( 'get_compiled_request', '/users/someuser/shops?api_key=foo' )->get();
        $sut->set_request_compiler( $request_compiler );
        $get_option    = Test::replace( 'get_option', 'someuser' );
        $response      = [
            'response' => [
                'code'    => 404,
                'message' => 'The supplied uri doesn\'t map to a valid command'
            ]
        ];
        $wp_remote_get = Test::replace( 'wp_remote_get', $response );

        $sut->run();
    }

    /**
     * @test
     * it should call next step and pass it the data if success
     */
    public function it_should_call_next_step_and_pass_it_the_data_if_success() {
        $sut = new ELH_ShopRetriever();

        $fake_api_key = 'foo';
        $api          = Test::replace( 'ELH_ApiInterface' )->method( 'get_api_key', $fake_api_key )->get();
        $sut->set_api( $api );
        $sut->set_keychain( test::replace( 'ELH_KeychainInterface' )->get() );
        $request = Test::replace( 'ELH_ApiRequestInterface' )->get();
        $sut->set_request( $request );
        $request_compiler = Test::replace( 'ELH_RequestCompilerInterface' )->method( 'set_request' )
                                ->method( 'get_compiled_request', '/users/someuser/shops?api_key=foo' )->get();
        $status           = Test::replace( 'ELH_Status' )->method( 'set' )->get();
        $sut->set_status( $status );
        $sut->set_request_compiler( $request_compiler );
        $next = Test::replace( 'ELH_AbstractSyncStep' )->method( 'set_status' )->method( 'run' )->get();
        $sut->set_next( $next );

        $get_option    = Test::replace( 'get_option', 'someuser' );
        $response      = [
            'response' => [
                'code'    => 200,
                'message' => 'Ok'
            ]
        ];
        $wp_remote_get = Test::replace( 'wp_remote_get', $response );

        $sut->run();

        $status->wasCalledWithOnce( [ 'data', $response ], 'set' );
        $next->wasCalledWithOnce( [ $status ], 'set_status' );
        $next->wasCalledOnce( 'run' );
    }

}

And writing that yielded the actual incarnation of the ELH_ShopRetriever class

class ELH_ShopRetriever extends ELH_AbstractSyncStep implements ELH_ApiRequestClientInterface {

    /**
     * @var ELH_ApiRequestInterface
     */
    protected $request;

    /**
     * @var ELH_RequestCompilerInterface
     */
    protected $request_compiler;

    public static function instance( ELH_KeychainInterface $keychain, ELH_ApiInterface $api, ELH_ApiRequestInterface $request, ELH_RequestCompilerInterface $request_compiler ) {
        $instance                   = new self();
        $instance->keychain         = $keychain;
        $instance->api              = $api;
        $instance->request          = $request;
        $instance->request_compiler = $request_compiler;

        return $instance;
    }

    public function set_request( ELH_ApiRequestInterface $request ) {
        $this->request = $request;
    }

    public function set_request_compiler( ELH_RequestCompilerInterface $request_compiler ) {
        $this->request_compiler = $request_compiler;
    }

    public function run() {
        $this->ensure_requirements();

        $url  = $this->get_request_url();

        $data = $this->get_response( $url );

        $this->maybe_call_next( $data );
    }

    protected function ensure_requirements() {
        if ( ! isset( $this->keychain ) ) {
            throw new ELH_MissingStepRequirement( 'Keychain parameter is not set' );
        }
        if ( ! isset( $this->api ) ) {
            throw new ELH_MissingStepRequirement( 'Api parameter is not set' );
        }
        if ( ! isset( $this->request ) ) {
            throw new ELH_MissingStepRequirement( 'Request parameter is not set' );
        }
        if ( ! isset( $this->request_compiler ) ) {
            throw new ELH_MissingStepRequirement( 'Request compiler parameter is not set' );
        }
    }

    /**
     * @param $data
     *
     * @return mixed
     * @throws ELH_SyncException
     */
    protected function ensure_response( $data ) {
        if ( empty( $data ) ) {
            throw new ELH_SyncException( 'Server returned empty data.' );
        }
        if ( ! isset( $data['response'] ) && ! isset( $data['response']['code'] ) ) {
            throw new ELH_SyncException( 'Server returned non coherent data' );
        }
        if ( $data['response']['code'] != '200' ) {
            $message = sprintf( 'Shop retrieving failed with code %d and message "%s"', $data['response']['code'], $data['response']['message'] );
            throw new ELH_SyncException( $message );
        }

        return $data;
    }

    /**
     * @return array
     */
    protected function get_request_url() {
        $data = array(
            'user_id' => get_option( ELH_Main::USER_ID_OPTION ),
            'api_key' => $this->api->get_api_key()
        );

        $this->request_compiler->set_request( $this->request );
        $uri = $this->request_compiler->get_compiled_request( $data );

        return ELH_Main::API_BASE . $uri;
    }

    /**
     * @param $url
     *
     * @return array|mixed|WP_Error
     * @throws ELH_SyncException
     */
    protected function get_response( $url ) {
        $data = wp_remote_get( $url );

        $data = $this->ensure_response( $data );

        return $data;
    }

    /**
     * @param $data
     */
    protected function maybe_call_next( $data ) {
        if ( isset( $this->next ) && isset( $this->status ) ) {
            $this->status->set( 'data', $data );
            $this->next->set_status( $this->status );
            $this->next->run();
        }
    }
}

Next

Before moving in data processing I will test the other class that’s making requests and see what refactoring can be done to keep code duplication, and testing effort, to a minimum.