Check data integrity
Prerequisite: You must be familiar with the Syntax used in Tutorials and have already created an extension.
- learning:
- Impose data integrity rules
- level:
- Intermediate
- domains:
- PHP, Constrain
- methods:
- AddCheckIssues, AddCheckWarning, EVENT_DB_CHECK_TO_WRITE
- min version:
- 2.1.0
In the below examples we will use a method to detect an incoherence and prevent such object to be saved in database.
-
This method in the Console or Portal, reports errors after submission.
-
And prevents creation and update of incoherent objects done by DataSynchro, REST/JSON and CSV import.
Theory
We will subscribe to event EVENT_DB_CHECK_TO_WRITE, and defined
a callback method evtCheckToWrite()
of the object
class:
-
This event is generated just before writing to database - See details of call stack.
-
The callback method should provide error message(s) if it encounters data incoherence.
-
Errors messages are generated using $this->AddCheckIssues('Some Error Message'),
-
Warnings messages use $this->AddCheckWarnings('Some Waring Message'),
-
-
When returning from this method, if there is at least one error the object is not written to database (creation or update)
-
Error and warning messages are
-
displayed to the user in interactive mode only: Console, Portal, CSV import
-
logged in itop/log/error.log depending on level of tracking for DataSynchro, REST/JSON, CLI
-
Migration: No visible effect on setup, but
objects not compliant can no more be modified, until they are made
compliant. So it could prevent a datasynchro or a REST/JSON script
to update other fields for eg.
To identify faulty objects, create an audit rule to retrieve
objects not compliant to this new constrain and fix them one by one
in the UI or by CSV import.
AddCheckIssue()
method in the callback of an EVENT_DB_CHECK_TO_WRITE prevents
creation/modification in all cases: on the Console, in the Portal,
in CSV import, in DataSynchro and in REST/JSON APISet
values on current object in the
EVENT_DB_CHECK_TO_WRITE callback method, it has no
effectBefore iTop 3.1.0 you could use the Extensibility API and put
that same code into
iApplicationObjectExtension::OnCheckToWrite()
Examples
Start date < End date
In this use case we will prevent a Change to be recorded with an End date which would be before the Start date.
- itop_design / classes
-
<class id="Change" _delta="must_exist"> <event_listeners> <event_listener id="evtCheckToWrite" _delta="define"> <!-- Id of the event does not have to be the same as the function, but why not --> <event>EVENT_DB_CHECK_TO_WRITE</event> <rank>10</rank> <!-- The callback must be the name of an existing class method. The name is free --> <callback>evtCheckToWrite</callback> </event_listener> </event_listeners>
- class:Change
-
public function evtCheckToWrite(Combodo\iTop\Service\Events\EventData $oEventData) { // Defensive programming, ensuring that 'end_date' and 'start_date' has not been removed // from the Change class by some extensions which I am not yet aware of. // Get the value in seconds before comparing them is safer if (MetaModel::IsValidAttCode(get_class($this), 'start_date') && MetaModel::IsValidAttCode(get_class($this), 'end_date') && (AttributeDateTime::GetAsUnixSeconds($this->Get('start_date')) > AttributeDateTime::GetAsUnixSeconds($this->Get('end_date')))) { $this->AddCheckIssues(Dict::Format('Class:Error:EndDateMustBeGreaterThanStartDate')); } }
Location required on production Server
In this use case we want to prevent a Server to be put in 'production' status without a Location to be provided.
- itop_design / classes
-
<class id="Server" _delta="must_exist"> <event_listeners> <event_listener id="evtCheckToWrite" _delta="define"> <!-- Id of the event does not have to be the same as the function, but why not --> <event>EVENT_DB_CHECK_TO_WRITE</event> <rank>10</rank> <!-- The callback must be the name of an existing class method. The name is free --> <callback>evtCheckToWrite</callback> </event_listener> </event_listeners>
- class:Server
-
public function evtCheckToWrite(Combodo\iTop\Service\Events\EventData $oEventData) { // Defensive programming, ensuring that 'status' is an existing field on the current class // then checking the condition: an enum value returns code, not label, so we test the code, if (MetaModel::IsValidAttCode(get_class($this), 'status') && ($this->Get('status') == 'production')) { // AttributeExternalKey are never NULL, O is the value used when empty if (MetaModel::IsValidAttCode(get_class($this), 'location_id') && ($this->Get('location_id') == 0)) { // 'Server:Error:LocationMandatoryInProduction' must be declared as a dictionary entry $this->AddCheckIssues(Dict::Format('Server:Error:LocationMandatoryInProduction')); } } }
// You may also provide a simple error message in plain text $this->AddCheckIssues('Location is mandatory for all Servers in production');
Here the way to define a dictionary entry in XML:
- itop_design / dictionaries / dictionary@EN US / entries
-
<entry id="Server:Error:LocationMandatoryInProduction" _delta="define"> <![CDATA['Location is mandatory for all Servers in production']]> </entry>
FunctionalCI name unique
In this use case we want to prevent two FunctionalCIs to have the same name. Except if the FunctionalCI is in fact a SoftwareInstance, a MiddlewareInstance, a DatabaseSchema or an ApplicationSolution, in which case, we don't care.
- class:FunctionalCI
-
public function evtCheckToWrite(Combodo\iTop\Service\Events\EventData $oEventData) { // Check that the name of the FunctionalCI must be unique $aChanges = $this->ListChanges(); // Check if the name field was set or changed if (array_key_exists('name', $aChanges)) { $sNewName = $aChanges['name']; // Retrieve all FunctionalCI having that new name, ignoring CIs from some sub-classes $oSearch = DBObjectSearch::FromOQL_AllData(" SELECT FunctionalCI WHERE name = :newFCI AND finalclass NOT IN ('DBServer','Middleware','OtherSoftware','WebServer', 'PCSoftware','MiddlewareInstance','DatabaseSchema','ApplicationSolution') "); $oSet = new DBObjectSet($oSearch, array(), array('newFCI' => $sNewName)); // If there is at least one FunctionalCI matching the required name if ($oSet->Count() > 0) { // Block the FunctionalCI writing the Database $this->AddCheckIssues(Dict::Format('Class:FunctionalCI:FCINameMustBeUnique', $oSet->Count(), $sNewName)); } } }
Here the way to define a dictionary entry with placeholders in XML:
- itop_design / dictionaries / dictionary@EN US / entries
-
<entry id="Class:FunctionalCI:FCINameMustBeUnique" _delta="define"> <![CDATA['Functional CI name are expected to be unique, there are already %1$s using the name %2$s']]> </entry>
Change must have a CI
Ensure that a Change has always at least one associated CI attached.
-
It's a good example to force a n:n relationship to have at least one entry.
- class:Change
-
public function evtCheckToWrite(Combodo\iTop\Service\Events\EventData $oEventData) { // Check if the current Change is expected to have a CI linked // under creation, with a particular type, service, status, whatever logic you need... if (is_subclass_of($this,'Change')) { $oSet = $this->Get('functionalcis_list'); if ($oSet->Count() == 0) { $this->AddCheckIssues(Dict::S('Class:Change/Error:AtLeastOneCiIsNeeded')); } } }
Maybe you want to guarantee that at any time a Change must have
at least one CI linked to it, since iTop 3.2.0 there is a mean to
check this.
-
In order to be sure that the CheckToWrite of the Change will be performed when a particular LinkedSet or LinkedSetIndirect is modified, just flag the field in XML with_php_constrains
- itop_design / classes
-
<class id="Ticket"> <fields> <field id="functionalcis_list"> <with_php_constrains _delta="define">true</with_php_constrains> </field> </fields> </class>
The functionalcis_list
field belongs to the Ticket,
as a result the CheckToWrite event will be triggered on all
Tickets, as soon as a functional CI is added or removed from a
Ticket
User must have a Profile
This is how iTop out of the box ensures that a user has always at least one profile attached.
-
Note that this is not enough to prevent the deletion of a Profile which would be the only one of a given User
-
Not a big deal in this particular example as iTop UI does not offer any mean to delete a Profile
- class:User
-
public function evtCheckToWrite(Combodo\iTop\Service\Events\EventData $oEventData) { // Check if the profile list was changed to avoid loading it for nothing $aChanges = $this->ListChanges(); if (array_key_exists('profile_list', $aChanges)) { $oSet = $this->Get('profile_list'); if ($oSet->Count() == 0) { $this->AddCheckIssues(Dict::S('Class:User/Error:AtLeastOneProfileIsNeeded')); } } }
Prevent creation
This method is not really user friendly, but it allows to let some users modify any FunctionalCI, but limit the creation of a new FunctionalCI only to users having the profile “Configuration Manager”
- class:FunctionalCI
-
public function evtCheckToWrite(Combodo\iTop\Service\Events\EventData $oEventData) { if ($this->IsNew() // Are we trying to create a new object && !(UserRights::HasProfile('Configuration Manager')) // The user does not have the profile "Configuration Manager" { $this->AddCheckIssues(Dict::S('Class:FunctionalCI/Error:CreationDenied')); } }
The dictionary entry in this example, does not exist, you must create it.