HiveBrain v1.2.0
Get Started
← Back to all entries
patternphpMinor

Structure of API wrapper

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
wrapperapistructure

Problem

I'm building an API wrapper for a bookkeeping SOAP API.

I have some questions regarding bast practice for structure of the wrapper and for error handling.

For now i've structured it like this:

EconClient.php

class EconClient {

    public function __construct() 
    {
        $wsdlUrl = 'https://api.e-conomic.com/secure/api1/EconomicWebservice.asmx?WSDL';
        $this->client = new SoapClient($wsdlUrl, array("trace" => 1, "exceptions" => 1));                    
    }

    public function connectWithToken($token, $appToken)
    {
        $this->client->ConnectWithToken(array('token' => $token, 'appToken' => $appToken));
    }

    public function debtor()
    {   
        $this->debtor = new Debtor($this->client);
        return $this->debtor;
    }

    public function debtorGroup()
    {
        $this->debtorGroup = new DebtorGroup($this->client);
        return $this->debtorGroup;
    }
}


Debtor.php

class Debtor {

    public function __construct($client)
    {
        $this->client = $client;
    }

    public function get($param, $value)
    {
        return $this->client->Debtor_GetData(array('entityHandle' => $this->getHandle($param, $value)))->Debtor_GetDataResult;
    }

    public function getHandle($param, $value)
    {
        switch ($param) {
            case 'number':
                return $this->client->Debtor_FindByNumber(array('number' => $value))->Debtor_FindByNumberResult;
                break;
            case 'email':
                return $this->client->Debtor_FindByEmail(array('number' => $value))->Debtor_FindByEmailResult;
    }
}


Is there any way i could structure this so i would'nt have to pass in the $client into the resource classes?

The only response i can get from the SOAP client except from data in case of success is exceptions. If i'm trying to fetch an ID that doesn't exist i will get an exception too. Should i try and catch these exceptions in for example Debtor.php? Or how could i structure error handl

Solution

First off:


The only response i can get from the SOAP client except from data in case of success is exceptions.

Of course, if your call wasn't successful, there's something wrong, and an exception should be thrown. If you query for an ID that doesn't exist, don't catch the exception. That means the user has provided your client with invalid data, and its that user who must deal with the problem. Not You.

An API doesn't really deal with that many exceptions. In fact, I'm one of those people who think that a well written, bug free API-wrapper doesn't need exception handling. If the wrapper/client's code is properly put together, exceptions are the result of either bad usage or exceptions being returned by the webservice. Both types of errors should be dealt with by the user.

If you don't want to pass the client to all those classes, the answer is simple: inheritance:

class BaseClient
{
    protected $client = null;//declare your properties!!!
    protected $config = null;
    //use type hints, default = null means you don't HAVE to pass the argument
    public function __construct(\SoapClient $client = null)
    {
        $this->client = $client;
    }
    //lazy-loading getter
    protected function getClient()
    {
        if ($this->client === null)
        {//set client only when it's required
            $this->setClient(
                new \SoapClient($this->config['wsdl'], $config['options']
            );
        }
        return $this->client;
    }
    //public to allow injection
    protected function setClient(\SoapClient $client)
    {
        $this->client = $client;
        return $this;//makes your api chainable
    }
}


Then define all the classes you'll actually be using along these lines:

class DebptorClient extends BaseClient
{
    protected $config = array(//optional
        'wsdl' => 'the specific wsdl',
        'options'=> array()//defaults
    );

    public function get($param, $value)
    {
        $client = $this->getClient();//loads if not yet loaded
        return $client->Debtor_GetData(
            array(
                'entityHandle' => $this->getHandle($param, $value)
            )
        )->Debtor_GetDataResult;
    }
}


And so on.

Now, some actual code-review:

Declare your properties

From the wrapper tag wiki:


A wrapper is an OOP technique where an object encapsulates (wraps) another object, hiding/protecting the object and controlling all access to it.

By not declaring $this->client, client is effectively added later on in your objects life, which means property lookups will be slower, but more importantly: $this->client will be a public property. If $this->client is public, then you don't have a wrapper because:


A wrapper [...] encapsulates another object, hiding/protecting the object and controlling all access to it.

Thus, $this->client has to be protected or private.

Next.

Seeing this code worries me:

public function getHandle($param, $value)
{
    switch ($param) {
        case 'number':
            return $this->client->Debtor_FindByNumber(array('number' => $value))->Debtor_FindByNumberResult;
            break;//unreachable statement, btw
        case 'email':
            return $this->client->Debtor_FindByEmail(array('number' => $value))->Debtor_FindByEmailResult;
}


You're expecting the people who are to use your api to know that $param and $value are expected to be. $param has only 2 valid options: number or email. anything else is invalid, yet at no point to you bother to validate the data you're being passed.

When developing an API wrapper, it's not a bad idea to create Argument models:

namespace Soap\Data;
class Argument
{
    private $type = null;
    private $value = null;
    public function __construct($type, $value)
    {
        $setter = 'set'.ucfirst(trim($type));//email becomes setEmail
        if (!method_exists($setter, $this))
        {
            throw new \InvalidArgumentException($type.' is not a valid type for '.__CLASS__);
        }
        $this->{$setter}($value);
    }
    public function setEmail($email)
    {
        if (!filter_var($email, \FILTER_VALIDATE_EMAIL))
        {
            throw new \InvalidArgumentException($email.' is not a valid email address');
        }
        $this->value = $email;
        return $this;
    }
}


This allows you to define methods like this:

public function getByType(Argument $value)


and the Argument class ensures that the data you'll receive is validated properly.

Now, error handling.

That very much depends on how you see your code being used. Is it supposed to be a sort of "module" to be included into various projects, in which case, I'd just throw my exceptions out for the caller to deal with them. Code that is used as a dependency shouldn't have to anticipate all sorts of errors that might occur, that's the users job.

If you want this code to work sort of in the background, You could write your own exceptio

Code Snippets

class BaseClient
{
    protected $client = null;//declare your properties!!!
    protected $config = null;
    //use type hints, default = null means you don't HAVE to pass the argument
    public function __construct(\SoapClient $client = null)
    {
        $this->client = $client;
    }
    //lazy-loading getter
    protected function getClient()
    {
        if ($this->client === null)
        {//set client only when it's required
            $this->setClient(
                new \SoapClient($this->config['wsdl'], $config['options']
            );
        }
        return $this->client;
    }
    //public to allow injection
    protected function setClient(\SoapClient $client)
    {
        $this->client = $client;
        return $this;//makes your api chainable
    }
}
class DebptorClient extends BaseClient
{
    protected $config = array(//optional
        'wsdl' => 'the specific wsdl',
        'options'=> array()//defaults
    );

    public function get($param, $value)
    {
        $client = $this->getClient();//loads if not yet loaded
        return $client->Debtor_GetData(
            array(
                'entityHandle' => $this->getHandle($param, $value)
            )
        )->Debtor_GetDataResult;
    }
}
public function getHandle($param, $value)
{
    switch ($param) {
        case 'number':
            return $this->client->Debtor_FindByNumber(array('number' => $value))->Debtor_FindByNumberResult;
            break;//unreachable statement, btw
        case 'email':
            return $this->client->Debtor_FindByEmail(array('number' => $value))->Debtor_FindByEmailResult;
}
namespace Soap\Data;
class Argument
{
    private $type = null;
    private $value = null;
    public function __construct($type, $value)
    {
        $setter = 'set'.ucfirst(trim($type));//email becomes setEmail
        if (!method_exists($setter, $this))
        {
            throw new \InvalidArgumentException($type.' is not a valid type for '.__CLASS__);
        }
        $this->{$setter}($value);
    }
    public function setEmail($email)
    {
        if (!filter_var($email, \FILTER_VALIDATE_EMAIL))
        {
            throw new \InvalidArgumentException($email.' is not a valid email address');
        }
        $this->value = $email;
        return $this;
    }
}
public function getByType(Argument $value)

Context

StackExchange Code Review Q#37731, answer score: 4

Revisions (0)

No revisions yet.