5
\$\begingroup\$

I am learning OOP. I feel I have a grasp of basic dependency injection in that as a rule of thumb, you pass an object to a method rather than the parameters to create an object. As such, never create an object within a method, but use a factory. Right?

The code below is for connecting to a REST API. However, I feel am I overcomplicating it. The $url which is used to access the API has a number of steps in its creation before it can be used. This is what is throwing me.

I think the best approach is to create a factory RequestFactory, call it statically from within getRecord() and pass the $type (GET, POST etc) $url, $username, and $password to return the results of either a ServiceGetRequest or a ServicePostRequest.

In my code, I have stopped just before this part however and just instantiated new objects. I don't know if the single factory is the right direction, or that I should have two factories.

My thinking is with two factories one factory would create an object RequestDetails (containing the $username, $password and $url) to pass to the other RequestFactory rather than a bunch of parameters as with only one factory. I assume this would adhere to basic dependency injection by passing an object to a method? The down side to creating an object just to pass to a method is that the Request Details object then shares properties of the ServiceDetails object - $username, $password...

My final thought is that I could instead change ServiceDetails to include a $url property which is empty on instantiation, then when the $url has been created, rather than create new objects using factories, simply modify the existing ServiceDetails $url property since this is the main object which is being passed around. I thought this could make sense to reduce needless code duplication as two objects would otherwise share the same $username and $password. Or would this violate SoC somehow? Or would this violate dependency injection since that property would be set only after a certain method is run?

A final question, is this implementation considered RESTful? From what I have gathered, many sources seem to imply that the API itself is either RESTful or not, rather than the code used to access it. Is this true? I have tried to aim for a RESTful connection(?) by using the HTTP verbs in the cURL context - ServiceGetRequest, ServicePostRequest

class ServiceDetails {
    public $username;
    public $password;
    public $api_endpoint;

    public function __construct( $username, $password, $api_endpoint ) {
        $this->username     = $username;
        $this->password     = $password;
        $this->api_endpoint = $api_endpoint;
    }
}

class API {

    public $service_details;

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

}


class SpecificAPI extends API {

    public function getRecord( $record_id ) {
        $service_details = $this->service_details;
        $endpoint        = $service_details->api_endpoint;
        $username        = $service_details->username;
        $password        = $service_details->password;

        $url = "{$endpoint}/Function?RecordID={$record_id}";

        // create obj to pass - even needed?
        // should be a factory?
        $request = new RequestDetails( $url, $username, $password );

        //should be another factory?
        $return = new ServiceGetRequest( $request );

        return $return->result;
    }
}


class RequestDetails {
    public $url;
    public $username;
    public $password;
    public $content;

    public function __construct( $url, $username, $password, $content = '' ) {
        $this->url      = $url;
        $this->username = $username;
        $this->password = $password;
        $this->content  = $content;
    }
}


class ServiceRequest {

    public $result = [ ];

    public function __construct( RequestDetails $service_details ) {
        $this->executeRequest( $service_details );
    }

    // Stub for extends
    public function initialiseOptions( RequestDetails $service_details ) {
        $options = array();

        return $options;
    }

    public function executeRequest( RequestDetails $service_details ) {
        $options = $this->initialiseOptions( $service_details );
        $url     = $service_details->url;

        $ch = curl_init( $url );
        curl_setopt_array( $ch, $options );
        $result = curl_exec( $ch );
        curl_close( $ch );

        $result       = json_decode( $result, true );
        $this->result = $result;
    }
}

class ServicePostRequest extends ServiceRequest {

    public function initialiseOptions( RequestDetails $service_details ) {
        $username = $service_details->username;
        $password = $service_details->password;
        $json     = json_encode( $service_details->content );

        $options = array(
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_USERPWD        => "{$username}:{$password}",
            CURLOPT_HTTPHEADER     => array( "Content-type: application/json" ),
            CURLOPT_POSTFIELDS     => $json
        );

        return $options;
    }
}

class ServiceGetRequest extends ServiceRequest {

    public function initialiseOptions( RequestDetails $service_details ) {
        $username = $service_details->username;
        $password = $service_details->password;

        $options = array(
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_USERPWD        => "{$username}:{$password}",
            CURLOPT_HTTPHEADER     => array( "Accept: application/json" ),
            CURLOPT_SSL_VERIFYPEER => false
        );

        return $options;
    }
}


$details = new ServiceDetails(
    "username",
    "password",
    "http://api.service.name.com"
);

$service = new SpecificAPI( $details );
$record  = $service->getRecord( '007' );

print_r( $record );
\$\endgroup\$
4
\$\begingroup\$

Never ever turn off SSL certificate verification:

CURLOPT_SSL_VERIFYPEER => false

means you're throwing away SSL protections, you might as well just not use SSL.

--

As such, never create an object within a method, but use a factory. Right?

Dependency injection and factory method are useful patterns to solve a certain problem, but you should not overuse them when the problem doesn't apply. Use the SOLID principle to guide your design, and apply these patterns to implement dependency inversion principle. In your code, the dependant class SpecificAPI actually knows the implementation of *Details concrete classes, thus it had failed to invert the dependency between *API to *Details.

I suggest that, rather than having ServiceDetails class, you should split it to class that does BasicAuth($username, $password). The reason we separate in a BasicAuth class is that so that the Auth classes encapsulate the details of the how the API authenticates to the service. You can swap it with other auth class, for example OAuth2, DigestAuth, MyCustomAPIAuth, etc and your SpecificAPI class would not need to change if you decided you want to change the authentication mechanism.

This can be done like so:

$auth = ...;

// convert unauthenticated request to authenticated request
$request = $auth->apply($request);
// alternatively, modify the request in-place
// $auth->apply($request);

--

From what I have gathered, many sources seem to imply that the API itself is either RESTful or not, rather than the code used to access it. Is this true?

Yes, ultimately REST is a characteristic of the API. But the code should also take advantage of the REST API to benefit from REST.

A REST API should be Resource-centric, not Service-centric. I don't see any Resources in your code.

\$\endgroup\$
  • \$\begingroup\$ Thank you. I looked at the bigger picture and started reading a book on understanding OOP concepts, including SOLID. \$\endgroup\$ – myol Jul 13 '15 at 16:19
2
\$\begingroup\$

RESTFul APIs

I would like to extend Lie Ryan answer about API RESTFul. As he states, a REST API should be Resource-centric, that means, you retrieve certain resource. It's most about the URL way of retrieving elements. You handle CRUD actions using HTTP methods mapped as follows:

GET /tickets - Retrieves a list of tickets
GET /tickets/12 - Retrieves a specific ticket
POST /tickets - Creates a new ticket
PUT /tickets/12 - Updates ticket #12
PATCH /tickets/12 - Partially updates ticket #12
DELETE /tickets/12 - Deletes ticket #12

And your URL structure doesn't looks like that at all...

A useful bunch of advice building API's RESTful is in http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api, or in http://www.sitepoint.com/best-practices-rest-api-scratch-introduction/

You also have a nice tutorial and explanation to REST APIs in http://www.restapitutorial.com/

And as a bonus, a way to implement it using PHP Slim can be found on http://www.sitepoint.com/writing-a-restful-web-service-with-slim/

RESTFul API Architecture

About the API architecture, I think that you could use a main class to manage the kind of Operation to do (depending on the headers, state what kind of CRUD operation is, and send the request to the proper model to manage it) and authentication. Something like APIController, that will control all the access and will take care of input of data and way of output the data. This main class will pass the data to the right models with the right operations to retrieve the data from the database, and format the output.

Update: HTTP Client

For easy development, you may use Guzzle to test all the PHP connections: http://guzzle.readthedocs.org/en/latest/

\$\endgroup\$
  • \$\begingroup\$ Thank you. I have now read all these resources, but yet I am no closer in how to actually accomplish this in PHP with HTTP verbs... \$\endgroup\$ – myol Jul 13 '15 at 16:17
  • \$\begingroup\$ HTTP verbs are going to be the flags that shows your API what the call wants to do: You'll get the header, read it, and then perform an action on it. One important part to test it for development will be having a client to launch the calls: Try Guzzle, easy and quick: guzzle.readthedocs.org/en/latest \$\endgroup\$ – Chococroc Jul 14 '15 at 7:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.