:: Version 2.7.0 ::

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.3.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 Component cost.
  • We have a lnkContractToComponent class with a contract_id and a component_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

Lets 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 handle by the creation of associated links
  • A Component is deleted ⇒ this is handle 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();
        }
    } 
}
Known limitation of the above code: if you change the cost of a Component and remove related Contracts within the same transaction, the new cost instead of the old one, will be subtract from removed contracts

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->ComputeExtensions();
            // This is needed because of reentrance protection
        }
        //... more code can be put here for other purpose
}
2_7_0/customization/cascade-update2.txt · Last modified: 2020/07/31 10:00 (external edit)
Back to top
Contact us