Let the platform do the work

Preventing Infinite Loops with Logic Hooks

Overview

Infinite loops often happen when a logic hook calls save on a bean in a scenario that triggers the same hook again. This example shows how to add a check to a logic hook to eliminate perpetual looping.

Saving in an After Save Hook

Infinite loops can sometimes happen when you have a need to update a record after it has been run through the workflow process in the after_save hook. Here is an example of a looping hook:

./custom/modules/Accounts/logic_hooks.php

  <?php

$hook_version = 1;
$hook_array = Array();
$hook_array['after_save'] = Array();
$hook_array['after_save'][] = Array(
    1,
    'Update Account Record',
    'custom/modules/Accounts/Accounts_Hook.php',
    'Accounts_Hook',
    'update_self'
);

./custom/modules/Accounts/Accounts_Hook.php

  <?php

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

class Accounts_Hook
{
    function update_self($bean, $event, $arguments)
    {
        //generic condition
        if ($bean->account_type == 'Analyst')
        {
            //update
            $bean->industry = 'Banking';
            $bean->save();
        }
    }
}

In the example above, there is a condition that, when met, will trigger the update of a field on the record. This will cause an infinite loop because calling the save() method will trigger once during the before_save hook and then again during the after_save hook. The solution to this problem is to add a new property on the bean to ignore any unneeded saves. For this example, we will name the property "ignore_update_c" and add a check to the logic hook to eliminate the perpetual loop. An example of this is shown below:

./custom/modules/Accounts/Accounts_Hook.php

  <?php

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

class Accounts_Hook
{
    function update_self($bean, $event, $arguments)
    {
        //loop prevention check
        if (!isset($bean->ignore_update_c) || $bean->ignore_update_c === false)
        {
            //generic condition
            if ($bean->account_type == 'Analyst')
            {
                //update
                $bean->ignore_update_c = true;
                $bean->industry = 'Banking';
                $bean->save();
            }
        }
    }
}

Sometimes logic hooks on two separate but related modules can cause an infinite loop. The problem arises when the two modules have logic hooks that update one another. This is often done when wanting to keep a field in sync between two modules. The example below will demonstrate this behavior on the accounts:contacts relationship:

./custom/modules/Accounts/logic_hooks.php

  <?php

$hook_version = 1;
$hook_array = Array();
$hook_array['before_save'] = Array();
$hook_array['before_save'][] = Array(
    1,
    'Handling associated Contacts records',
    'custom/modules/Accounts/Accounts_Hook.php',
    'Accounts_Hook',
    'update_contacts'
);

./custom/modules/Accounts/Accounts_Hook.php

  <?php

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

class Accounts_Hook
{
    function update_contacts($bean, $event, $arguments)
    {
        //if relationship is loaded
        if ($bean->load_relationship('contacts'))
        {
            //fetch related beans
            $relatedContacts = $bean->contacts->getBeans();

            foreach ($relatedContacts as $relatedContact)
            {
                //perform any other contact logic
                $relatedContact->save();
            }
        }
    }
}

The above example will loop through all contacts related to the account and save them. At this point, the save will then trigger our contacts logic hook shown below:

./custom/modules/Contacts/logic_hooks.php

  <?php

$hook_version = 1;
$hook_array = Array();
$hook_array['before_save'] = Array();
$hook_array['before_save'][] = Array(
    1,
    'Handling associated Accounts records',
    'custom/modules/Contacts/Contacts_Hook.php',
    'Contacts_Hook',
    'update_account'
);

./custom/modules/Contacts/Contacts_Hook.php

  <?php

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

class Contacts_Hook
{
    function update_account($bean, $event, $arguments)
    {

        //if relationship is loaded
        if ($bean->load_relationship('accounts'))
        {
            //fetch related beans
            $relatedAccounts = $bean->accounts->getBeans();

            $parentAccount = false;
            if (!empty($relatedAccounts))
            {
                //order the results
                reset($relatedAccounts);

                //first record in the list is the parent
                $parentAccount = current($relatedAccounts);
            }

            if ($parentAccount !== false && is_object($parentAccount))
            {
                //perform any other account logic
                $parentAccount->save();
            }
        }
    }
}

These two logic hooks will continuously trigger one another in this scenario until you run into a max_execution or memory_limit error. The solution to this problem is to add a new property on the bean to ignore any unneeded saves. In our example, we will name this property "ignore_update_c" and add a check to our logic hook to eliminate the perpetual loop. The following code snippets are copies of the two classes with the ignore_update_c property added.

./custom/modules/Accounts/Accounts_Hook.php

  <?php

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

class Accounts_Hook
{
    function update_contacts($bean, $event, $arguments)
    {
        //if relationship is loaded
        if ($bean->load_relationship('contacts'))
        {
            //fetch related beans
            $relatedContacts = $bean->contacts->getBeans();

            foreach ($relatedContacts as $relatedContact)
            {
                //check if the bean's ignore_update_c attribute is not set
                if (!isset($bean->ignore_update_c) || $bean->ignore_update_c === false)
                {
                    //set the ignore update attribute
                    $relatedContact->ignore_update_c = true;

                    //perform any other contact logic
                    $relatedContact->save();
                }
            }
        }
    }
}

./custom/modules/Contacts/Contacts_Hook.php

  <?php

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

class Contacts_Hook
{
    function update_account($bean, $event, $arguments)
    {
        //if relationship is loaded
        if ($bean->load_relationship('accounts'))
        {
            //fetch related beans
            $relatedAccounts = $bean->accounts->getBeans();

            $parentAccount = false;
            if (!empty($relatedAccounts))
            {
                //order the results
                reset($relatedAccounts);

                //first record in the list is the parent
                $parentAccount = current($relatedAccounts);
            }

            if ($parentAccount !== false && is_object($parentAccount))
            {
                //check if the bean's ignore_update_c element is set
                if (!isset($bean->ignore_update_c) || $bean->ignore_update_c === false)
                {
                    //set the ignore update attribute
                    $parentAccount->ignore_update_c = true;

                    //perform any other account logic
                    $parentAccount->save();
                }
            }
        }
    }
}