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();
}
}
}
}
Related Record Save Loops
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();
}
}
}
}
}