SugarCRM Developers

SugarCRM Cookbook - So you wanna override an endpoint

Blog Post created by SugarCRM Developers on Mar 21, 2014

Post originally written by sugarraagaard.

 

 

1. Introduction

 

So you have yourself in a bit of a bind, you need to get an existing API to do something different. Maybe you need more information on an Account record, or perhaps your custom module needs some custom handling to go along with it. This cookbook entry is going to give you a few examples on how to override the default behavior of the REST API.

 

2. Before we get started

 

Before we get started you need to have some basic idea of how to interact with the SugarCRM API from a client standpoint and The School of REST – Part 1 is a great place to start. After that you should probably learn how to add a new API so you could use that instead and for that I will point you to Adding a rest endpoint.With that taken care of let's get started on modifying an existing API.

 

3. Api Helper Classes

 

In order to facilitate different modules having different needs we added the SugarApiHelper class. You can find this class in data/SugarBeanApiHelper.php this class is used by all modules and is responsible for both formatting any bean before it is displayed to the client and also handling populating the bean from the API's requests to save/update.

 

To create a new ApiHelper for the class Contacts put a file in either custom/module/Contacts/ContactsApiHelper.php and then name the class in there CustomContactsApiHelper and be sure to extend the ContactsApiHelper. An important thing to note here is that you use the module name in front of ApiHelper, so it is Contacts instead of just Contact. For your own custom modules you can just put that ApiHelper class in your custom module's base directory. Some of our shipped modules have ApiHelpers, and others don't so if you are creating a custom api helper be sure to check if the base api helper exists and extend that if it does, otherwise just extend the core SugarBeanApiHelper class.

 

4. Overriding FormatForApi()

 

For our cookbook entry here we are going to build a method to display some information about the biggest open opportunity related to this contact.

 

To add a api helper for the Contacts module first we check for an existing helper. Out of the box helpers will be located in the modules directory and will be named the module name with ApiHelper.php on the end. So if we look at modules/Contacts/ we will see a ContactsApiHelper.php file (note: It's not ContactApiHelper.php we use the plural here). Since there is a module specific helper already when we create our custom helper we will need to be sure to extend our CustomContactsApiHelper class from the ContactsApiHelper instead of from SugarBeanApiHelper. The method we are going to override here is formatForApi(). This method is responsible for converting a bean object into an array suitable for outputting over the API, it is called from all over the API, single record views, listing records, showing lists of related records, everywhere.  One of the things it does for example is that it reformats all of the dates and times into a standardized ISO-8601 format to make it simple for the other side to parse. To get this started we will make it simple we are just overriding the formatForApi() method, calling the parent method in there and then adding some filler data to the top_opp section and returning the data. This will be an easy way for us to check and make sure our api helper is being called. If you try this and it doesn't work the first thing to do is run a quick repair and rebuild to have it rebuild the file path cache and then the second thing is to make sure you are using the plural version and you name your class correctly.

 

ContactsApiHelper.php

<?php
require_once('modules/Contacts/ContactsApiHelper.php');

// Since the ContactsApiHelper exists, we'll extend it
// If it didn't we would just extend the SugarBeanApiHelper
class CustomContactsApiHelper extends ContactsApiHelper
{
// Mimic the SugarBeanApiHelper->formatForApi() class
public function formatForApi(SugarBean $bean, array $fieldList = array(), array $options = array())
    {
// First off, call the parent class
$data = parent::formatForApi($bean, $fieldList, $options);

// Make sure they requested the top_opp field, or no field restriction at all
if (empty($fieldList) || in_array('top_opp', $fieldList) ) {
// Let's just put some filler data in here for now
$data['top_opp'] = 'Rest in Peas Burger';
        }

// formatForApi returns the bean formatted as a hash in API format
return $data;
    }
}

 

Here is what we get back in our initial response. Notice that we added a fields parameter to the list in order to make sure we fetched the top_opp field.

 

first_response.json

# GET rest/v10/Contacts?fields=id,name,top_opp&max_num=2
{
"next_offset": 2,
"records": [
        {
"id": "f2d4042e-6d9e-88af-3c00-53271512e333",
"name": "Tabitha Johansson",
"date_modified": "2014-03-17T15:29:16+00:00",
"_acl": {
"fields": {}
            },
"top_opp": "Rest in Peas Burger",
"_module": "Contacts"
        },
        {
"id": "f23e19e3-a59e-8985-4202-5327156a36a9",
"name": "Kuchi Kopi",
"date_modified": "2014-03-17T15:29:16+00:00",
"_acl": {
"fields": {}
            },
"top_opp": "Rest in Peas Burger",
"_module": "Contacts"
        }
    ]
}

 

To fill in the rest of this function we turn to our good friend SugarQuery. We build a seed bean, pass that to SugarQuery so it knows how to translate the fields then we join it to the current contact. Finally we restrict it down to the top opportunity. With that we grab the bean using fetchFromQuery() so we don't have to manually populate the bean, run that bean through the Opportunities formatForApi() to keep things nice and consistent and copy the API ready format over to the output data.

 

ContactsApiHelper.php

<?php
require_once('modules/Contacts/ContactsApiHelper.php');

// Since the ContactsApiHelper exists, we'll extend it
// If it didn't we would just extend the SugarBeanApiHelper
class CustomContactsApiHelper extends ContactsApiHelper
{
// Mimic the SugarBeanApiHelper->formatForApi() class
public function formatForApi(SugarBean $bean, array $fieldList = array(), array $options = array())
    {
// First off, call the parent class
$data = parent::formatForApi($bean, $fieldList, $options);

// Make sure they requested the top_opp field, or no field restriction at all
if (empty($fieldList) || in_array('top_opp', $fieldList) ) {
// Build a list of the fields we care about
static $oppFields = array(
'id',
'name',
'currency_id',
'amount',
'date_closed',
            );

// Make sure we don't populate this some other way
if (!isset($bean->top_opp) && !empty($bean->id)) {
// Get a seed for Opportunities
$oppSeed = BeanFactory::newBean('Opportunities');
$q = new SugarQuery();
// Set the from bean
$q->from($oppSeed);
// Join in using the 'contacts' link field
$contactJoin = $q->join('contacts');
// Limit our join to just the contact we care about
$q->where()->equals($contactJoin->joinName().'.id', $bean->id);
$q->where()->notIn('sales_stage', array('Closed Lost', 'Closed Won'));
// Sort by the "usdollar" amount, the normal amount field could be in
// different currencies and so the sorting will be all wrong.
$q->orderBy('amount_usdollar DESC');
// Just get the top opportunity
$q->limit(1);

// Use fetchFromQuery and pass in the fields we care about
$beans = $oppSeed->fetchFromQuery($q, $oppFields);

if (count($beans) > 0) {
// Even though we had a limit of 1, fetchFromQuery still returns an array
// They are indexed by ID, so let's just fetch the first and only one
$bean->top_opp = array_pop($beans);
                } else {
// Flag it so we know we tried to set it, there just wasn't anything there
$bean->top_opp = null;
                }
            }

$data['top_opp'] = array();
if ($bean->top_opp != null) {
// getHelper will get us the helper we want
$data['top_opp'] = ApiHelper::getHelper($GLOBALS['service'], $bean->top_opp)
->formatForApi($bean->top_opp, $oppFields);
            }
        }

// formatForApi returns the bean formatted as a hash in API format
return $data;
    }
}

 

Now you see that we get the top opportunity on every Contact. Also note how the numbers and the dates are formatted the exact same way as everything else in the API thanks to our call to formatForApi()

 

second_response.json

# GET rest/v10/Contacts?fields=id,name,top_opp&max_num=2
{
"next_offset": 2,
"records": [
        {
"id": "f23e19e3-a59e-8985-4202-5327156a36a9",
"name": "Kuchi Kopi",
"date_modified": "2014-03-18T16:45:29+00:00",
"_acl": {
"fields": {}
            },
"top_opp": {
"id": "f116eb68-1f2d-d6be-6d07-532715473850",
"name": "Make glowing burger",
"amount": "12171.000000",
"currency_id": "-99",
"_acl": {
"fields": {}
                }
            },
"_module": "Contacts"
        },
        {
"id": "f2d4042e-6d9e-88af-3c00-53271512e333",
"name": "Tabitha Johansson",
"date_modified": "2014-03-18T16:26:44+00:00",
"_acl": {
"fields": {}
            },
"top_opp": {
"id": "83ef4ea5-56d8-7e30-e592-532715a73955",
"name": "Build Custom Piano Burger",
"amount": "12279.000000",
"currency_id": "-99",
"_acl": {
"fields": {}
                }
            },
"_module": "Contacts"
        }
    ]
}

 

5. Overriding populateFromApi()

 

Typical relate fields in SugarCRM do not respond to changes on the parent record. This is an intentional design decision to prevent users from accidentally changing parent records. For "top_opp" however we want it to be different, we want to support changing that record directly from the related Contact. This is where populateFromApi() comes in. The symmetric method for formatForApi() this method takes API formatted data and populates it into a bean. By overriding this method in the same class we defined above we will be able to force feed the data into the top Opportunity and save it manually. To do this we just call the parent method, make sure there are no errors and that they actually submitted some "top_opp" data. After we have verified that we should populate a bean, we build a bean, retrieve it based on the passed in ID and then run it through the Opportunities ApiHelper to populate that data. If there are no errors there we manually save the Opportunity and return true so that the Contact is saved by whatever called it.

 

ContactsApiHelper.php

<?php
require_once('modules/Contacts/ContactsApiHelper.php');

// Since the ContactsApiHelper exists, we'll extend it
// If it didn't we would just extend the SugarBeanApiHelper
class CustomContactsApiHelper extends ContactsApiHelper
{
// Mimic the SugarBeanApiHelper->formatForApi() class
public function formatForApi(SugarBean $bean, array $fieldList = array(), array $options = array())
    {
// Same as before, snipped for display purposes.
    }

// Mimic SugarBeanApiHelper->populateFromApi()
public function populateFromApi(SugarBean $bean, array $submittedData, array $options = array() )
    {
$errors = parent::populateFromApi($bean, $submittedData, $options);
if ($errors !== true || empty($submittedData['top_opp']['id'])) {
// There were errors in the original, don't bother saving the opp
// Or they aren't trying to save the opp, so just pass back the parent
return $errors;
        }

// Do a full retrieve so the logic hooks on save have everything they need
$opp = BeanFactory::getBean('Opportunities', $submittedData['top_opp']['id']);
// Load up the ApiHelper class for Opportunities and have it populate from
// this subsection of data.
$oppErrors = ApiHelper::getHelper($GLOBALS['service'], $opp)
->populateFromApi($opp, $submittedData['top_opp']);
if ( $oppErrors !== true ) {
// Errors populating the opportunity
return $oppErrors;
        }
// Typically the save happens after this is all done, but since that function doesn't
// know about this extra opportunity, we'll just save it here.
$opp->save();

// Checked both the parent errors, and the opp errors, they are both true so return true
return true;
    }   
}

 

Testing it out we see that we can change the name from the related Contact. Amounts and expected closed dates could also be set in PRO editions, but ENT has some special logic around those so we can't populate them externally as easily.

 

third_response.json

# PUT /rest/v10/Contacts/f23e19e3-a59e-8985-4202-5327156a36a9?fields=id,name,top_opp
# Contents of the PUT request
{
"top_opp": {
"id": "f116eb68-1f2d-d6be-6d07-532715473850",
"name": "Make super glowing burger"
  }
}
# Returned
{
"id": "f23e19e3-a59e-8985-4202-5327156a36a9",
"name": "Kuchi Kopi",
"date_modified": "2014-03-19T10:04:51-06:00",
"_acl": {
"fields": {}
    },
"top_opp": {
"id": "f116eb68-1f2d-d6be-6d07-532715473850",
"name": "Make super glowing burger",
"amount": "12171.000000",
"currency_id": "-99",
"date_closed": "2014-09-23",
"_acl": {
"fields": {}
        }
    },
"_module": "Contacts"
}

 

6. Adding hooks to fetchFromQuery()

 

Eagle eyed readers may have spotted the fact that if you run a query in fetchFromApi() that means on a list you will run that query once per row, for efficiency sake that's not exactly the best thing to do. We will use this as a good example on how to modify list views to run alternative queries. Please note that in the future normal retrieve() calls map run through this same set of code, so be prepared for that. We've seen fetchFromQuery() run in the previous example of fetchFromApi(), so we see that it takes in a query object and returns an array of populated beans. The good news with this function is that it was designed to be hooked into using both the before_fetch_query and after_fetch_query logic hooks. The before hook is perfect for modifying the query to have it return different data for a set of fields, or return extra data that you can get at from the after hook to populate into the bean. While we could have done it using both a before and after hook I decided to make it easy and just run it as a separate query per list and run it all through the after_fetch_query hook.

 

To get started here we need to add a logic hook file into custom/modules/Contacts/logic_hooks.php that looks like this:

 

logic_hooks.php

<?php
// If you are making this module loadable, put it in an extension file instead
// These are needed if you are hand-building a logic hook file
$hook_version = 1;
$hook_array = Array();
// Setup a hook into the after_fetch_query hook
$hook_array['after_fetch_query'][] = array(
90, // Priorty, what order does this hook run in
'DemoProcessQuery', // What do we want to name this hook
'custom/modules/Contacts/DemoHook.php', // What filename contains the hook class
'DemoHook', // What is the demo hook class name
'processQuery', // What function to call in the class
);

 

Now if we look at the actual code of the custom/modules/Contacts/DemoHook.php that we referenced in the logic_hooks.php file we will notice that we skip out early if we aren't in the requested list of fields, or if there are no beans in the list. If we are clear to proceed we go ahead and build up the SugarQuery in much the same way as the previous example, except this time instead of passing in a single contact we pass in an array of contacts to match on and instead of limiting to the first result we group by the contact id and order by the amount_usdollar, this should give us the top opportunity per contact id. This will only work on MySQL, but I figured it was the easiest way to demo this. From there we run fetchFromQuery() on the Opportunities themselves and then using the raw rows that were returned we re-associate them with the parent contact.

 

DemoHook.php

<?php
class DemoHook
{
// Called from our definition in the logic_hooks file
public function processQuery($bean, $event, $args)
    {
if (!empty($args['fields']) && !in_array('top_opp',$args['fields'])) {
// They don't want the top_opp, don't process the query
return;
        }
if (empty($args['beans'])) {
// There are no results to this query, don't need to process it
return;
        }

// Same fields as formatForApi
static $oppFields = array(
'id',
'name',
'currency_id',
'amount',
'date_closed',
        );

// The beans array is keyed by the bean id
$beanIds = array_keys($args['beans']);

// Get a seed for Opportunities
$oppSeed = BeanFactory::newBean('Opportunities');
$q = new SugarQuery();
// Set the from bean
$q->from($oppSeed);
// Join in using the 'contacts' link field
$contactJoin = $q->join('contacts');
// Make sure to select the contact ID, so we can sort it out later
$q->select(array(array($contactJoin->joinName().'.id','contacts__id')));
// Fetch them by the bean id's
$q->where()->in($contactJoin->joinName().'.id', $beanIds);
$q->where()->notIn('sales_stage', array('Closed Lost', 'Closed Won'));
// Sort by the "usdollar" amount, the normal amount field could be in
// different currencies and so the sorting will be all wrong.
$q->orderBy('amount_usdollar DESC');
// Just get the top opportunity
$q->groupBy($contactJoin->joinName().'.id');

// Use fetchFromQuery and pass in the fields we care about
$opps = $oppSeed->fetchFromQuery($q, $oppFields, array('returnRawRows' => true));

// Raw rows are returned keyed by _rows, but we don't want to try and process
// that as a bean, so let's set them aside for later
$rows = $opps['_rows'];
unset($opps['_rows']);

// Loop through the opportunities and with the help of the raw rows link them to their contacts
foreach ($opps as $oppId => $opp) {
$contactId = $rows[$oppId]['contacts__id'];
$args['beans'][$contactId]->top_opp = $opp;
        }
    }
}

 

Don't need to put in a request/response here because with all of that added work everything looks the same just now we don't run a bunch of queries on a list view, only one.

 

7. So you really have to override an endpoint.

 

Some days all of the logic hooks in the world aren't enough. I've tried to give you a lot of very simple and supported methods to override our API but if they just aren't enough for you then you can always override the endpoint. Be cautious and consult an expert before overriding an endpoint, take a stroll and think about it, and just know that we may add functionality to an endpoint that you aren't expecting so you may break things on a normal update.

 

Didn't scare you off and you still want to add an endpoint? Okay then, here you go. Overriding an endpoint is very similar to creating a new endpoint so start by following the directions there. Don't worry about your path conflicting with another path in the base app, the REST lookup code prefers endpoints registered in the custom directory, you will notice that two identical endpoints registered for the same path will appear in /help, but the one with the highest score wins. If you want to live in the danger zone and override an endpoint so that the REST API will match a less-specific path you can add bonus score by putting a value in the option extraScore in your registerApiRest() array for that endpoint. In our example here we will register a new endpoint living on top of the out of the box /Contacts/:record endpoint. Let's create this class in the file custom/modules/Contacts/clients/base/api/FavoriteBurgerApi.php making sure that the filename matches the class name. In our override class we will also extend the ModuleApi because that is what normally handles these requests. In our overridden retrieveRecord() method we call the parent method so it can continue doing the things it normally does and after that is done we just go in and manipulate the returned data before we send it back. Be sure to run a quick repair and rebuild and check the /help to make sure the REST API picked up your newly created endpoint, it helps to make the short help something unique so it's easier to spot.

 

FavoriteBurgerApi.php

<?php

require_once('clients/base/api/ModuleApi.php');

class FavoriteBurgerApi extends ModuleApi
{
static public $favoriteBurgers = array(
"Poutine on the Ritz Burger",
"Mesclun Around Burger",
"The Don't Get Creme Fraiche With Me Burger",
"Onion-tended Consequences Burger",
"Bruschetta Bout It Burger",
"MediterrAin't Misbehavin' Burger",
"I'm Gonna Get You Succotash Burger",
"Every Breath You Tikka Masala Burger",
    );

public function registerApiRest()
    {
return array(
'retrieve' => array(
'reqType' => 'GET',
'path' => array('Contacts','?'),
'pathVars' => array('module','record'),
'method' => 'retrieveRecord',
'shortHelp' => 'Returns a single record, with their favorite burger attached',
'longHelp' => 'include/api/help/module_record_get_help.html',
            ),
        );
    }

public function retrieveRecord($api, $args)
    {
// Have the moduleApi do the hard work
$data = parent::retrieveRecord($api, $args);

// People don't remember what burger they tried so just give them a random favorite
$burgerNum = rand(1,count(self::$favoriteBurgers)) - 1;
$data['favorite_burger'] = self::$favoriteBurgers[$burgerNum];

// Return the modified data
return $data;
    }
}

 

As you can see in the sample request we have now added a "favorite_burger" field to the returned data.

 

fourth_response.json

# GET /rest/v10/Contacts/f23e19e3-a59e-8985-4202-5327156a36a9?fields=id,name
{
"id": "f23e19e3-a59e-8985-4202-5327156a36a9",
"name": "Kuchi Kopi",
"date_modified": "2014-03-19T10:04:51-06:00",
"_acl": {
"fields": {}
    },
"_module": "Contacts",
"favorite_burger": "Bruschetta Bout It Burger"
}

 

8. And that's all

 

While there are many more ways to manipulate the data inside of Sugar, these are the new ways to manipulate the data directly relating to the REST API that all non-backwards compatible code runs through in newer versions of Sugar. I hope this gives you all a good idea on where you can start getting your customizations in for your individual implementations of SugarCRM.

Outcomes