Extending the SugarCRM API: Updating Dropdowns

v4 of the Sugar API lets you do a lot of cool stuff. But it’s not always enough. Recently, I had to figure out a way to create or update dropdowns from outside of Sugar.

There are actually a couple different ways to do this: you could create a custom entry point, or add a custom bean on which you override the save method, or whatever crazy thing comes to mind.

Plus, if you really feel like it, and you’re not in Sugar OnDemand (OD), you can edit the core classes directly. But that’s dirty.

I already had a lot of code oriented around calling the API (authorization and such), so extending the existing Sugar API turned out to be the simplest way to handle it.

When you extend the API, you’ll be deriving from an existing API. What this means is that although you’ll be hitting a custom endpoint, all the normal functionality you’re used to will be there.

Implementing the API method

Let’s implement our new API method first. For this, all we really have to do is derive from our parent API implementation class, and add our new method. In our case, we’re extending the v4 API, so we extend service/v4/SugarWebServiceImplv4.php:

<?php
require_once('service/v4/SugarWebServiceImplv4.php');
class SugarWebServiceImpl_v4_m extends SugarWebServiceImplv4 {

The name of this class is arbitrary. I just appended the exceptionally creative _m, for “modified”.

Then, we have to create our method:

    public function set_dropdown($session, $dropdown_name, $dropdown_language, 
        $list_value, $use_push = true) {

Note that the first argument to this method is $session. This is going to be passed in by the API framework. If we don’t check this, then this API method can be called without a valid login. Which is bad. Luckily Sugar gives us a little method to call to be sure we’re authenticated:

if(!self::$helperObject->checkSessionAndModuleAccess($session, '')) {
    return;
}

We don’t need to do anything if it fails. The framework will handle the error for us. That second parameter is a little funky. It’s actually the module we should be checking access levels for. Since dropdowns are stored in separate language files, we don’t really need to check for module access. If we give it a blank string, it’ll ignore that portion of the check.

The other parameters are those we need to actually set the dropdown value using the Sugar ParserDropDown class. I won’t get into the details of this, except to note one thing. We’re passing a JSON-encoded string in here for the dropdown values. You could actually use the name_value_list type already defined by the API, and not have to deal with a JSON-encoded string within a JSON-encoded string. But name_value_lists are a pain to deal with, so JSON it is.

Here’s the whole of the class:

<?php
//file: custom/service/v4_m/sugar_mas_impl.php
require_once('service/v4/SugarWebServiceImplv4.php');
class SugarWebServiceImpl_v4_m extends SugarWebServiceImplv4 {
    /**
     * Overwrite or create a dropdown.
     * @param string $session -- Authenticated session
     * @param string $dropdown_name -- Name of the dropdown
     * @param string $dropdown_language -- Language of the dropdown, e.g. 'en_us'
     * @param string $list_value -- Json-encoded hash of dropdown values.
     *               e.g., {"key1": "value1", "key2": "value2"}
     * @param bool use_push -- Push new values onto existing on true, replace all on false
     * @return bool true on success, false on failure.
     * @exception 'SoapFault' -- The SOAP error, if any
     */
    public function set_dropdown($session, $dropdown_name, $dropdown_language, $list_value, $use_push = true) {
        if(!(self::$helperObject->checkSessionAndModuleAccess($session, ''))) {
            return;
        }
        require_once('modules/ModuleBuilder/MB/ModuleBuilder.php');
        require_once('modules/ModuleBuilder/parsers/parser.dropdown.php');

        $parser = new ParserDropDown();
        $decoded_dropdown = json_decode($list_value);
        if(!$decoded_dropdown) return false;

        $flattened_dropdown = array();

        foreach($decoded_dropdown as $k => $v) {
            $flattened_dropdown[] = array($k, $v);
        }

        $dropdown = array(
            'dropdown_name' => $dropdown_name,
            'list_value' => json_encode($flattened_dropdown),
            'dropdown_lang' => $dropdown_language ? $dropdown_language : 'en_us',
            'view_package' => 'studio',
            'view_module' => '',
            'use_push' => $use_push
        );

        $parser->saveDropdown($dropdown);

        return true;
    }
}

Creating the registry

The purpose of the registry is just to … well, register functions as available in the API. The crux here is that you have to have a (protected) function called registerFunction. Since we’re only going to be creating a single method, with pretty vanilla parameters, this is very simple:

//file: custom/service/v4_m/sugar_mas_registry.php
<?php
require_once('service/v4/registry.php');
class sugar_mas_registry extends registry_v4 {
    public function __construct($serviceClass) {
        parent::__construct($serviceClass);
    }

    protected function registerFunction() {
        parent::registerFunction();
        $this->serviceClass->registerFunction(
            'set_dropdown', array(
                'session' => 'xsd:string', 
                'dropdown_name' => 'xsd:string',
                'dropdown_language' => 'xsd:string',
                'list_value' => 'xsd:string',
                'use_push' => 'xsd:bool',
            ),
            array('return' => 'xsd:bool')
        );
    }
}

The array here should (obviously?) mirror the signature of the method we defined in the implementation in step 1. Don’t forget the call to parent::registerFunction(), or you’ll lose all the functionality from the API you’re deriving from. Or it’ll crash, I dunno, I didn’t test it.

Note the xsd:string and xsd:bool parameters. Pretty self explanatory here, but if you’re feeling especially masochistic, you can register your own complex type. This would also happen in the registry. Look at service/v2/registry.php for examples (all the registerType calls).

Registering your new stuff

Now, we need to tell Sugar where all of our new classes are. For that, we look to service/v4/rest.php for inspiration:

<?php
//file: service/v4/rest.php
// Bunch of license stuff...
chdir('../..');
require_once('SugarWebServiceImplv4.php');
$webservice_class = 'SugarRestService';
$webservice_path = 'service/core/SugarRestService.php';
$webservice_impl_class = 'SugarWebServiceImplv4';
$registry_class = 'registry';
$location = '/service/v4/rest.php';
$registry_path = 'service/v4/registry.php';
require_once('service/core/webservice.php');

That chdir sticks out as a little scary, especially because it’s blacklisted in OD. But it turns out it’s not strictly necessary. Here’s what I came up with:

<?php
//file: custom/service/v4_m/rest.php
$webservice_class = 'SugarRestService';
$webservice_path = 'service/core/SugarRestService.php';
$webservice_impl_class = 'SugarWebServiceImpl_v4_m';
$webservice_impl_class_path = 'custom/service/v4_m/sugar_mas_impl.php';
$registry_class = 'sugar_mas_registry';
$registry_path = 'custom/service/v4_m/sugar_mas_registry.php';
$location = 'custom/service/v4_m/rest.php';
require_once('../../../service/core/webservice.php');

$webservice_class and $webservice_path remain the same, as we’re not overriding that portion of the API. $webservice_impl_class is the new implementation we defined in step 1. Note the new $webservice_impl_class_path that wasn’t defined before; now that we’re in custom territory, we have to tell Sugar precisely where to find our implementation. Same for $registry_class and $registry_path.

The require_once is a little weird now. Since this script (rest.php) is hit directly, the current working directory will be custom/service/v4_m. Rather than doing a chdir at the beginning of this thing (which we can’t do in OD), we just give it the path to the core webservice.php, relative to our current directory.

Assuming everything was done correctly, go to the new endpoint (custom/service/v4_m/rest.php) in a browser. You should see your new method right at the top:

Class [  class SugarWebServiceImpl_v4_m extends SugarWebServiceImplv4 ] {

  - Constants [0] {
  }

  - Static properties [1] {
    Property [ public static $helperObject ]
  }

  - Static methods [0] {
  }

  - Properties [0] {
  }

  - Methods [42] {
    /**
     * Overwrite or create a dropdown.
     * @param string $session -- Authenticated session
     * @param string $dropdown_name -- Name of the dropdown
     * @param string $dropdown_language -- Language of the dropdown, e.g. 'en_us'
     * @param string $list_value -- Json-encoded hash of dropdown values.
     *               e.g., {"key1": "value1", "key2": "value2"}
     * @param bool use_push -- Push new values onto existing on true, replace all on false
     * @return bool true on success, false on failure.
     * @exception 'SoapFault' -- The SOAP error, if any
     */
    Method [  public method set_dropdown ] {

      - Parameters [5] {
        Parameter #0 [  $session ]
        Parameter #1 [  $dropdown_name ]
        Parameter #2 [  $dropdown_language ]
        Parameter #3 [  $list_value ]
        Parameter #4 [  $use_push = true ]
      }
    }

From here on out, you will no longer be hitting

https://<sugar url>/service/v4/rest.php

. Instead, you’ll be going directly to

https://<sugar url>/custom/service/v4_m/rest.php

.

If you’re dealing with an on-site instance, you could just copy these files over. Of course, to put this in an OD instance, you’ll need to package it up for installation in the module loader. How to do that is beyond the scope of this post, but all you need to do is copy the files to the custom/service/v4_m directory for it to work.