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
- 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 overwrite the method DoCheckToWrite()
of
the object class:
-
This method is invoked just before writing to database - See details of call stack.
-
The method should provide error message(s) if it encounters data incoherence.
-
Errors messages are recorded in an array $this->m_aCheckIssues[],
-
Warnings messages are recorded in an array $this->m_aCheckWarnings[],
-
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 FIXME -to be checked !-
-
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.
DoCheckToWrite
method can prevent creation/modification in all cases: on the
Console, in the Portal, in CSV import, in DataSynchro and in
REST/JSON APIDoCheckToWrite()
method, you can also use the Extensions API and put
that same code into
iApplicationObjectExtension::OnCheckToWrite()
-
The advantage of the API is that multiple extensions can do their check in parallel,
-
The overwrite of method can only be done by a single extension.
Set
values on current object in this method as it has
effectExamples
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.
- class:Change
-
public function DoCheckToWrite() { // Always ask the parent class to perform its own check parent::DoCheckToWrite(); // 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->m_aCheckIssues[] = 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.
- class:Server
-
public function DoCheckToWrite() { // Always ask the parent class to perform their own check parent::DoCheckToWrite(); // 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->m_aCheckIssues[] = Dict::Format('Server:Error:LocationMandatoryInProduction'); } } }
// You may also provide a simple error message in plain text $this->m_aCheckIssues[] = '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 DoCheckToWrite() { // Call the function on the parent class as it may need to check stuff as well parent::DoCheckToWrite(); // 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->m_aCheckIssues[] = Dict::Format('Class:FunctionalCI:FCINameMustBeUnique', $sNewName); } } }
User must have a Profile
This is how iTop ensures that a user has always at least one profile attached.
-
It's a good example to force a n:n relationship to have at least one entry.
-
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
-
DoCheckToWrite
or
iApplicationObjectExtension::OnCheckToWrite()
can
not guarantee that the rule will always be
applied, if the check is made on other objects that the current
one. This is the case for eg. when testing condition on LinkedSet
or LinkedSetIndirect fields, as in this example.FIXME Write a tuto on how to guarantee that a LinkedSet field as always at least one entry
- class:User
-
public function DoCheckToWrite() { // Call the function on the parent class as it may need to check stuff as well parent::DoCheckToWrite(); // Check that the name of the FunctionalCI must be unique $aChanges = $this->ListChanges(); // Check if the profile list was changed to avoid loading it for nothing if (array_key_exists('profile_list', $aChanges)) { $oSet = $this->Get('profile_list'); if ($oSet->Count() == 0) { $this->m_aCheckIssues[] = 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 FonctionalCI only to users having the profile “Configuration Manager”
- class:FunctionalCI
-
public function DoCheckToWrite() { 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->m_aCheckIssues[] = Dict::S('Class:FunctionalCI/Error:CreationDenied'); } }