Pages

Tuesday, October 11, 2011

SugarCRM Logic Hooks: Unintended Loops


There is something intriguing about working with logic hooks. Through their use, one can accomplish quite a bit given that the only real limitation is our ability to write PHP code. 

For example, we can use a logic hook to modify a value on a record that is being saved. We can also use it to connect to a web service and process data whenever a record is retrieved in SugarCRM. In addition, we can leverage the SugarCRM framework and add records in other modules, such as automatically adding a call upon the creation of a new Lead record.

Our possibilities are virtually endless, but at the same time, this power can sometimes present some odd problems that are difficult to troubleshoot or resolve. 

One such matter relates to the duplication of data, usually the result of what appears to be an unintended loop within the code executed by the logic hook. On the surface, this sounds simple enough to troubleshoot, but looks can be deceiving.

Lets take a look a code snippet which we will assume is being executed by our logic hook:

class LogicHookTest {
        function addContact(&$bean, $event, $arguments)
        {
             //Grab contact ID value stored in custom field on current record
             $contact_id = $bean->contact_id_c;         

             require_once('modules/Contacts/Contact.php');
             $contact = new Contact();

             //Retrieve Contact record with matching ID value
             $contact->retrieve($contact_id); 
             //Set value on Contact's custom field 
             $contact->custom_field_c = 'New Value';  
             $contact->save();
        }
}

To elaborate, the code is grabbing a value from the contact_id_c field of the record currently being accessed in SugarCRM and assigning it to the $contact_id variable. For the purposes of the example we will assume the record being accessed is in a custom module.

Next, the code instantiates a Contact object and uses that same $contact_id value to retrieve the record with the matching ID value from the Contacts module. Lastly, a custom field named custom_field_c is modified on that Contact record and then saved. 

In summary, the logic hook retrieves a record from the Contacts module and modifies a field on that record, without us actually needing to be in the Contacts module.

Assuming the referenced custom fields existed, the above code would work. However, this may not always yield the desired results, which leads us into the problem at the heart of this post.


There are occasions under which the above code causes SugarCRM to spiral into a loop, and rather than updating a record, numerous new records of the same type are created. In this case it could end up creating numerous duplicate contacts. The exact number of duplicates it creates tends to vary and there does not appear to be a reliable method for calculating it.

As odd as that may sound, the real oddity is in the fact that there are not any loops in the logic hook code being executed, at least not the one we wrote. So why does it spiral out of control?

It turns out that this one line in particular is usually at fault in these scenarios:

$contact->retrieve($contact_id);

The above line of code is responsible for retrieving the record from the desired module. It requires that one provide it the ID value of the desired record in order to determine which record should be fetched. In our example, the ID value is contained in the $contact_id variable.

Should we fail to populate that variable with a value, the process fails and spins out of control into the loop. A good example of this would be failing to populate the contact_id_c custom field on the custom module record and then causing the logic hook to execute.

Another scenario that leads to the loop problem is one where an ID value is provided, but the record is not found in the corresponding module, in this case, Contacts. To further clarify, the ID may be valid and conform to the SugarCRM standards, but it is not one that belongs to a record from the module being searched.

So, if you are working with logic hooks and interacting with other SugarCRM objects and encounter an odd loop such as the one I described, verify that the ID values being passed to the retrieve() method are valid and correspond with the target module. 

8 comments:

  1. Angel,

    I always use isset to check for null values prior to retrieving them. That will take care of one of the two issues above.

    ReplyDelete
  2. Yes, good suggestion. That's kind of the point of the article, i.e. make sure the variable you pass to $bean->retrieve() is populated.

    ReplyDelete
  3. The is one of the good programming techniques.A good programmer will always take time to go through API once before implementing it. Any fool can write a code that a computer can understand , but only a good programmer will write a code that a human can understand.

    ReplyDelete
  4. Hola Angel,
    este comentario no tiene que ver con este post. Solo quería darte las gracias. Me estaba volviendo loco un bug de Infohand, que es un Sugar, "Your session was terminated due to a significant change in your IP address", y lo he solucionado gracias a uno de tus post. Este post se llevo a tu blog. Asi que gracias y un saludo.

    ReplyDelete
  5. Muchas gracias por su comentario. Me alegra mucho que le pude ayudar en una manera u otra.

    ReplyDelete
  6. Angel, I am experimenting some kind of loop in a logic hook. It seems that when I use an after_retrieve hook, where I execute a $bean->save(), this is calling another hook (after save or before save), that then force a retrieve...then PHP goes off by timeout. The called bean element is there.
    The challenge seems to be, to avoid this. What do you think, instead of executing a bean->save() should i use an update/set in the database?

    ReplyDelete
    Replies
    1. This approach worked fine, and get rid of the loop:

      class marcarVistoOColorearLista{
      function marcarVisto(&$bean,$event,$arguments=""){
      if($GLOBALS['action'] == 'detail' || $GLOBALS['action'] == 'DetailView'){
      if($_SESSION['authenticated_user_id'] == $bean->assigned_user_id){
      $query1 = strtolower($GLOBALS['module']).'_cstm';
      $query = 'UPDATE '.$query1.' SET registrovisto_c = 1 WHERE id_c = "'.$bean->id.'"';
      $results = $bean->db->query($query, true);
      /* database update instead of $bean->save()
      if(isset($bean->registrovisto_c)){
      if(!$bean->registrovisto_c){
      $bean->registrovisto_c = 1;
      $bean->save();
      }
      }*/
      }
      }
      }

      }

      Delete
    2. Hola Daniel,

      I've had to the same in the past as well, to avoid the endless loop caused by $bean->save().

      It shouldn't cause any problems, so long as schema changes don't affect the query.

      Delete

Your comments, feedback and suggestions are welcome, but please refrain from using offensive language and/or berating others. Thank you in advance.