Sum Components cost on Contract
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.7.0
This example is another specific example of that tutorial Calculated field & Cascading update
Here we want to see in the details of an object A, the sum of objects B which are linked to the object A through a many-to-many relationship. In this example, we will sum on the Contract the cost of each Component included in that contract.
-
The
cost
is a field of the Component class (this particular class does not exist in the Standard Datamodel) -
We create a field
components_total_cost
on the Contract class, to store the sum of Componentcost
. -
We have a lnkContractToComponent class with a
contract_id
and acomponent_id
external keys, which store the many-to-many relationships. -
We have a
contracts_list
field on Component, providing the list of Contracts linked to that Component -
We have a
components_list
field on Contract, providing the list of Components linked to that Contract
Let's define when we need to compute what?
On Contract
First we create functions on the Contract class:
-
One to recompute the total of extensions cost from scratch, so parsing extensions one by one
-
One to compute the total by just adding or removing one Component (for efficiency)
They will be called on multiple events, let's avoid to duplicate the code.
- class::Contract
-
// This function, retrieve all related components and sum their cost public function ComputeComponents() { $iSum = 0; $oSet = $this->Get('components_list'); while($oLnk = $oSet->Fetch()) { $oComponent = MetaModel::GetObject('Component', $oLnk->Get('component_id'), false, true); if (is_object($oComponent )) { $iSum = $iSum + $oComponent ->Get('cost'); } } $this->Set('components_total_cost', $iSum); } // This function, retrieve one component and add or remove its cost from the Total public function ComputeComponentsDelta($idComponent, $bAdd) { $oSource = MetaModel::GetObject('Component', $idComponent, false, true); // protection against db incoherence if (is_object($oSource)) { // Get the value from the Source object $iSource = $oSource->Get('cost'); $iToUpdate = $this->Get('components_total_cost'); $i = $bAdd ? ($iToUpdate + $iSource) : ($iToUpdate - $iSource); $this->Set('components_total_cost', $i); } }
On lnk objects
What to do when a link
object is created, deleted
or modified?
Creation
-
Ask the Contract to Add the Component to its Total
Modification
In this case I am looking at all sorts of possible change on this lnk object. In the Standard user interface, most of those cases are limited to an administrator or a REST/json API. But to be bulletproof, you need to suppose that everything can happen:
-
the contract is changed, but not the component
-
Remove the Component cost from old Contract
-
Add the Component cost to new Contract
-
-
the component is changed, but not the Contract
-
Remove the old Component cost from the Contract
-
Add the new Component cost from the Contract
-
-
even both are changed at the same time
-
Remove the old Component cost from old Contract
-
Add the new Component cost to the new Contract
-
Deletion
-
Ask the Contract to Remove the deleted Component from its Total
- class::lnkContractToComponent
-
public function AfterInsert() { $oContract = MetaModel::GetObject('Contract', $this->Get('contract_id'), false, true); if (is_object($oContract)) { // Here we ask the Contract to "Add" the value of a particular component $oContract->ComputeComponentsDelta($this->Get('component_id'),true); $oContract->DBUpdate(); } } public function AfterUpdate() { $aChanges = $this->ListPreviousValuesForUpdatedAttributes(); $bContractChanged = array_key_exists('contract_id', $aChanges); $bComponentChanged = array_key_exists('component_id', $aChanges); // Compute the Removed Component, regardless if it was changed or not $iRemovedComponent = ($bComponentChanged) ? $aChanges['component_id'] : $this->Get('component_id'); $iAddedComponent = $this->Get('component_id'); $iPreviousContract = ($bContractChanged) ? $aChanges['contract_id'] : 0; $iNewContract = $this->Get('contract_id'); if ($bContractChanged) { $oPreviousContract = MetaModel::GetObject('Contract', $aChanges['contract_id'], false, true); if (is_object($oPreviousContract)) { $oPreviousContract->ComputeComponentsDelta($iRemovedComponent, false); $oPreviousContract->DBUpdate(); } $oNewContract = MetaModel::GetObject('Contract', $this->Get('contract_id'), false, true); if (is_object($oNewContract)) { $oNewContract->ComputeComponentsDelta($iAddedComponent, true); $oNewContract->DBUpdate(); } } else if ($bComponentChanged) { $oContract = MetaModel::GetObject('Contract', $this->Get('contract_id'), false, true); if (is_object($oContract)) { $oContract->ComputeComponentsDelta($iRemovedComponent, false); $oContract->ComputeComponentsDelta($iAddedComponent, true); $oContract->DBUpdate(); } } } public function AfterDelete() { $oContract = MetaModel::GetObject('Contract', $this->Get('contract_id'), false, true); if (is_object($oContract)) { $oContract->ComputeComponentsDelta($this->Get('component_id'), false); $oContract->DBUpdate(); } }
On Component
Then we have to imagine the various cases which can happen to a Component
-
A Component is created ⇒ this is handled by the creation of associated links
-
A Component is deleted ⇒ this is handled by the cascading deletion of associated links
-
A Component has its
cost
modified
- class::Component
-
public function AfterUpdate() { $aChanges = $this->ListPreviousValuesForUpdatedAttributes(); // If the cost has changed if (array_key_exists('cost', $aChanges)) { // for each related Contract $oSet = $this->Get('contracts_list'); while($oLnk = $oSet->Fetch()) { // Retrieve the Contract $oContract = MetaModel::GetObject('Contract', $oLnk->Get('contract_id'), false, true); // Recompute from scratch $oContract->ComputeComponents(); $oContract->DBUpdate(); } } }
Pitfall
In theory, this should be enough,…
But when you update the Contract and the lnkContractToComponent in the same transaction, the code start a Contract::DBUpdate(), within which it calls for each lnkContractToComponent either OnInsert(), OnUpdate() or OnDelete() which we have coded to perform a $oContract→DBUpdate() and boom! we enter an infinite loop, which is prevented by the code. As a result, this second $oContract→DBUpdate() is not performed at all (silently).
The workaround is to code something special on the Contract itself:
-
When it is created, lnk can be created as well (For eg. by Object-Copier)
-
When the Contract is updated, then lets recompute from scratch the Total, as we know for sure that the reentrance protection will ignore the contract DBUpdate requested during the lnk processing.
-
When the Contract is deleted, no action is required.
- class::Contract
-
public function OnUpdate() { // That function may do other stuff than this... $aChanges = $this->ListChanges(); // If the list of linked Components has changed: if (array_key_exists('components_list', $aChanges)) { // Do a full computation, $this->ComputeComponents(); // This is needed because of reentrance protection } //... more code can be put here for other purpose }