2012/04/04

Building a PHPSuite while napping (pt. 3)

In the last two posts we reviewed how to setup a RESTful web service in Netsuite and a general-purpose REST client in PHP.  In this entry we'll finally make some magic happen by creating two classes to tie this project up.



Our REST client from the last post, while having a little Netsuite customization magic, is not very specific either to Netsuite or our script from the first post in this series.  We now require an interface tailored specifically for our special suitescript.  Just as our script is designed to be fairly extensible, our interface needs to be fairly abstract so we can build-out functionality down the road.  Presenting, the uber-simple interface class:


<?php
require_once( 'NsRestRequest.php' );
define( 'NS_RECORD_RESTLET', 'https://rest.netsuite.com/app/site/hosting/restlet.nl?script=1000&deploy=1' );
abstract class NsRecord
{
    protected $http_method;
    protected $request_url;
    protected $request_method;
    protected $request_args;
    protected $request_compare;
    function __construct( $amethod, $restleturl='' )
    {
        $this->http_method = 'GET';
        $this->request_method = $amethod;
        $this->request_args = null;
        $this->request_compare = null;
        $this->request_url = !empty( $restleturl ) ? $restleturl : NS_RECORD_RESTLET;
    }
    public function execute( $arguments )
    {
        if ($this->request_method === null || strlen( $this->request_method ) < 1)
        {
            throw new Exception( 'Request type must be defined.' );
        }
        if ($arguments == null || empty( $arguments ))
        {
            throw new Exception( 'Cannot intiate a request with empty argument set.' );
        }
        // prepare the arguments for transport by: converting to json, performing integrity check and encoding for url transport
        $json_args = json_encode( $arguments );
        $this->request_compare = sha1( $json_args );
        $this->request_args = base64_encode( rawurlencode( $json_args ) );
        $rest_args = array(
            'method' => $this->request_method,
            'rq' => $this->request_args,
            'cf' => $this->request_compare,
        );
        // perform curl request on restlet and return results
        $request_url = $this->request_url;
        if (strtoupper( $this->http_method ) == 'GET')
        {
            foreach ($rest_args as $arg => $value)
            {
                $request_url .= "&$arg=$value";
            }
        }
        $rest_client = new NsRestRequest( $request_url );
        $rest_client->execute();
        $result = json_decode( $rest_client->getResponse() );
        unset( $rest_client );
        return $result;
    }
}
As in the previous posts we are focusing specifically on the GET verb, but it would not be very difficult to implement POST, PUT or DELETE into this function.  Note that while preparing $rest_args we put together cf as a security procedure.  We mentioned this in the suitescript, in my experience testing this implementation the PHP sha1 hash does not match the hash generated by Suitescript.  We'll continue generating the comparison argument here anyways with the expectation that the Suitescript hashing issue can be resolved.

This is a very simple wrapper that we will now extend with a not-so-simple searching class.  To keep some consistency between PHP and Suitescript we are going to mirror the NS Searching API so far as we can:


<?php
require_once( 'NsRecord.php' );
class NsRecordSearch extends NsRecord
{
    public $search_type;
    public $search_id;
    public $search_filters;
    public $search_columns;
    const ALLOWED_SUMMARIES = 'group,sum,count,avg,min';
    const REQUEST_METHOD = 'search';
    function __construct( $atype=null, $aid=null )
    {
        parent::__construct( self::REQUEST_METHOD );
        $this->search_type = $atype;
        $this->search_id = $aid;
        $this->search_filters = array();
        $this->search_columns = array();
    }
    public function flush()
    {
        if (!empty( $this->search_filters ))
        {
            unset( $this->search_filters );
            $this->search_filters = array();
        }
        if (!empty( $this->search_columns ))
        {
            unset( $this->search_columns );
            $this->search_columns = array();
        }
    }
    public function setFilter( $aname, $ajoin, $aop, $avalue, $avalue2=null, $fldtype=null, $formula=null, $srchLeftParen=null, $srchRightParen=null, $srchOr=null )
    {
        $fitems = array(
            'name' => $aname,
            'join' => $ajoin,
            'operator' => $aop,
            'value' => $avalue,
            'value2' => $avalue2,
            'fldtype' => $fldtype,
            'leftparens' => $srchLeftParen,
            'rightparens' => $srchRightParen,
            'or' => $srchOr,
        );
        if ($formula !== null)
        {
            $fitems['formula'] = $formula;
        }
        $this->search_filters[] = (object)$fitems;
    }
    public function setColumn( $aname, $ajoin=null, $asummary=null, $sorton=null, $label=null, $function=null, $formula=null )
    {
        $citems = array(
            'name' => $aname,
            'join' => $ajoin,
            'summary' => ($asummary !== null && in_array( $asummary, explode( ',', self::ALLOWED_SUMMARIES )  ) ? $asummary : null),
        );
        if ($sorton !== null)
        {
            $citems['sort'] = (int)((bool)$sorton);
        }
        if ($label !== null)
        {
            $citems['label'] = (string)$label;
        }
        if ($function !== null)
        {
            $citems['functionid'] = (string)$function;
        }
        if ($formula !== null)
        {
            $citems['formula'] = (string)$formula;
        }
        $this->search_columns[] = (object)$citems;
    }
    public function lookup()
    {
        if ($this->search_type === null)
        {
            trigger_error( 'Search type must be defined.', E_USER_NOTICE );
            return null;
        }
        if (empty( $this->search_filters ) && empty( $this->search_columns ) && $this->search_id === null)
        {
            trigger_error( 'Insufficiant criteria to perform search.', E_USER_WARNING );
            return null;
        }
        $results = $this->execute( array(
            'type' => $this->search_type,
            'id' => $this->search_id,
            'filters' => $this->search_filters,
            'columns' => $this->search_columns,
        ) );
        return $results;
    }
}
A few gotchas here:
In setFilter() we see again our 'fldtype' hack we use to work around the date-type inconsistency in our REST script.  In the api for nlobjSearchFilter you'll find a setter for adding a single formula to a search filter, comparable to setting a formula in the Saved Search GUI in Netsuite.  Here we implement the formula setter as an optional argument.  We do the same thing for the undocumented methods to set parentheses and or-logic.  Otherwise setFilter() mirrors the constructor for nlobjSearchFilter.
Similarly setColumn() is very similar to the constructor for nlobjSearchColumn, with the addition of optional arguments for the column special setters (setFormula, setFunction, setLabel and setSort). [the newest release of Netsuite (2012.1) adds a new setter, setWhenOrderedBy, that has not been addressed here but could be added in very easily.]

To step back for a moment lets review what we have accomplished here:

  • We have a Netsuite RESTlet that can receive requests for disparate services, that can be extended through additional methods.
  • We have a general-purpose REST client in PHP.  This can be used for consuming most REST services, but has specific customizations required by Netsuite.
  • We have a general interface, NsRecord, for sending requests to our REST service.
  • And finally we have a specific PHP implementation, NsRecordSearch, of our saved search service.
We can now actually run a search with code like the following:
<?php
require_once( 'NsRecordSearch.php' );
$search = NsRecordSearch( 'client', 'custsearch_client_primary' );
$search->setFilter( 'status', null, 'anyof', array('CLIENT','CLIENT-ARCHIVED') );
$search->setFilter( 'inactive', null, 'is', 'T' );
$search->setColumns( 'field' );
$records = $search->lookup();
?>
The real power of this tool is it's ability to scale, adding a new service only requires a method to be added to the RESTlet and a new implementation of NsRecord.  This has been surprisingly handy in my work, hopefully you get some use out of it as well.

Please let me know how this works out for you, and any tweaks or improvements you come up with.  Cheers!

No comments:

Post a Comment