Store Count of 1:n relation
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 on 1:n
Here we want to see in the details of an object A, the count of objects B which are linked to the object A through an one-to-many relationship (an external key field on class B pointing to A).
We will see two flavors of that particular case:
- 
On Ticket count the associated WorkOrders
 - 
On User Request count of sub-UserRequests (special case where classes A and B are the same class)
 
That information is easily visible on the details of a single Ticket/User Request, but you have no mean to retrieve quickly all the Ticket/User Request with more that 2 WorkOrders/sub-requests or having no WorkOrders/sub-requests at all. This tutorial, will explain how to resolve that issue
WorkOrders on Ticket
In order to be able to do this:
- 
We need to create a field
count_workorders_liston the Ticket class, to store the number permanently so it can be queried. - 
We have a
workorders_listfield on Ticket, providing the list of sub-request under the current one - 
We have a
ticket_idfield on WorkOrder, providing the Ticket which work-order count must be updated 
Lets define when we need to recompute?
- 
When a Work Order is created, modified or deleted alone
 - 
When the list of work orders is modified with the Ticket it-self
 
- itop_design / classes / class@Ticket
 - 
<fields> <field id="workorders_list" xsi:type="AttributeLinkedSet"> <with_php_computation _delta="force">true</with_php_computation> </field> <field id="count_workorders" xsi:type="AttributeInteger" _delta="define"> <sql>count_workorders</sql> <default_value>0</default_value> <is_null_allowed>false</is_null_allowed> <tracking_level>all</tracking_level> </field> <fields> <event_listeners> <event_listener id="EvtOnLinksChanged" _delta="define"> <event>EVENT_DB_LINKS_CHANGED</event> <callback>OnLinksChanged</callback> <rank>0</rank> </event_listener> <event_listener id="EvtBeforeWrite" _delta="define"> <event>EVENT_DB_BEFORE_WRITE</event> <callback>BeforeWrite</callback> <rank>0</rank> </event_listener> </event_listeners> <methods>
 
- Ticket callbacks
 - 
public function OnLinksChanged(?Combodo\iTop\Service\Events\EventData $oEventData) { // Note the ? in the method declaration, allowing to be called with null as parameter $oSet = $this->Get('workorders_list'); $this->Set('count_workorders', $oSet->count()); // $this->DBUpdate(); useless when $this is modified as it's automatically called at the end of callbacks } public function BeforeWrite(?Combodo\iTop\Service\Events\EventData $oEventData) { $aChanges = $this->ListChanges(); if (array_key_exists('workorders_list', $aChanges)) { $this->OnLinksChanged(null); // We have no EventData to provide, which is anyhow not used in this method } }
 
When a WorkOrder object is created, deleted or
modified alone and not within a modification of its Ticket, the
callback Ticket::OnLinksChanged() will be called automatically
Child UserRequest
This use case is very similar to the above one. With previous methods because classes A and B are the same it was a bit different, now it is no more.
In order to be able to do this:
- 
We need to create a field
count_related_request_liston the UserRequest class, to store the number of sub requests permanently so it can be queried. - 
We have a
related_request_listfield on UserRequest, providing the list of sub-request under the current one - 
We have a
parent_request_idfield on UserRequest, providing the parent 
Lets define when we need to compute what?
- itop_design / classes / class@UserRequest
 - 
<fields> <field id="related_request_list" xsi:type="AttributeLinkedSet"> <with_php_computation _delta="force">true</with_php_computation> </field> <field id="count_related_request" xsi:type="AttributeInteger" _delta="define"> <sql>count_related_request</sql> <default_value>0</default_value> <is_null_allowed>false</is_null_allowed> <tracking_level>all</tracking_level> </field> <fields> <event_listeners> <event_listener id="EvtOnLinksChanged" _delta="define"> <event>EVENT_DB_LINKS_CHANGED</event> <callback>OnLinksChanged</callback> <rank>0</rank> </event_listener> <event_listener id="EvtBeforeWrite" _delta="define"> <event>EVENT_DB_BEFORE_WRITE</event> <callback>BeforeWrite</callback> <rank>0</rank> </event_listener> </event_listeners> <methods>
 
- UserRequest callbacks
 - 
public function BeforeWrite(?Combodo\iTop\Service\Events\EventData $oEventData) { $aChanges = $this->ListChanges(); if (array_key_exists('related_request_list', $aChanges)) { $this->OnLinksChanged(null); // Never call $this->DBUpdate(); within a callback, it's useless and ignored } } public function OnLinksChanged(?Combodo\iTop\Service\Events\EventData $oEventData) { $oSet = $this->Get('related_request_list'); $this->Set('count_related_request', $oSet->count()); // Calling ->DBUpdate() is useful ONLY if the updated object is not $oEventData->Get('object') }
 
You can look at how it was done before, to realize how it has been simplified
