N°8760 - rename GetCreatedIn <- GetModuleName + compute module name live instead having complex stuff in MetaModel/compilation

This commit is contained in:
odain
2026-01-27 17:26:16 +01:00
parent 02ea17d897
commit 54c7af1140
6 changed files with 45 additions and 441 deletions

View File

@@ -22,6 +22,8 @@ use Combodo\iTop\Application\EventRegister\ApplicationEvents;
use Combodo\iTop\Core\MetaModel\FriendlyNameType;
use Combodo\iTop\Service\Events\EventData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Setup\ModuleDependency\Module;
use Combodo\iTop\Setup\ModuleDiscovery\ModuleFileReader;
require_once APPROOT.'core/modulehandler.class.inc.php';
require_once APPROOT.'core/querymodifier.class.inc.php';
@@ -30,7 +32,6 @@ require_once APPROOT.'core/computing.inc.php';
require_once APPROOT.'core/relationgraph.class.inc.php';
require_once APPROOT.'core/apc-compat.php';
require_once APPROOT.'core/expressioncache.class.inc.php';
require_once APPROOT.'core/metamodel/IncludeFileReader.php';
/**
* We need to have all iLoginFSMExtension/iLoginUIExtension impl loaded ! Cannot use autoloader...
@@ -289,10 +290,6 @@ abstract class MetaModel
* @var array
*/
private static $m_aRootClasses = [];
//used to populate DM create_in field for classes declared in 'datamodel'
public static array $m_aCreatedIn = [];
/**
* array of ("classname" => array of "parentclass")
*
@@ -473,11 +470,36 @@ abstract class MetaModel
* @return string
* @throws \CoreException
*/
final public static function GetCreatedIn($sClass)
final public static function GetModuleName($sClass)
{
self::_check_subclass($sClass);
try {
$oReflectionClass = new ReflectionClass($sClass);
$sDir = realpath(dirname($oReflectionClass->getFileName()));
$sApproot = realpath(APPROOT);
while (($sDir !== $sApproot) && (str_contains($sDir, $sApproot))) {
$aFiles = glob("$sDir/module.*.php");
if (count($aFiles) > 1) {
return 'core';
}
return self::$m_aClassParams[$sClass]['created_in'] ?? "";
if (count($aFiles) == 0) {
$sDir = realpath(dirname($sDir));
continue;
}
$sModuleFilePath = $aFiles[0];
$aModuleInfo = ModuleFileReader::GetInstance()->ReadModuleFileInformation($sModuleFilePath);
$sModuleId = $aModuleInfo[ModuleFileReader::MODULE_INFO_ID];
list($sModuleName, ) = ModuleDiscovery::GetModuleName($sModuleId);
return $sModuleName;
}
} catch (\Exception $e) {
IssueLog::Error("Cannot compute class properly", null, [$e->getTraceAsString(), $e->getMessage()]);
return "";
}
return 'core';
}
/**
@@ -3163,7 +3185,6 @@ abstract class MetaModel
$aMandatParams = [
"category" => "group classes by modules defining their visibility in the UI",
"key_type" => "autoincrement | string",
//"created_in" => "module_name where class is defined",
"name_attcode" => "define which attribute is the class name, may be an array of attributes (format specified in the dictionary as 'Class:myclass/Name' => '%1\$s %2\$s...'",
"state_attcode" => "define which attribute is representing the state (object lifecycle)",
"reconc_keys" => "define the attributes that will 'almost uniquely' identify an object in batch processes",
@@ -3190,10 +3211,6 @@ abstract class MetaModel
self::$m_aParentClasses[$sClass] = [];
self::$m_aChildClasses[$sClass] = [];
if (! array_key_exists('created_in', $aParams)) {
$aParams['created_in'] = self::$m_aCreatedIn[$sClass] ?? '';
}
self::$m_aClassParams[$sClass] = $aParams;
self::$m_aAttribDefs[$sClass] = [];
@@ -5873,7 +5890,6 @@ abstract class MetaModel
// todo - verifier que toutes les classes mentionnees ici sont chargees dans InitClasses()
self::$m_aExtensionClassNames = $result['m_aExtensionClassNames'];
self::$m_Category2Class = $result['m_Category2Class'];
self::$m_aCreatedIn = $result['m_aCreatedIn'];
self::$m_aRootClasses = $result['m_aRootClasses'];
self::$m_aParentClasses = $result['m_aParentClasses'];
self::$m_aChildClasses = $result['m_aChildClasses'];
@@ -5909,7 +5925,6 @@ abstract class MetaModel
$aCache = [];
$aCache['m_aExtensionClassNames'] = self::$m_aExtensionClassNames;
$aCache['m_Category2Class'] = self::$m_Category2Class;
$aCache['m_aCreatedIn'] = self::$m_aCreatedIn; // array of "classname" => "created_in"
$aCache['m_aRootClasses'] = self::$m_aRootClasses; // array of "classname" => "rootclass"
$aCache['m_aParentClasses'] = self::$m_aParentClasses; // array of ("classname" => array of "parentclass")
$aCache['m_aChildClasses'] = self::$m_aChildClasses; // array of ("classname" => array of "childclass")
@@ -6010,12 +6025,11 @@ abstract class MetaModel
/**
* @param string $sToInclude
* @param string|null $sModuleType
* @param string|null $sModuleName
* @param string $sModuleType
*
* @throws \CoreException
*/
public static function IncludeModule($sToInclude, $sModuleType = null, $sModuleName = null)
public static function IncludeModule($sToInclude, $sModuleType = null)
{
$sFirstChar = substr($sToInclude, 0, 1);
$sSecondChar = substr($sToInclude, 1, 1);
@@ -6046,17 +6060,6 @@ abstract class MetaModel
}
}
if (! is_null($sModuleName)) {
try {
$aClasses = IncludeFileReader::GetInstance()->GetClasses($sFile);
foreach ($aClasses as $sFoundClass) {
MetaModel::$m_aCreatedIn[$sFoundClass] = $sModuleName;
}
} catch (\Exception $e) {
SetupLog::Error(__METHOD__, null, [$e]);
}
}
// Note: We do not expect the modules to output characters while loading them.
// Therefore, and because unexpected characters can corrupt the output,
// they must be trashed here.

View File

@@ -1,52 +0,0 @@
<?php
use PhpParser\Error;
use PhpParser\ParserFactory;
class IncludeFileReader
{
private static IncludeFileReader $oInstance;
protected function __construct()
{
}
final public static function GetInstance(): IncludeFileReader
{
if (!isset(IncludeFileReader::$oInstance)) {
IncludeFileReader::$oInstance = new IncludeFileReader();
}
return self::$oInstance;
}
final public static function SetInstance(?IncludeFileReader $oInstance): void
{
IncludeFileReader::$oInstance = $oInstance;
}
public function GetClasses($sPath): array
{
try {
$oParser = (new ParserFactory())->createForNewestSupportedVersion();
$aNodes = $oParser->parse(file_get_contents($sPath));
} catch (Error $e) {
throw new Exception("PHP Class Parsing of $sPath caused an exception: ".$e->getMessage(), 0, $e);
}
$aRes = [];
try {
foreach ($aNodes as $sKey => $oNode) {
if ($oNode instanceof PhpParser\Node\Stmt\Class_) {
/** @var PhpParser\Node\Stmt\Class_ $oNode */
//var_dump($oNode->name);
$aRes[] = $oNode->name->name;
}
}
return $aRes;
} catch (Exception $e) {
throw new Exception("PHP Class Discovery of $sPath caused an exception: ".$e->getMessage(), 0, $e);
}
}
}

View File

@@ -477,7 +477,7 @@ class MFCompiler
$sClass = $oClass->getAttribute("id");
$aAllClasses[] = $sClass;
try {
$sCompiledCode .= $this->CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir);
$sCompiledCode .= $this->CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sRelativeDir);
} catch (DOMFormatException $e) {
$sMessage = "Failed to process class '$sClass', ";
if (!empty($sModuleRootDir)) {
@@ -626,7 +626,7 @@ EOF;
// files to include (PHP datamodels)
foreach ($oModule->GetFilesToInclude('business') as $sRelFileName) {
if (file_exists("{$sTempTargetDir}/{$sRelativeDir}/{$sRelFileName}")) {
$aDataModelFiles[] = sprintf("MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName', null, '%s');", $oModule->GetName());
$aDataModelFiles[] = "MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName');";
} else {
/** @noinspection NestedPositiveIfStatementsInspection */
if (utils::IsDevelopmentEnvironment()) {
@@ -643,7 +643,7 @@ EOF;
}
// files to include (PHP webservices providers)
foreach ($oModule->GetFilesToInclude('webservices') as $sRelFileName) {
$aWebservicesFiles[] = sprintf("MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName', null, '%s');", $oModule->GetName());
$aWebservicesFiles[] = "MetaModel::IncludeModule(MODULESROOT.'/$sRelativeDir/$sRelFileName');";
}
} // foreach module
@@ -1189,7 +1189,6 @@ EOF
/**
* @param \MFElement $oClass
* @param string $sModuleName
* @param string $sTempTargetDir
* @param string $sFinalTargetDir
* @param string $sModuleRelativeDir
@@ -1197,7 +1196,7 @@ EOF
* @return string
* @throws \DOMFormatException
*/
protected function CompileClass($oClass, $sModuleName, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir)
protected function CompileClass($oClass, $sTempTargetDir, $sFinalTargetDir, $sModuleRelativeDir)
{
$sClass = $oClass->getAttribute('id');
$oProperties = $oClass->GetUniqueElement('properties');
@@ -1210,7 +1209,6 @@ EOF
$aClassParams = [];
$aClassParams['category'] = $this->GetPropString($oProperties, 'category', '');
$aClassParams['key_type'] = "'autoincrement'";
$aClassParams['created_in'] = "'$sModuleName'";
if ((bool)$this->GetPropNumber($oProperties, 'is_link', 0)) {
$aClassParams['is_link'] = 'true';
}

View File

@@ -499,24 +499,29 @@ class MetaModelTest extends ItopDataTestCase
];
}
public function testGetCreatedIn_UnknownClass()
{
$this->assertEquals('', MetaModel::GetModuleName('GABUZOMEU'));
}
public function testGetCreatedIn_ClassComingFromCorePhpFile()
{
$this->assertEquals('', MetaModel::GetCreatedIn('BackgroundTask'));
$this->assertEquals('core', MetaModel::GetModuleName('BackgroundTask'));
}
public function testGetCreatedIn_ClassComingFromCorePhpFile2()
{
$this->assertEquals('core', MetaModel::GetCreatedIn('lnkActionNotificationToContact'));
$this->assertEquals('core', MetaModel::GetModuleName('lnkActionNotificationToContact'));
}
public function testGetCreatedIn_ClassComingFromModulePhpFile()
{
$this->assertEquals('itop-attachments', MetaModel::GetCreatedIn('CMDBChangeOpAttachmentAdded'));
$this->assertEquals('itop-attachments', MetaModel::GetModuleName('CMDBChangeOpAttachmentAdded'));
}
public function testGetCreatedIn_ClassComingFromXmlDataModelFile()
{
$this->assertEquals('authent-ldap', MetaModel::GetCreatedIn('UserLDAP'));
$this->assertEquals('authent-ldap', MetaModel::GetModuleName('UserLDAP'));
}
}

View File

@@ -1,22 +0,0 @@
<?php
use Combodo\iTop\Test\UnitTest\ItopTestCase;
class IncludeFileReaderTest extends ItopTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->RequireOnceItopFile('core/metamodel/IncludeFileReader.php');
}
public function testGetClasses()
{
$expected = [
'iTopOwnershipToken',
'iTopOwnershipLock',
];
$this->assertEquals($expected, IncludeFileReader::GetInstance()->GetClasses(__DIR__.'/resources/ownershiplock.class.inc.php'));
}
}

View File

@@ -1,328 +0,0 @@
<?php
// Copyright (C) 2024 Combodo SAS
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Mechanism to obtain an exclusive lock while editing an object
*
* @package iTopORM
*/
/**
* Persistent storage (in the database) for remembering that an object is locked
*/
class iTopOwnershipToken extends DBObject
{
public static function Init()
{
$aParams =
[
'category' => '',
'key_type' => 'autoincrement',
'name_attcode' => ['obj_class', 'obj_key'],
'state_attcode' => '',
'reconc_keys' => [''],
'db_table' => 'priv_ownership_token',
'db_key_field' => 'id',
'db_finalclass_field' => '',
];
MetaModel::Init_Params($aParams);
MetaModel::Init_InheritAttributes();
MetaModel::Init_AddAttribute(new AttributeDateTime("acquired", ["allowed_values" => null, "sql" => 'acquired', "default_value" => 'NOW()', "is_null_allowed" => false, "depends_on" => []]));
MetaModel::Init_AddAttribute(new AttributeDateTime("last_seen", ["allowed_values" => null, "sql" => 'last_seen', "default_value" => 'NOW()', "is_null_allowed" => false, "depends_on" => []]));
MetaModel::Init_AddAttribute(new AttributeString("obj_class", ["allowed_values" => null, "sql" => 'obj_class', "default_value" => '', "is_null_allowed" => false, "depends_on" => []]));
MetaModel::Init_AddAttribute(new AttributeInteger("obj_key", ["allowed_values" => null, "sql" => 'obj_key', "default_value" => '', "is_null_allowed" => true, "depends_on" => []]));
MetaModel::Init_AddAttribute(new AttributeString("token", ["allowed_values" => null, "sql" => 'token', "default_value" => '', "is_null_allowed" => true, "depends_on" => []]));
MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", ["targetclass" => "User", "jointype" => '', "allowed_values" => null, "sql" => "user_id", "is_null_allowed" => true, "on_target_delete" => DEL_SILENT, "depends_on" => []]));
MetaModel::Init_SetZListItems('details', ['obj_class', 'obj_key', 'last_seen', 'token']);
MetaModel::Init_SetZListItems('standard_search', ['obj_class', 'obj_key', 'last_seen', 'token']);
MetaModel::Init_SetZListItems('list', ['obj_class', 'obj_key', 'last_seen', 'token']);
}
}
/**
* Utility class to acquire/extend/release/kill an exclusive lock on a given persistent object,
* for example to prevent concurrent edition of the same object.
* Each lock has an expiration delay of 120 seconds (tunable via the configuration parameter 'concurrent_lock_expiration_delay')
* A watchdog (called twice during this delay) is in charge of keeping the lock "alive" while an object is being edited.
*/
class iTopOwnershipLock
{
protected $sObjClass;
protected $iObjKey;
protected $oToken;
/**
* Acquires an exclusive lock on the specified DBObject. Once acquired, the lock is identified
* by a unique "token" string.
* @param string $sObjClass The class of the object for which to acquire the lock
* @param integer $iObjKey The identifier of the object for which to acquire the lock
* @return multitype:boolean iTopOwnershipLock Ambigous <boolean, string, DBObjectSet>
*/
public static function AcquireLock($sObjClass, $iObjKey)
{
$oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
$oMutex->Lock();
$oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
$token = $oOwnershipLock->Acquire();
$oMutex->Unlock();
return ['success' => $token !== false, 'token' => $token, 'lock' => $oOwnershipLock, 'acquired' => $oOwnershipLock->oToken->Get('acquired')];
}
/**
* Extends the ownership lock or acquires it if none exists
* Returns a hash array with 3 elements:
* 'status': either true or false, tells if the lock is still owned
* 'owner': is status is false, the User object currently owning the lock
* 'operation': whether the lock was 'renewed' (i.e. the lock was valid, its duration has been extended) or 'acquired' (there was no valid lock for this object and a new one was created)
* @param string $sToken
* @return multitype:boolean string User
*/
public static function ExtendLock($sObjClass, $iObjKey, $sToken)
{
$oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
$oMutex->Lock();
$oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
$aResult = $oOwnershipLock->Extend($sToken);
$oMutex->Unlock();
return $aResult;
}
/**
* Releases the given lock for the specified object
*
* @param string $sObjClass The class of the object
* @param int $iObjKey The identifier of the object
* @param string $sToken The string identifying the lock
* @return boolean
*/
public static function ReleaseLock($sObjClass, $iObjKey, $sToken)
{
$oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
$oMutex->Lock();
$oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
$bResult = $oOwnershipLock->Release($sToken);
self::DeleteExpiredLocks(); // Cleanup orphan locks
$oMutex->Unlock();
return $bResult;
}
/**
* Kills the lock for the specified object
*
* @param string $sObjClass The class of the object
* @param int $iObjKey The identifier of the object
* @return boolean
*/
public static function KillLock($sObjClass, $iObjKey)
{
$oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
$oMutex->Lock();
$sOQL = "SELECT iTopOwnershipToken WHERE obj_class = :obj_class AND obj_key = :obj_key";
$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, ['obj_class' => $sObjClass, 'obj_key' => $iObjKey]));
while ($oLock = $oSet->Fetch()) {
$oLock->DBDelete();
}
$oMutex->Unlock();
}
/**
* Checks if an exclusive lock exists on the specified DBObject.
* @param string $sObjClass The class of the object for which to acquire the lock
* @param integer $iObjKey The identifier of the object for which to acquire the lock
* @return multitype:boolean iTopOwnershipLock Ambigous <boolean, string, DBObjectSet>
*/
public static function IsLocked($sObjClass, $iObjKey)
{
$bLocked = false;
$oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
$oMutex->Lock();
$oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
if ($oOwnershipLock->IsOwned()) {
$bLocked = true;
}
$oMutex->Unlock();
return ['locked' => $bLocked, 'owner' => $oOwnershipLock->GetOwner()];
}
/**
* Get the current owner of the lock
* @return User
*/
public function GetOwner()
{
if ($this->IsTokenValid()) {
return MetaModel::GetObject('User', $this->oToken->Get('user_id'), false, true);
}
return null;
}
/**
* The constructor is protected. Use the static methods AcquireLock / ExtendLock / ReleaseLock / KillLock
* which are protected against concurrent access by a Mutex.
* @param string $sObjClass The class of the object for which to create a lock
* @param integer $iObjKey The identifier of the object for which to create a lock
*/
protected function __construct($sObjClass, $iObjKey)
{
$sOQL = "SELECT iTopOwnershipToken WHERE obj_class = :obj_class AND obj_key = :obj_key";
$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, ['obj_class' => $sObjClass, 'obj_key' => $iObjKey]));
$this->oToken = $oSet->Fetch();
$this->sObjClass = $sObjClass;
$this->iObjKey = $iObjKey;
// IssueLog::Info("iTopOwnershipLock::__construct($sObjClass, $iObjKey) oToken::".($this->oToken ? $this->oToken->GetKey() : 'null'));
}
protected function IsOwned()
{
return $this->IsTokenValid();
}
protected function Acquire($sToken = null)
{
if ($this->IsTokenValid()) {
// IssueLog::Info("Acquire($sToken) returns false");
return false;
} else {
$sToken = $this->TakeOwnership($sToken);
// IssueLog::Info("Acquire($sToken) returns $sToken");
return $sToken;
}
}
/**
* Extends the ownership lock or acquires it if none exists
* Returns a hash array with 3 elements:
* 'status': either true or false, tells if the lock is still owned
* 'owner': is status is false, the User object currently owning the lock
* 'operation': whether the lock was 'renewed' (i.e. the lock was valid, its duration was extended) or 'expired' (there was no valid lock for this object) or 'lost' (someone else grabbed it)
* 'acquired': date at which the lock was initially acquired
* @param string $sToken
* @return multitype:boolean string User
*/
protected function Extend($sToken)
{
$aResult = ['status' => true, 'owner' => '', 'operation' => 'renewed'];
if ($this->IsTokenValid()) {
if ($sToken === $this->oToken->Get('token')) {
$this->oToken->Set('last_seen', date(AttributeDateTime::GetSQLFormat()));
$this->oToken->DBUpdate();
$aResult['acquired'] = $this->oToken->Get('acquired');
} else {
// IssueLog::Info("Extend($sToken) returns false");
$aResult['status'] = false;
$aResult['operation'] = 'lost';
$aResult['owner'] = $this->GetOwner();
$aResult['acquired'] = $this->oToken->Get('acquired');
}
} else {
$aResult['status'] = false;
$aResult['operation'] = 'expired';
}
// IssueLog::Info("Extend($sToken) returns true");
return $aResult;
}
protected function HasOwnership($sToken)
{
$bRet = false;
if ($this->IsTokenValid()) {
if ($sToken === $this->oToken->Get('token')) {
$bRet = true;
}
}
// IssueLog::Info("HasOwnership($sToken) return $bRet");
return $bRet;
}
protected function Release($sToken)
{
$bRet = false;
// IssueLog::Info("Release... begin [$sToken]");
if (($this->oToken) && ($sToken === $this->oToken->Get('token'))) {
// IssueLog::Info("oToken::".$this->oToken->GetKey().' ('.$sToken.') to be deleted');
$this->oToken->DBDelete();
// IssueLog::Info("oToken deleted");
$this->oToken = null;
$bRet = true;
} elseif ($this->oToken == null) {
// IssueLog::Info("Release FAILED oToken == null !!!");
} else {
// IssueLog::Info("Release FAILED inconsistent tokens: sToken=\"".$sToken.'", oToken->Get(\'token\')="'.$this->oToken->Get('token').'"');
}
// IssueLog::Info("Release... end");
return $bRet;
}
protected function IsTokenValid()
{
$bRet = false;
if ($this->oToken != null) {
$sToken = $this->oToken->Get('token');
$sDate = $this->oToken->Get('last_seen');
if (($sDate != '') && ($sToken != '')) {
$oLastSeenTime = new DateTime($sDate);
$iNow = date('U');
if (($iNow - $oLastSeenTime->format('U')) < MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay')) {
$bRet = true;
}
}
}
return $bRet;
}
protected function TakeOwnership($sToken = null)
{
if ($this->oToken == null) {
$this->oToken = new iTopOwnershipToken();
$this->oToken->Set('obj_class', $this->sObjClass);
$this->oToken->Set('obj_key', $this->iObjKey);
}
$this->oToken->Set('acquired', date(AttributeDateTime::GetSQLFormat()));
$this->oToken->Set('user_id', UserRights::GetUserId());
$this->oToken->Set('last_seen', date(AttributeDateTime::GetSQLFormat()));
if ($sToken === null) {
$sToken = sprintf('%X', microtime(true));
}
$this->oToken->Set('token', $sToken);
$this->oToken->DBWrite();
return $this->oToken->Get('token');
}
protected static function DeleteExpiredLocks()
{
$sOQL = "SELECT iTopOwnershipToken WHERE last_seen < :last_seen_limit";
$oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, ['last_seen_limit' => date(AttributeDateTime::GetSQLFormat(), time() - MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay'))]));
while ($oToken = $oSet->Fetch()) {
$oToken->DBDelete();
}
}
}