:: Version 3.2.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:
-
On a Ticket, you want the time spent on all of its WorkOrders
-
On a Ticket, you want the count of manually added CIs.
-
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.
-
To be able to query on it, we must store it in database, so make it a persistent field.
-
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 -
GetAttributeFlags()
: 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 theAfterUpdate()
/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 notOnXXX()
, 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 theedit mode
then this function is needed. -
GetInitialStateAttributeFlags()
: to specify that the “calculated” field is hidden on Ticket creation form -
GetAttributeFlags()
: 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 fieldticket_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
3_2_0/customization/cascade-update.txt · Last modified:
2024/08/13 09:54 (external edit)