Creating A Custom Connector

Overview

Describes all the various files needed when creating a connector. A downloadable example is attached to this article.

Connectors

The minimal files a connector should provide are a config file (config.php), a variable definition file (vardefs.php), a language file (<language>.lang.php) and a connector source file (<connector name>.php). An optional default mapping file that associates your connector’s fields with fields in Sugar’s modules may be provided (mapping.php). You will also need to choose a connector protocol type of REST or SOAP. This will be reflected in the directory the connector resides in of ‘rest’ or ‘soap’.
Your directory should look like this:
  • ./custom/modules/Connectors/connectors/sources/ext/<protocol type>/example/source/<connector name>.php
  • ./custom/modules/Connectors/connectors/sources/ext/<protocol type>/example/source/vardefs.php
  • ../custom/modules/Connectors/connectors/sources/ext/<protocol type>/example/config.php
  • ./custom/modules/Connectors/connectors/sources/ext/<protocol type>/example/language/<lang>.lang.php
    • (default language file for localization)
  • ./custom/modules/Connectors/connectors/sources/ext/<protocol type>/example/source/mapping.php
    • (optional)

Protocol Type

The first step is to determine the connector protocol type to create the connector source file. Currently the two Web Services protocols supported are REST and SOAP. If the web service is a REST protocol, then the connector should extend the ext_rest class defined in the file ./include/connectors/sources/ext/rest/rest.php. The class name should contain the class name ext_rest_<suffix>.
<?php
     require_once('include/connectors/sources/ext/rest/rest.php');
     class ext_rest_example extends ext_rest
     {
        
     }
?>
If the web service is a SOAP protocol, then the connector should extend the ext_soap class defined in the file ./include/connectors/sources/ext/soap/soap.php. The class name should contain the class name ext_soap_<suffix>
<?php
     require_once('include/connectors/sources/ext/soap/soap.php');
     class ext_soap_example extends ext_soap
     {
        
     }
?>

Source

The next step in creating a connector is to create the source. This example connector will be created as a REST connector in the directory ./custom/modules/Connectors/connectors/sources/ext/rest/example/ that extends./include/connectors/sources/ext/rest/rest.php.

Connector Class (<connector name>.php)

This connector class will be named ‘ext_rest_example’. This class will reside in the file ‘example.php’ and will provide implementations for getItem() and getList() methods. These are the two methods that a connector must override and provide an implementation for. They are called by the component class (include/connectors/component.php). The getList() method as its name suggests, returns a list of results for a given set of search criteria that your connector can handle. The getItem() method will return a single connector record. For example, if your connector is a person lookup service, the getList() method may return matching person values based on a first and last name search. The getItem() method should return values for a unique person. Your service may uniquely identify a person based on an internal id or perhaps an email address.

getList()

The getList() method accepts two arguments. $args is an Array of argument values and $module is a String value of the module that the connector framework is interacting with. The getList() method should return a multi-dimensional Array with each record’s unique id as the key. The value should be another Array of key/value pairs where the keys are the field names as defined in the vardefs.php file.
public function getList($args=array(), $module=null)
{
    $results = array();
    if(!empty($args['name']['last']) && strtolower($args['name']['last']) == 'doe')
    {
        $results[1] = array('
            id' => 1,
            'firstname' => 'John',
            'lastname' => 'Doe',
            'website' => 'www.johndoe.com',
            'state' => 'CA'
        );
       
        $results[2] = array(
            'id' => 2,
            'firstname' => 'Jane',
            'lastname' => 'Doe',
            'website' => 'www.janedoe.com',
            'state' => 'HI'
        );
    }
   
    return $results;
}

getItem()

The getItem() method also accepts two arguments. $args is an Array of argument values and $module is a String value of the module that the connector framework is interacting with. The getItem() method will be called with a unique id as defined in the getList() method’s results.
public function getItem($args=array(), $module=null)
{
    $result = null;
    if ($args['id'] == 1)
    {
        $result = array();
        $result['id'] = '1'; //Unique record identifier
        $result['firstname'] = 'John';
        $result['lastname'] = 'Doe';
        $result['website'] = 'http://www.johndoe.com';
        $result['state'] = 'CA';
    }
    else if ($args['id'] == 2)
    {
        $result = array();
        $result['id'] = '2'; //Unique record identifier
        $result['firstname'] = 'Jane';
        $result['lastname'] = 'Doe';
        $result['website'] = 'http://www.janedoe.com';
        $result['state'] = 'HI';
    }
  
    return $result;
}

test()

This is an optional step where you may wish to provide functionality for your connector so that it may be tested through the administration interface under the "Set Connector Properties" section. To enable testing for your connector, set the connector class variable _has_testing_enabled to true in the constructor and provide a test () method implementation.
public function __construct()
{
    parent::__construct();
    $this->_has_testing_enabled = true;
}

public function test()
{
    $item = $this->getItem(array('id'=>'1'));
    return !empty($item['firstname']) && ($item['firstname'] == 'John');
}
You should also note that to enable the formatter hover component, you will need to also add the following to the construct:
$this->_enable_in_hover = true;

Class Example

The full ’ext_rest_example’ class is shown below:
./custom/modules/Connectors/connectors/sources/ext/rest/example/example.php
<?php

    if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

    require_once('include/connectors/sources/ext/rest/rest.php');

    class ext_rest_example extends ext_rest
    {
        public function __construct()
        {
            parent::__construct();

            //Used for testing
            $this->_has_testing_enabled = true;

            //Used to enable hover for the formatter
            $this->_enable_in_hover = true;
        }

        /**
         * test
         * Returns true or false to test criteria
         *
         * @return Boolean true if test completes successfully
         */
        public function test()
        {
            $item = $this->getItem(array('id'=>'1'));
            return !empty($item['firstname']) && ($item['firstname'] == 'John');
        }

        /**
         * getItem
         * Returns an array containing a key/value pair(s) of a source record
         *
         * @param Array $args Array of arguments to search/filter by
         * @param String $module String optional value of the module that the connector framework is attempting to map to
         * @return Array of key/value pair(s) of the source record; empty Array if no results are found
         */
        public function getItem($args=array(), $module=null)
        {
            $result = null;
            if ($args['id'] == 1)
            {
                $result = array();
                $result['id'] = '1'; //Unique record identifier
                $result['firstname'] = 'John';
                $result['lastname'] = 'Doe';
                $result['email'] = 'john.doe@sugar.crm';
                $result['state'] = 'CA';
            }
            else if ($args['id'] == 2)
            {
                $result = array();
                $result['id'] = '2'; //Unique record identifier
                $result['firstname'] = 'Jane';
                $result['lastname'] = 'Doe';
                $result['email'] = 'jane.doe@sugar.crm';
                $result['state'] = 'HI';
            }

            return $result;
        }

        /**
         * getList
         * Returns a nested array containing a key/value pair(s) of a source record
         *
         * @param Array $args Array of arguments to search/filter by
         * @param String $module String optional value of the module that the connector framework is attempting to map to
         * @return Array of key/value pair(s) of source record; empty Array if no results are found
         */
        public function getList($args=array(), $module=null)
        {
            $results = array();
            if(!empty($args['name']['last']) && strtolower($args['name']['last']) == 'doe')
            {
                $results[1] = array('
                    id' => 1,
                    'firstname' => 'John',
                    'lastname' => 'Doe',
                    'email' => 'john.doe@sugar.crm',
                    'state' => 'CA'
                );

                $results[2] = array(
                    'id' => 2,
                    'firstname' => 'Jane',
                    'lastname' => 'Doe',
                    'email' => 'john.doe@sugar.crm',
                    'state' => 'HI'
                );
            }

            return $results;
        }
    }

?>

Vardefs (vardefs.php)

The next step is to provide a list of variable definitions that your connector uses. These should be the fields used by your connector. For example, if your connector is a person lookup service, then these fields may be a first and last name, email address and phone number. You must also provide a unique field identifier for each connector record. This unique field needs to be named "id". Each vardef field entry is a defined within a PHP Array variable. The syntax is similar to a Sugar module’s vardefs.php file with the exception of the ‘hidden’, ‘input’, ‘search’, ‘hover’ and ‘options’ keys.
  • The ’hidden’ key/value parameter is used to hide the connector’s field in the framework. The required "id" field should be declared hidden because this is a unique record identifier that is used internally by the connector framework.
  • The ’search’ key/value parameter is an optional entry used to specify which connector field(s) are searchable. In step 1 of the connector wizard screen, a search form will be generated for your connector so that the user may optionally refine the list of results shown. Currently, we do not filter the fields that may be added to the search form so the use of the ‘search’ key/value parameter serves more as a visual indication.
  • The ’hover’ key/value parameter is an optional entry to support the hover functionality. A vardef field that is denoted as the hover field contains the value the hover code will use in displaying a popup that displays additional detail in the Detail Views.
  • The ’options’ key/value parameter allows the developer to map values returned by the connector to values that may be used by the Sugar database.
In our example, the state field returns the abbreviated value of the State (CA for California, HI for Hawaii, etc.). If we wish to use the State name instead of the abbreviated name, we may specify the ‘options’ key/value parameter and then add the mapping entry to enable this translation. This is especially helpful should your system depend on a predefined set of values that differ from those returned by the connector. Here is a complete example of our example connector’s vardefs.php file:
<?php

    if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

    $dictionary['ext_rest_example'] = array(
        'comment' => 'vardefs for example connector',
        'fields' => array (
            'id' => array (
                'name' => 'id',
                'vname' => 'LBL_ID',
                'type' => 'id',
                'comment' => 'Unique identifier',
                'hidden' => true,
            ),
            'fullname' => array (
                'name' => 'fullname',
                'vname' => 'LBL_FULL_NAME',
                'hover' => true,
            ),
            'firstname' => array (
                'name' => 'firstname',
                'vname' => 'LBL_FIRST_NAME',
            ),
            'lastname' => array (
                'name' => 'lastname',
                'vname' => 'LBL_LAST_NAME',
                'input' => 'lastname',
                'search' => true,
            ),
            'email' => array (
                'name' => 'email',
                'vname' => 'LBL_EMAIL',
            ),
            'state' => array (
                'name' => 'state',
                'vname' => 'LBL_STATE',
                'options' => 'states_dom',
            ),
        )
    );

?>

Things to Note

The ‘input’ key/value parameter is an optional entry to provide an input argument name conversion. In other words, if your connector service expects a "firstName" argument but your connector’s field is named "firstname", you may provide an additional input entry so that the connector framework will call your connector’s getItem() and getList() methods with an argument named "firstName". Typically, the ‘input’ key/value parameter is used to support searching.
'lastname' => array (
     'name' => 'lastname',
     'vname' => 'LBL_LAST_NAME',
     'input' => 'lastName',
     'search' => true,
     'hover' => 'true',
),
Although the benefit of this is probably not captured well in this example, you could potentially have a service that groups arguments in a nested array. For example imagine the following array of arguments for a connector service:
$args['name']['first']
$args['name']['last']
$args['phone']['mobile']
$args['phone']['office']
Here we have an array with ‘name’ and ‘phone’ indexes in the first level. If your connector expects arguments in this format then you may supply an input key/value entry in your vardefs.php file to do this conversion. The input key value should be delimited with a period (.).
'lastname' => array (
     'name' => 'lastname',
     'vname' => 'LBL_LAST_NAME',
     'input' => 'name.last', // Creates Array argument ['name']['last']
     'search' => true,
     'hover' => 'true',
),

Configuration (config.php)

The configuration file (config.php) holds a PHP array with two keys. The "name" is used to provide a naming label for your connector that will appear in the tabs throughout the application. The "properties" key may be used to store runtime properties for your connector. Here we have simply provided the name "Example" and a properties value that we may use to control the various aspects of our connector such as max results.
./custom/modules/Connectors/connectors/sources/ext/rest/example/config.php
<?php
/***CONNECTOR SOURCE***/
$config['name'] = 'Example';
$config['properties']['max_results'] = 50;

Labels (<lang>.lang.php)

The next step is to create a language file with labels for your application. Notice that the properties defined in the config.php file are indexed by the property key ("max_results"). Otherwise, the vardefs.php entries should be indexed off the "vname" values.
./custom/modules/Connectors/connectors/sources/ext/rest/example/language/en_us.lang.php.
<?php

    if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

    $connector_strings = array (
        //Vardef labels
        'LBL_LICENSING_INFO' => '<table border="0" cellspacing="1"><tr><td valign="top" width="35%" class="dataLabel">Licensing Info</td></tr></table>',
        'LBL_FULL_NAME' => 'Full Name',
        'LBL_FIRST_NAME' => 'First Name',
        'LBL_LAST_NAME' => 'Last Name',
        'LBL_EMAIL' => 'Email Address',
        'LBL_STATE' => 'State',

        //Configuration labels
        'max_results' => 'Max Results',
    );

?>

Mappings (mapping.php)

An optional mapping.php file may be provided so that default mappings are defined. These mappings assist the connector framework’s component class. In our vardefs.php file, we enabled the hover link for the full_name field. To explicitly place this hover link on the full_name field for the Contacts module we provide the mapping entry as follows. A mapping.php file is needed though if you use the ‘options’ attribute for entries in the vardefs.php file.
<?php

$mapping = array (
  'beans' =>
  array (
    'Contacts' =>
    array (
      'firstname' => '',
      'lastname' => '',
      'email' => '',
      'state' => '',
      'fullname' => '',
      'id' => '',
    ),
  ),
);
Note that you can also predefine the mapping for the user as shown below:
<?php

$mapping = array (
  'beans' =>
  array (
    'Contacts' =>
    array (
      'firstname' => 'first_name',
      'lastname' => 'last_name',
      'email' => 'email1',
      'state' => 'primary_address_state',
      'fullname' => 'full_name',
      'id' => 'id',
    ),
  ),
);

Formatter

The optional formatter components are used by the connector framework to render a widget that may display additional details and information. Currently, they are shown in the detail view screens for modules that are enabled for the connector. You should note that formatters should reside in a directory structure similar to their source connectors involving the protocol type. An example of this structure is ./custom/modules/Connectors/connectors/formatters/ext/<protocol type>/<connector name>/
The first step in creating a formatter is to enable hover in its source class. For this example, we will need to make sure that the contruct of our source class (./custom/modules/Connectors/connectors/sources/ext/rest/example/example.php) has _enable_in_hover set to true:
$this->_enable_in_hover = true;
Next, we will create our formatter class. Our formatter class, ext_rest_example_formatter, will handle how the hover widget is displayed.
./custom/modules/Connectors/connectors/formatters/ext/rest/example/example.php
<?php

    if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

    require_once('include/connectors/formatters/default/formatter.php');

    class ext_rest_example_formatter extends default_formatter
    {

        public function getDetailViewFormat()
        {
           $mapping = $this->getSourceMapping();
           $mapping_name = !empty($mapping['beans'][$this->_module]['fullname']) ? $mapping['beans'][$this->_module]['fullname'] : '';

           if(!empty($mapping_name))
           {
               $this->_ss->assign('mapping_name', $mapping_name);
               return $this->fetchSmarty();
           }

           $GLOBALS['log']->error($GLOBALS['app_strings']['ERR_MISSING_MAPPING_ENTRY_FORM_MODULE']);
           return '';
        }

        public function getIconFilePath()
        {
           //icon for display
           return 'themes/Sugar/images/icon_basic_32.png';
        }

    }

?>
Next we will define our display template.
./custom/modules/Connectors/connectors/formatters/ext/rest/example/tpls/display.tpl
<script type="text/javascript" src="{sugar_getjspath file='include/connectors/formatters/default/company_detail.js'}"></script>

{literal}
<style type="text/css">
    .yui-panel .hd {
        background-color:#3D77CB;
        border-color:#FFFFFF #FFFFFF #000000;
        border-style:solid;
        border-width:1px;
        color:#000000;
        font-size:100%;
        font-weight:bold;
        line-height:100%;
        padding:4px;
        white-space:nowrap;
    }
</style>
{/literal}

<script type="text/javascript">
    function show_ext_rest_example(event)
    {literal}
    {
        var xCoordinate = event.clientX;
        var yCoordinate = event.clientY;
        var isIE = document.all?true:false;

        if(isIE) {
            xCoordinate = xCoordinate + document.body.scrollLeft;
            yCoordinate = yCoordinate + document.body.scrollTop;
        }

    {/literal}

        cd = new CompanyDetailsDialog("example_popup_div", '<div id="example_div">Connector Content</div>', xCoordinate, yCoordinate);
        cd.setHeader("{$fields.{{$mapping_name}}.value}");
        cd.display();

    {literal}
    }
    {/literal}
</script>

Building a Module Loadable Package

The final step of a connector would be to zip your connector’s contents along with a manifest.php file so that it may be installed by the Module Loader.
<basepath>/manifest.php
<?php

     $manifest = array (
         'acceptable_sugar_flavors' => array (),
         'acceptable_sugar_versions' => array(),
         'is_uninstallable' => true,
         'name' => 'Example Connector',
         'description' => 'A connector example',
         'author' => 'SugarCRM',
         'published_date' => '2013/02/12',
         'version' => '1',
         'type' => 'module',
         'icon' => '',
     );
     
     $installdefs = array (
         'id' => 'ext_rest_example',
         'connectors' => array (
             array (
                 'connector' => '<basepath>/example/source',
                 'formatter' => '<basepath>/example/formatter',
                 'name' => 'ext_rest_example',
             ),
         ),
     );
?>
This manifest will install the specified files in the paths for ‘connector’ and ‘formatter’ as a connector. The path for the connector is determined from the connector ‘name’ index. In this case, with a name of ext_rest_example, the sources will be installed to ./custom/modules/Connectors/connectors/sources/ext/rest/example/ and the formatters to ./custom/modules/Connectors/connectors/formatters/ext/rest/example/.
A full module loadable example is available for download here.