:: Version 2.7.0 ::

Calculated field & Cascading update

Prerequisite: You must be familiar with the Syntax used in Tutorials and have already created an extension.

learning:
Update an object based on related objects
level:
Advanced
domains:
PHP, Automation
min version:
2.3.0

Assuming you want to define a “calculated” field on Class A, which would depend on data defined on class(es) B object(s), something more complex than just an ExternalField. For examples:

  1. On a Ticket, you want the time spent on all of its WorkOrders
  2. On a Ticket, you want the count of manually added CIs.
  3. On FunctionalCI, you want a Location field, which would be simply the location for PhysicalDevice, and a computed location on Logical Device, based on the location of the Physical device on which the Logical is running.

You want to be able to search on that “calculated” field.
You want to be able to run audit against that “calculated” field.

Note that such “calculated” field induces risks on performance while updating large number of Class B objects

Generic Strategy

Here is the generic strategy to implement such “calculated” field.

  1. To be able to query on it, we must store it in database, so make it a persistent field.
  2. In order to ensure that it is always accurate, you need to identify when it must be recomputed and do it. For this we will define/overwrite those functions:

On Class A

  • ComputeValues(): calculate the “calculated” field value based on other fields / related objects.
  • GetInitialStateAttributeFlags(): to specify that the “calculated” field is hidden in creation form
  • GetAttributeFlag(): to specify that the “calculated” field is read-only on modification

On depending Classes B

  • OnUpdate(), OnDelete(): Check if a field has been modified which would impact a “calculated” field of Class A, set an internal variable to remember it within the AfterUpdate() / AfterDelete().
  • AfterInsert,AfterUpdate(),AfterDelete: If needed (based on the internal variable set above), force recomputation on all objects A which have a “calculated” field which are impacted by that change.

Tips & explanations

  • We use AfterXXX() to cascade the change, and not OnXXX(), because otherwise the remote object would not yet be saved in database, so the computation based on ComputeValues would use the inaccurate data found in database.
  • We use OnUpdate() to check if we need to cascade the change, because after update/delete the array $this→ListChanges() is empty.
  • ComputeValues() is called quite often, but not when loading the object in memory.

Use Case: Sum

In this use case, we want to Sum on a Ticket the timespent on all WorkOrders of this Ticket.

  • First we need to add a time_spent integer attribute on WorkOrder and Ticket classes
  • In order to ensure that Ticket::time_spent is always accurate, we need to identify when it must be recomputed:
    • When the Ticket is updated
    • When a WorkOrder is created → we need to recompute its Ticket
    • When a WorkOrder is updated → we need to recompute its former and its new Ticket
    • When a WorkOrder is deleted → we need to recompute its former Ticket
  • Then define/overwrite those functions:

On Class Ticket

  • ComputeTimeSpent(): calculate the “time_spent” field value based on “time_spent” of WorOrders.
  • OnUpdate(): the list of Workorders cannot be modified from a Ticket in the default datamodel, but if this is enable with the edit mode then this function is needed.
  • GetInitialStateAttributeFlags(): to specify that the “calculated” field is hidden on Ticket creation form
  • GetAttributeFlag(): to specify that the “calculated” field is read-only on Ticket modification
class::Ticket
protected function ComputeTimeSpent()
{
   $iSum = 0;
   $oWorkOrderSet = $this->Get('workorders_list');
   while($oWorkOrder = $oWorkOrderSet->Fetch())
   {
        $iSum += $oWorkOrder->Get('time_spent');
   }
   $this->Set('time_spent', $iSum);
}
 
protected function OnUpdate()
{
  $aChanges = $this->ListChanges();
  if (array_key_exists('workorders_list', $aChanges))
  {
      $this->ComputeTimeSpent();
  }
}
public function GetInitialStateAttributeFlags($sAttCode, &$aReasons = array())
{
   // Hide the calculated field in object creation form
   if (($sAttCode == 'time_spent'))
         return(OPT_ATT_HIDDEN | parent::GetInitialStateAttributeFlags($sAttCode, $aReasons));
   return parent::GetInitialStateAttributeFlags($sAttCode, $aReasons);
}
 
public function GetAttributeFlags($sAttCode, &$aReasons = array(), $sTargetState = '')
{
   // Force the computed field to be read-only, preventing it to be written
   if (($sAttCode == 'time_spent'))
         return(OPT_ATT_READONLY | parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState));
   return parent::GetAttributeFlags($sAttCode, $aReasons, $sTargetState);
}

On Class WorkOrder

  • OnUpdate, OnDelete: If field ticket_id has been modified then store temporarily the previous value.
  • AfterInsert,AfterUpdate,AfterDelete: Force recomputation of current and former Ticket.
class::WorkOrder
protected function UpdateTicket($id)
{
   if ($id != 0) 
   {
      $oObject = MetaModel::GetObject('Ticket', $id, false);
      // FYI: MetaModel::GetObject('Ticket', 0); generates a FatalError
      if (is_object($oObject))  // in case the user is not allowed to see the object
      {
         $oObject->ComputeTimeSpent();
         $oObject->DBUpdate();
      }
   }              
}
 
protected function OnUpdate()
{
  $aChanges = $this->ListChanges();
  if (array_key_exists('ticket_id', $aChanges))
  {
      // store in the WorkOrder memory object the previous value
      $this->iPreviousTicketId = $this->GetOriginal('ticket_id');
  }
  if (array_key_exists('time_spent', $aChanges))
  {
      // record in the WorkOrder memory object that time spent was changed
      $this->bTimeChanged = true;
  }
}
 
protected function AfterUpdate()
{
   // The WorkOrder is updated in DB and Time spent was changed,
   if (isset($this->bTimeChanged))
   {
       // we need to recompute TimeSpent on the Ticket
       $this->UpdateTicket($this->Get('ticket_id'));
   }
   // If there was a "former" Ticket then we also need to update it
   if (isset($this->iPreviousTicketId )) $this->UpdateTicket($this->iPreviousTicketId);
}
 
protected function AfterInsert()
{
   // A new Workorder was created with time_spent, so let's recompute the Ticket 
   if ($this->Get('time_spent') > 0)   $this->UpdateTicket($this->Get('ticket_id'));
}
 
protected function OnDelete()
{
   // If the deleted Workorder had some time spent set, 
   // then let's flag that a recomputation is needed, and store the former Ticket id for later use
   if ($this->GetOriginal('time_spent') > 0)   
   {
      $this->iPreviousTicketId = $this->GetOriginal('ticket_id');
   }
}
 
protected function AfterDelete()
{
   // If a recomputation of the former Ticket is needed, let's do it
   if (isset($this->iPreviousTicketId )) $this->UpdateTicket($this->iPreviousTicketId);
}
This example was improved by checking if the WorkOrder::time_spent was modified in OnXXXX() function, then set a flag and use it in AfterXXX() to call UpdateTicket() only when truly needed
2_7_0/customization/cascade-update.txt · Last modified: 2020/04/15 15:23 (external edit)
Back to top
Contact us