:: Version 2.7.0 ::

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 API
Instead of overwriting the DoCheckToWrite() 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.

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.

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.

Uniqueness rules can perform similar uniqueness checks, but all in XML, without the need to write PHP code.
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');
        }
    }
}
2_7_0/customization/check-to-write.txt ยท Last modified: 2020/04/15 15:23 (external edit)
Back to top
Contact us