Skip to content

Adding Methods to the API

Jason Knight edited this page Sep 5, 2013 · 15 revisions

I am writing this page while actually adding a new feature to the API, this will be particularly in depth.

The first step is to open up classes/class-wc-json-api.php and find the function called public static function getImplementedMethods(), it will look similar to this:

<?php
public static function getImplementedMethods() {
    if (self::$implemented_methods) {
      return self::$implemented_methods;
    }
    self::$implemented_methods = array(
      'get_system_time',
      'get_products',
      'get_categories',
      'get_taxes',
      'get_shipping_methods',
      'get_payment_gateways',
      'get_tags',
      'get_products_by_tags',
      'get_customers',
      
      // Write capable methods
      
      'set_products',
      'set_categories',

    );
    return self::$implemented_methods;
  }

In this case, we will add the method "get_orders". All methods are named get|set_{resource}s. Since the API thinks ONLY IN COLLECTIONS.

We will modify it like so:

<?php
  public static function getImplementedMethods() {
    if (self::$implemented_methods) {
      return self::$implemented_methods;
    }
    self::$implemented_methods = array(
      'get_system_time',
      'get_products',
      'get_categories',
      'get_taxes',
      'get_shipping_methods',
      'get_payment_gateways',
      'get_tags',
      'get_products_by_tags',
      'get_customers',
      'get_orders', // New Method
      
      // Write capable methods
      
      'set_products',
      'set_categories',

    );
    return self::$implemented_methods;
  }

Here we have added the new method get_orders. We will need to scroll to the bottom of the file and add a method like so:

<?php
public function get_customers( $params ) {
    global $wpdb;
    
    blah blah blah
    $this->result->setPayload($customers);
    return $this->done();
  }

  public function get_orders( $params ) {
    $posts_per_page = $this->helpers->orEq( $params['arguments'], 'per_page', 15 ); 
    $paged          = $this->helpers->orEq( $params['arguments'], 'page', 0 );
    $ids            = $this->helpers->orEq( $params['arguments'], 'ids', false);

    ... Your code will be here ...

    $this->result->setPayload( $orders );
    return $this->done();
  }

In this case, we added get_orders right after the function get_customers. Normally, we group functions of like kind together. So new methods for customers would be inserted after get_customers and before get_orders.

Always end the function with return $this->done(); This is a special method that takes care of preparing our results and returning them based on the configuration.

This is really all that is API specific. At this point, you can use any code you want to populate the $orders collection. It can be an array of arrays, or an array of objects, or models, or even string representations (Like CSV lines perhaps?) You are not limited in any way.

In this case, we will get a bit complicated. We will create an order model to help us out.

Models inherit from JSONAPIBaseRecord class.

<?php
/**
 * A Product class to insulate the API from the details of the
 * database representation
*/
require_once(dirname(__FILE__) . "/class-rede-base-record.php");

class WC_JSON_API_Order extends JSONAPIBaseRecord {

  public static $_meta_attributes_table; 
  public static $_post_attributes_table;
  
  private $_meta_attributes;
  private $_post_attributes;
  private $_status;

}

We want to have as seamless as possible an interface between the API and the somewhat chaotic WP/WooCom backend. What we would consider a "Model" in OOP terms is generally split across at least two, and in the case of an order 3 different database tables. I imagine the WooCom team had something important in mind when they chose this method, it may have something to do with Wordpress' term API, but for humble/ignorant programmers like me, it's all much to sophisticated, I prefer a kind of stupid simplicity.

Because we want to abstract away the dirty business of updating, fetching, and creating records as much as possible and simply deal with collections, we will have to create a map of the attributes.

With Meta attributes and Post attributes, this is all very trivial, with an order, we will eventually have to deal with the status being saved as a term.

Here is our Map.

<?php
  public static function setupMetaAttributes() {
    if ( self::$_meta_attributes_table ) {
      return;
    }
    // We only accept these attributes.
    self::$_meta_attributes_table = array(
      'order_key'				      => array('name' => '_order_key',                  'type' => 'string'), 
      'billing_first_name'	  => array('name' => '_billing_first_name',         'type' => 'string'), 
      'billing_last_name' 	  => array('name' => '_billing_last_name',          'type' => 'string'), 
      'billing_company'		    => array('name' => '_billing_company' ,           'type' => 'string'), 
      'billing_address_1'		  => array('name' => '_billing_address_1',          'type' => 'string'), 
      'billing_address_2'		  => array('name' => '_billing_address_2',          'type' => 'string'), 
      'billing_city'			    => array('name' => '_billing_city',               'type' => 'string'), 
      'billing_postcode'		  => array('name' => '_billing_postcode',           'type' => 'string'), 
      'billing_country'		    => array('name' => '_billing_country',            'type' => 'string'), 
      'billing_state' 		    => array('name' => '_billing_state',              'type' => 'string'), 
      'billing_email'			    => array('name' => '_billing_email',              'type' => 'string'), 
      'billing_phone'			    => array('name' => '_billing_phone',              'type' => 'string'), 
      'shipping_first_name'	  => array('name' => '_shipping_first_name',        'type' => 'string'), 
      'shipping_last_name'	  => array('name' => '_shipping_last_name' ,        'type' => 'string'), 
      'shipping_company'		  => array('name' => '_shipping_company',           'type' => 'string'), 
      'shipping_address_1'	  => array('name' => '_shipping_address_1' ,        'type' => 'string'), 
      'shipping_address_2'	  => array('name' => '_shipping_address_2',         'type' => 'string'), 
      'shipping_city'			    => array('name' => '_shipping_city',              'type' => 'string'), 
      'shipping_postcode'		  => array('name' => '_shipping_postcode',          'type' => 'string'), 
      'shipping_country'		  => array('name' => '_shipping_country',           'type' => 'string'), 
      'shipping_state'		    => array('name' => '_shipping_state',             'type' => 'string'), 
      'shipping_method'		    => array('name' => '_shipping_method' ,           'type' => 'string'), 
      'shipping_method_title'	=> array('name' => '_shipping_method_title',      'type' => 'string'), 
      'payment_method'		    => array('name' => '_payment_method',             'type' => 'string'), 
      'payment_method_title' 	=> array('name' => '_payment_method_title',       'type' => 'string'), 
      'order_discount'		    => array('name' => '_order_discount',             'type' => 'number'), 
      'cart_discount'			    => array('name' => '_cart_discount',              'type' => 'number'), 
      'order_tax'				      => array('name' => '_order_tax' ,                 'type' => 'number'), 
      'order_shipping'		    => array('name' => '_order_shipping' ,            'type' => 'number'), 
      'order_shipping_tax'	  => array('name' => '_order_shipping_tax' ,        'type' => 'number'), 
      'order_total'			      => array('name' => '_order_total',                'type' => 'number'), 
      'customer_user'			    => array('name' => '_customer_user',              'type' => 'number'), 
      'completed_date'		    => array('name' => '_completed_date',             'type' => 'datetime'), 
      'status'                => array(
                                        'name' => 'status', 
                                        'type' => 'string', 
                                        'getter' => 'getStatus',
                                        'setter' => 'setStatus'
                                ),
      //'quantity'          => array('name' => '_stock',            'type' => 'number', 'filters' => array('woocommerce_stock_amount') ),
      
    );
    /*
      With this filter, plugins can extend this ones handling of meta attributes for a product,
      this helps to facilitate interoperability with other plugins that may be making arcane
      magic with a product, or want to expose their product extensions via the api.
    */
    self::$_meta_attributes_table = apply_filters( 'woocommerce_json_api_order_meta_attributes_table', self::$_meta_attributes_table );
  } // end setupMetaAttributes

In the code base, this is all well formatted, here it looks a bit chaotic at first, but is fairly transparent.This allows us to choose which Meta attributes we support, and how we handle them, it's more or less a copy/paste job. The only one of real interest here is:

<?php
'status'                => array(
                                        'name' => 'status', 
                                        'type' => 'string', 
                                        'getter' => 'getStatus',
                                        'setter' => 'setStatus'
                                ),

This tells the system that we have a special getter/setter function defined, and that it should call this function when we try to access this attribute on the model.

The rest of them were just copy and pasted in.

Our next stop is to define the Post attributes, if this is indeed a "custom post type", later on we'll talk about handling Models that have their own table, such as OrderItems.

<?php
  public static function setupPostAttributes() {
    if ( self::$_post_attributes_table ) {
      return;
    }
    self::$_post_attributes_table = array(
      'name'            => array('name' => 'post_title',  'type' => 'string'),
      'guid'            => array('name' => 'guid',        'type' => 'string'),

    );
    self::$_post_attributes_table = apply_filters( 'woocommerce_json_api_order_post_attributes_table', self::$_post_attributes_table );
  }

Now we will create thate getter/setter function:

<?php
  public function getStatus() {
    if ( $this->_status ) {
      return $this->_status;
    }
    $terms = wp_get_object_terms( $this->id, 'shop_order_status', array('fields' => 'slugs') );
    $this->_status = (isset($terms[0])) ? $terms[0] : 'pending';
    return $this->_status;
  }

  public function setStatus( $s ) {
    $this->_status = $s;
  }

The next step is to define how this Model will be converted into an array for delivery through the API.

<?php
  public function asApiArray() {
    global $wpdb;
    $attributes = array_merge(self::$_post_attributes_table, self::$_meta_attributes_table);
    $attributes_to_send['id'] = $this->getModelId();

    foreach ( $attributes as $name => $desc ) {
      $attributes_to_send[$name] = $this->dynamic_get( $name, $desc, $this->getModelId());
    }
    return $attributes_to_send;
  }

You can more or less just copy and paste this function in, as it's pretty standard code. Unless, like in the Product Model, you need to do something else.

When we extend this Model to include it's OrderItems, you'll see what I mean.

Next, we need to tell the system how to find Models by id. This is more or less for convience, and allows you to have a simple FindById method.

<?php
  public static function find( $id ) {
    global $wpdb;
    self::setupPostAttributes();
    self::setupMetaAttributes();
    $order = new WC_JSON_API_Order();
    $order->setValid( false );
    $post = get_post( $id, 'ARRAY_A' );
    if ( $post ) {
      $order->setModelId( $id );
      foreach ( self::$_post_attributes_table as $name => $desc ) {
        $order->dynamic_set( $name, $desc,$post[$desc['name']] );
      }
      foreach ( self::$_meta_attributes_table as $name => $desc ) {
        $value = get_post_meta( $id, $desc['name'], true );
        // We may want to do some "funny stuff" with setters and getters.
        // I know, I know, "no funny stuff" is generally the rule.
        // But WooCom or WP could change stuff that would break a lot
        // of code if we try to be explicity about each attribute.
        // Also, we may want other people to extend the objects via
        // filters.
        $order->dynamic_set( $name, $desc, $value, $order->getModelId() );
      }
      $order->setValid( true );
      $order->setNewRecord( false );
    }
    return $order;
  }

We only have a few more things to define on this model to get it up and running:

<?php
    /**
  *  From here we have a dynamic getter. We return a special REDENOTSET variable.
  */
  public function __get( $name ) {
    if ( isset( self::$_meta_attributes_table[$name] ) ) {
      if ( isset(self::$_meta_attributes_table[$name]['getter'])) {
        return $this->{self::$_meta_attributes_table[$name]['getter']}();
      }
      if ( isset ( $this->_meta_attributes[$name] ) ) {
        return $this->_meta_attributes[$name];
      } else {
        return '';
      }
    } else if ( isset( self::$_post_attributes_table[$name] ) ) {
      if ( isset( $this->_post_attributes[$name] ) ) {
        return $this->_post_attributes[$name];
      } else {
        return '';
      }
    }
  } // end __get
  
  // Dynamic setter
  public function __set( $name, $value ) {
    if ( isset( self::$_meta_attributes_table[$name] ) ) {
      if ( isset(self::$_meta_attributes_table[$name]['setter'])) {
        $this->{self::$_meta_attributes_table[$name]['setter']}( $value );
      }
      $this->_meta_attributes[$name] = $value;
    } else if ( isset( self::$_post_attributes_table[$name] ) ) {
      $this->_post_attributes[$name] = $value;
    } else {
      throw new Exception( __('That attribute does not exist to be set.','woocommerce_json_api') . " `$name`");
    }
  } 

Again here we just have to copy/paste some code in. I make work out a way for this to all be handled by the base record at some point.

And finally, we need to create an all() function.

<?php
  public static function all($fields = 'id') {
    global $wpdb;
    $sql = "SELECT $fields from {$wpdb->posts} WHERE post_type IN ('shop_order')";
    $order = new WC_JSON_API_Product();
    $order->addQuery($sql);
    return $order;
  }

Now that that is done, we can return to the main API function get_orders and fill it out.

The first bit of code we will add is this:

<?php
    if ( ! $ids ) {
      
      $posts = WC_JSON_API_Order::all()->per($posts_per_page)->page($paged)->fetch(function ( $result) {
        return $result['id'];
      });
      JSONAPIHelpers::debug( "IDs from all() are: " . var_export($posts,true) );
    } else if ( $ids ) {
    
      $posts = $ids;
      
    }

This let's us request paginated results from the database. The most important line is:

<?php
      $posts = WC_JSON_API_Order::all()->per($posts_per_page)->page($paged)->fetch(function ( $result) {
        return $result['id'];
      });

Here we are using a closure (newly supported in PHP) to format the results. The function fetch( $callback ) is defined in JSONAPIBaseRecord like so:

<?php
  public function fetch( $callback ) {
    global $wpdb;
    $sql = $this->_queries_to_run[count($this->_queries_to_run) - 1];
    if ( ! empty($sql) ) {
      if ( $this->_per_page && $this->_page) {
        $sql .= " LIMIT {$this->_page},{$this->_per_page}";
      }
      $results = $wpdb->get_results($sql,'ARRAY_A');
      JSONAPIHelpers::debug("in function fetch: WPDB returned " . count($results) . " results");
      foreach ( $results as &$result ) {
        if ( $callback ) {
          $result = call_user_func($callback,$result);
        }
      }
      if (count($results) < 1) {
        JSONAPIHelpers::debug("in function fetch, empty result set using: $sql");
      } else {
        JSONAPIHelpers::debug("in function fetch: " . count($results) . " were returned from: " . $sql);
      }
      return $results;
    } else {
      JSONAPIHelpers::debug("in function fetch, sql was empty.");
      return null;
    }
  }

If you are wondering, JSONAPIHelpers::debug() is defined in class-rede-helpers.php as:

<?php
  public static function debug($text) {
    if ( ! defined('WC_JSON_API_DEBUG') ) {
      return;
    }
    $fp = @fopen(REDE_PLUGIN_BASE_PATH . "debug.log",'a');
    if ($fp) {
      fwrite($fp,$text . "\n");
      fclose($fp);
    }
    
  }

Now we need to prepare the collection to be sent via the API, here is the code for that:

<?php
    $orders = array();
    foreach ( $posts as $post_id) {
      try {
        $post = WC_JSON_API_Order::find($post_id);
      } catch (Exception $e) {
        JSONAPIHelpers::error("An exception occurred attempting to instantiate a Order object: " . $e->getMessage());
        $this->result->addError( __("Error occurred instantiating Order object"),-99);
        return $this->done();
      }
      
      if ( !$post ) {
        $this->result->addWarning( $post_id. ': ' . __('Order does not exist','woocommerce_json_api'), WCAPI_ORDER_NOT_EXISTS, array( 'id' => $post_id) );
      } else {
        $orders[] = $post->asApiArray();
      }
      
    }

At this point, we create a test file in tests/get_orders.php and see if it all works:

<?php
include "functions.php";
include "config.php";

$data = array(
  'action'      => 'woocommerce_json_api',
  'proc'        => 'get_orders',
  'arguments'   => array(
    'token' => $token,
    'per_page' => 2,
    'page'     => 1
  )
);
echo json_encode($data,JSON_PRETTY_PRINT);

$result = curl_post($url,$data);
echo "Result is: \n\n";
echo $result;
echo "\n\n";

And we run it with the command php tests/get_orders.php

And receive(real output):

salor@salor-retail:/var/www/woo/wp-content/plugins/woocommerce-json-api$ php tests/get_orders.php 
{
    "action": "woocommerce_json_api",
    "proc": "get_orders",
    "arguments": {
        "token": "1234",
        "per_page": 2,
        "page": 1
    }
}Result is: 

{
    "action": "woocommerce_json_api",
    "proc": "get_orders",
    "arguments": {
        "token": "1234",
        "per_page": "2",
        "page": "1"
    },
    "status": true,
    "errors": [

    ],
    "warnings": [

    ],
    "notifications": [

    ],
    "payload": [
        {
            "id": "1562",
            "name": "Order &ndash; Jul 19, 2013 @ 12:35 PM",
            "guid": "http:\/\/deals.localhost\/?post_type=shop_order&#038;p=1562",
            "order_key": "order_51e932a64a690",
            "billing_first_name": "John",
            "billing_last_name": "Doe",
            "billing_company": "",
            "billing_address_1": "My complete address",
            "billing_address_2": "",
            "billing_city": "MyCity",
            "billing_postcode": "99999",
            "billing_country": "FR",
            "billing_state": "",
            "billing_email": "[email protected]",
            "billing_phone": "5555367",
            "shipping_first_name": "John",
            "shipping_last_name": "Doe",
            "shipping_company": "",
            "shipping_address_1": "My complete address",
            "shipping_address_2": "",
            "shipping_city": "MyCity",
            "shipping_postcode": "99999",
            "shipping_country": "FR",
            "shipping_state": "",
            "shipping_method": "free_shipping",
            "shipping_method_title": "Free Shipping",
            "payment_method": "cheque",
            "payment_method_title": "Cheque Payment",
            "order_discount": "0.00",
            "cart_discount": "0.00",
            "order_tax": "0",
            "order_shipping": "0.00",
            "order_shipping_tax": "0",
            "order_total": "15.95",
            "customer_user": "0",
            "completed_date": "2013-07-19 23:53:45",
            "status": "pending"
        },
        {
            "id": "1573",
            "name": "Order &ndash; July 20, 2013 @ 12:19 AM",
            "guid": "http:\/\/deals.localhost\/?post_type=shop_order&#038;p=1573",
            "order_key": "order_51e9c99362da7",
            "billing_first_name": "John",
            "billing_last_name": "Doe",
            "billing_company": "",
            "billing_address_1": "My Other Address",
            "billing_address_2": "",
            "billing_city": "MyCity",
            "billing_postcode": "99999",
            "billing_country": "FR",
            "billing_state": "",
            "billing_email": "[email protected]",
            "billing_phone": "5555367",
            "shipping_first_name": "John",
            "shipping_last_name": "Doe",
            "shipping_company": "",
            "shipping_address_1": "2955",
            "shipping_address_2": "",
            "shipping_city": "MyCity",
            "shipping_postcode": "99999",
            "shipping_country": "FR",
            "shipping_state": "",
            "shipping_method": "free_shipping",
            "shipping_method_title": "Free Shipping",
            "payment_method": "bacs",
            "payment_method_title": "Direct Bank Transfer",
            "order_discount": "0.00",
            "cart_discount": "0.00",
            "order_tax": "0",
            "order_shipping": "0.00",
            "order_shipping_tax": "0",
            "order_total": "53.96",
            "customer_user": "1",
            "completed_date": "2013-07-20 15:55:16",
            "status": "pending"
        }
    ],
    "payload_length": 2
}



 - No errors

Clone this wiki locally