mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 15:34:12 +01:00
950 lines
27 KiB
PHP
950 lines
27 KiB
PHP
<?php
|
|
/*
|
|
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
|
* @license http://opensource.org/licenses/AGPL-3.0
|
|
*/
|
|
|
|
namespace Combodo\iTop\Core\AttributeDefinition;
|
|
|
|
use CMDBSource;
|
|
use Combodo\iTop\Application\UI\Links\Set\BlockLinkSetDisplayAsProperty;
|
|
use Combodo\iTop\Form\Field\LinkedSetField;
|
|
use Combodo\iTop\Portal\Helper\ScopeValidatorHelper;
|
|
use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer;
|
|
use Combodo\iTop\Service\Links\LinkSetModel;
|
|
use CoreException;
|
|
use CSVParser;
|
|
use DBObject;
|
|
use DBObjectSearch;
|
|
use DBObjectSet;
|
|
use Dict;
|
|
use Exception;
|
|
use ExceptionLog;
|
|
use IssueLog;
|
|
use MetaModel;
|
|
use ModuleDesign;
|
|
use ormLinkSet;
|
|
use ValueSetObjects;
|
|
|
|
/**
|
|
* Set of objects directly linked to an object, and being part of its definition
|
|
*
|
|
* @package iTopORM
|
|
*/
|
|
class AttributeLinkedSet extends AttributeDefinition
|
|
{
|
|
/**
|
|
* Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)
|
|
*
|
|
* @see https://www.php.net/manual/fr/language.oop5.decon.php states that child constructor can be ommited
|
|
* @see https://bugs.php.net/bug.php?id=79010 bug solved in PHP 7.4.9
|
|
*
|
|
* @param string $sCode
|
|
* @param array $aParams
|
|
*
|
|
* @throws Exception
|
|
* @noinspection SenselessProxyMethodInspection
|
|
*/
|
|
public function __construct($sCode, $aParams)
|
|
{
|
|
parent::__construct($sCode, $aParams);
|
|
$this->aCSSClasses[] = 'attribute-set';
|
|
}
|
|
|
|
public static function ListExpectedParams()
|
|
{
|
|
return array_merge(parent::ListExpectedParams(),
|
|
array("allowed_values", "depends_on", "linked_class", "ext_key_to_me", "count_min", "count_max"));
|
|
}
|
|
|
|
public function GetEditClass()
|
|
{
|
|
return "LinkedSet";
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public static function IsBulkModifyCompatible(): bool
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public function IsWritable()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public static function IsLinkSet()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public function IsIndirect()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public function GetValuesDef()
|
|
{
|
|
$oValSetDef = $this->Get("allowed_values");
|
|
if (!$oValSetDef) {
|
|
// Let's propose every existing value
|
|
$oValSetDef = new ValueSetObjects('SELECT '.LinkSetModel::GetTargetClass($this));
|
|
}
|
|
|
|
return $oValSetDef;
|
|
}
|
|
|
|
public function GetEditValue($value, $oHostObj = null)
|
|
{
|
|
/** @var ormLinkSet $value * */
|
|
if ($value->Count() === 0) {
|
|
return '';
|
|
}
|
|
|
|
/** Return linked objects key as string **/
|
|
$aValues = $value->GetValues();
|
|
|
|
return implode(' ', $aValues);
|
|
}
|
|
|
|
public function GetPrerequisiteAttributes($sClass = null)
|
|
{
|
|
return $this->Get("depends_on");
|
|
}
|
|
|
|
/**
|
|
* @param \DBObject|null $oHostObject
|
|
*
|
|
* @return \ormLinkSet
|
|
*
|
|
* @throws Exception
|
|
* @throws CoreException
|
|
* @throws CoreWarning
|
|
*/
|
|
public function GetDefaultValue(DBObject $oHostObject = null)
|
|
{
|
|
if ($oHostObject === null) {
|
|
return null;
|
|
}
|
|
|
|
$sLinkClass = $this->GetLinkedClass();
|
|
$sExtKeyToMe = $this->GetExtKeyToMe();
|
|
|
|
// The class to target is not the current class, because if this is a derived class,
|
|
// it may differ from the target class, then things start to become confusing
|
|
/** @var AttributeExternalKey $oRemoteExtKeyAtt */
|
|
$oRemoteExtKeyAtt = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToMe);
|
|
$sMyClass = $oRemoteExtKeyAtt->GetTargetClass();
|
|
|
|
$oMyselfSearch = new DBObjectSearch($sMyClass);
|
|
if ($oHostObject !== null) {
|
|
$oMyselfSearch->AddCondition('id', $oHostObject->GetKey(), '=');
|
|
}
|
|
|
|
$oLinkSearch = new DBObjectSearch($sLinkClass);
|
|
$oLinkSearch->AddCondition_PointingTo($oMyselfSearch, $sExtKeyToMe);
|
|
if ($this->IsIndirect()) {
|
|
// Join the remote class so that the archive flag will be taken into account
|
|
/** @var AttributeLinkedSetIndirect $this */
|
|
$sExtKeyToRemote = $this->GetExtKeyToRemote();
|
|
/** @var AttributeExternalKey $oExtKeyToRemote */
|
|
$oExtKeyToRemote = MetaModel::GetAttributeDef($sLinkClass, $sExtKeyToRemote);
|
|
$sRemoteClass = $oExtKeyToRemote->GetTargetClass();
|
|
if (MetaModel::IsArchivable($sRemoteClass)) {
|
|
$oRemoteSearch = new DBObjectSearch($sRemoteClass);
|
|
/** @var \AttributeLinkedSetIndirect $this */
|
|
$oLinkSearch->AddCondition_PointingTo($oRemoteSearch, $this->GetExtKeyToRemote());
|
|
}
|
|
}
|
|
//Add silo in portal context
|
|
if (defined('PORTAL_ID'))
|
|
{
|
|
$oModuleDesign = new ModuleDesign(PORTAL_ID);
|
|
$oScopeValidatorHelper = new ScopeValidatorHelper($oModuleDesign, PORTAL_ID);
|
|
$oScopeValidatorHelper->AddScopeToQuery($oLinkSearch, $oLinkSearch->GetClass());
|
|
}
|
|
|
|
$oLinks = new DBObjectSet($oLinkSearch);
|
|
$oLinkSet = new ormLinkSet($this->GetHostClass(), $this->GetCode(), $oLinks);
|
|
|
|
return $oLinkSet;
|
|
}
|
|
|
|
public function GetTrackingLevel()
|
|
{
|
|
return $this->GetOptional('tracking_level', MetaModel::GetConfig()->Get('tracking_level_linked_set_default'));
|
|
}
|
|
|
|
/**
|
|
* @return string see LINKSET_EDITMODE_* constants
|
|
*/
|
|
public function GetEditMode()
|
|
{
|
|
return $this->GetOptional('edit_mode', LINKSET_EDITMODE_ACTIONS);
|
|
}
|
|
|
|
/**
|
|
* @return int see LINKSET_EDITWHEN_* constants
|
|
* @since 3.1.1 3.2.0 N°6385
|
|
*/
|
|
public function GetEditWhen(): int
|
|
{
|
|
return $this->GetOptional('edit_when', LINKSET_EDITWHEN_ALWAYS);
|
|
}
|
|
|
|
/**
|
|
* @return string see LINKSET_DISPLAY_STYLE_* constants
|
|
* @since 3.1.0 N°3190
|
|
*/
|
|
public function GetDisplayStyle()
|
|
{
|
|
$sDisplayStyle = $this->GetOptional('display_style', LINKSET_DISPLAY_STYLE_TAB);
|
|
if ($sDisplayStyle === '') {
|
|
$sDisplayStyle = LINKSET_DISPLAY_STYLE_TAB;
|
|
}
|
|
|
|
return $sDisplayStyle;
|
|
}
|
|
|
|
/**
|
|
* Indicates if the current Attribute has constraints (php constraints or datamodel constraints)
|
|
*
|
|
* @return bool true if Attribute has constraints
|
|
* @since 3.1.0 N°6228
|
|
*/
|
|
public function HasPHPConstraint(): bool
|
|
{
|
|
return $this->GetOptional('with_php_constraint', false);
|
|
}
|
|
|
|
/**
|
|
* @return bool true if Attribute has computation (DB_LINKS_CHANGED event propagation, `with_php_computation` attribute xml property), false otherwise
|
|
* @since 3.1.1 3.2.0 N°6228
|
|
*/
|
|
public function HasPHPComputation(): bool
|
|
{
|
|
return $this->GetOptional('with_php_computation', false);
|
|
}
|
|
|
|
public function GetLinkedClass()
|
|
{
|
|
return $this->Get('linked_class');
|
|
}
|
|
|
|
public function GetExtKeyToMe()
|
|
{
|
|
return $this->Get('ext_key_to_me');
|
|
}
|
|
|
|
public function GetBasicFilterOperators()
|
|
{
|
|
return array();
|
|
}
|
|
|
|
public function GetBasicFilterLooseOperator()
|
|
{
|
|
return '';
|
|
}
|
|
|
|
public function GetBasicFilterSQLExpr($sOpCode, $value)
|
|
{
|
|
return '';
|
|
}
|
|
|
|
/** @inheritDoc * */
|
|
public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true)
|
|
{
|
|
if ($this->GetDisplayStyle() === LINKSET_DISPLAY_STYLE_TAB) {
|
|
return $this->GetAsHTMLForTab($sValue, $oHostObject, $bLocalize);
|
|
} else {
|
|
return $this->GetAsHTMLForProperty($sValue, $oHostObject, $bLocalize);
|
|
}
|
|
}
|
|
|
|
public function GetAsHTMLForTab($sValue, $oHostObject = null, $bLocalize = true)
|
|
{
|
|
if (is_object($sValue) && ($sValue instanceof ormLinkSet)) {
|
|
$sValue->Rewind();
|
|
$aItems = array();
|
|
while ($oObj = $sValue->Fetch()) {
|
|
// Show only relevant information (hide the external key to the current object)
|
|
$aAttributes = array();
|
|
foreach (MetaModel::ListAttributeDefs($this->GetLinkedClass()) as $sAttCode => $oAttDef) {
|
|
if ($sAttCode == $this->GetExtKeyToMe()) {
|
|
continue;
|
|
}
|
|
if ($oAttDef->IsExternalField()) {
|
|
continue;
|
|
}
|
|
$sAttValue = $oObj->GetAsHTML($sAttCode);
|
|
if (strlen($sAttValue) > 0) {
|
|
$aAttributes[] = $sAttValue;
|
|
}
|
|
}
|
|
$sAttributes = implode(', ', $aAttributes);
|
|
$aItems[] = $sAttributes;
|
|
}
|
|
|
|
return implode('<br/>', $aItems);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function GetAsHTMLForProperty($sValue, $oHostObject = null, $bLocalize = true): string
|
|
{
|
|
try {
|
|
|
|
/** @var ormLinkSet $sValue */
|
|
if (is_null($sValue) || $sValue->Count() === 0) {
|
|
return '';
|
|
}
|
|
|
|
$oLinkSetBlock = new BlockLinkSetDisplayAsProperty($this->GetCode(), $this, $sValue);
|
|
|
|
return ConsoleBlockRenderer::RenderBlockTemplates($oLinkSetBlock);
|
|
}
|
|
catch (Exception $e) {
|
|
$sMessage = "Error while displaying attribute {$this->GetCode()}";
|
|
IssueLog::Error($sMessage, IssueLog::CHANNEL_DEFAULT, [
|
|
'host_object_class' => $this->GetHostClass(),
|
|
'host_object_key' => $oHostObject->GetKey(),
|
|
'attribute' => $this->GetCode(),
|
|
]);
|
|
|
|
return $sMessage;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $sValue
|
|
* @param DBObject $oHostObject
|
|
* @param bool $bLocalize
|
|
*
|
|
* @return string
|
|
*
|
|
* @throws CoreException
|
|
*/
|
|
public function GetAsXML($sValue, $oHostObject = null, $bLocalize = true)
|
|
{
|
|
if (is_object($sValue) && ($sValue instanceof ormLinkSet)) {
|
|
$sValue->Rewind();
|
|
$sRes = "<Set>\n";
|
|
while ($oObj = $sValue->Fetch()) {
|
|
$sObjClass = get_class($oObj);
|
|
$sRes .= "<$sObjClass id=\"".$oObj->GetKey()."\">\n";
|
|
// Show only relevant information (hide the external key to the current object)
|
|
foreach (MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) {
|
|
if ($sAttCode == 'finalclass') {
|
|
if ($sObjClass == $this->GetLinkedClass()) {
|
|
// Simplify the output if the exact class could be determined implicitely
|
|
continue;
|
|
}
|
|
}
|
|
if ($sAttCode == $this->GetExtKeyToMe()) {
|
|
continue;
|
|
}
|
|
if ($oAttDef->IsExternalField()) {
|
|
/** @var \AttributeExternalField $oAttDef */
|
|
if ($oAttDef->GetKeyAttCode() == $this->GetExtKeyToMe()) {
|
|
continue;
|
|
}
|
|
/** @var AttributeExternalField $oAttDef */
|
|
if ($oAttDef->IsFriendlyName()) {
|
|
continue;
|
|
}
|
|
}
|
|
if ($oAttDef instanceof AttributeFriendlyName) {
|
|
continue;
|
|
}
|
|
if (!$oAttDef->IsScalar()) {
|
|
continue;
|
|
}
|
|
$sAttValue = $oObj->GetAsXML($sAttCode, $bLocalize);
|
|
$sRes .= "<$sAttCode>$sAttValue</$sAttCode>\n";
|
|
}
|
|
$sRes .= "</$sObjClass>\n";
|
|
}
|
|
$sRes .= "</Set>\n";
|
|
} else {
|
|
$sRes = '';
|
|
}
|
|
|
|
return $sRes;
|
|
}
|
|
|
|
/**
|
|
* @param $sValue
|
|
* @param string $sSeparator
|
|
* @param string $sTextQualifier
|
|
* @param DBObject $oHostObject
|
|
* @param bool $bLocalize
|
|
* @param bool $bConvertToPlainText
|
|
*
|
|
* @return mixed|string
|
|
* @throws CoreException
|
|
*/
|
|
public function GetAsCSV(
|
|
$sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true,
|
|
$bConvertToPlainText = false
|
|
)
|
|
{
|
|
$sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator');
|
|
$sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator');
|
|
$sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator');
|
|
$sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier');
|
|
|
|
if (is_object($sValue) && ($sValue instanceof ormLinkSet)) {
|
|
$sValue->Rewind();
|
|
$aItems = array();
|
|
while ($oObj = $sValue->Fetch()) {
|
|
$sObjClass = get_class($oObj);
|
|
// Show only relevant information (hide the external key to the current object)
|
|
$aAttributes = array();
|
|
foreach (MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) {
|
|
if ($sAttCode == 'finalclass') {
|
|
if ($sObjClass == $this->GetLinkedClass()) {
|
|
// Simplify the output if the exact class could be determined implicitely
|
|
continue;
|
|
}
|
|
}
|
|
if ($sAttCode == $this->GetExtKeyToMe()) {
|
|
continue;
|
|
}
|
|
if ($oAttDef->IsExternalField()) {
|
|
continue;
|
|
}
|
|
if (!$oAttDef->IsBasedOnDBColumns()) {
|
|
continue;
|
|
}
|
|
if (!$oAttDef->IsScalar()) {
|
|
continue;
|
|
}
|
|
$sAttValue = $oObj->GetAsCSV($sAttCode, $sSepValue, '', $bLocalize);
|
|
if (strlen($sAttValue) > 0) {
|
|
$sAttributeData = str_replace($sAttributeQualifier, $sAttributeQualifier.$sAttributeQualifier,
|
|
$sAttCode.$sSepValue.$sAttValue);
|
|
$aAttributes[] = $sAttributeQualifier.$sAttributeData.$sAttributeQualifier;
|
|
}
|
|
}
|
|
$sAttributes = implode($sSepAttribute, $aAttributes);
|
|
$aItems[] = $sAttributes;
|
|
}
|
|
$sRes = implode($sSepItem, $aItems);
|
|
} else {
|
|
$sRes = '';
|
|
}
|
|
$sRes = str_replace($sTextQualifier, $sTextQualifier.$sTextQualifier, $sRes);
|
|
$sRes = $sTextQualifier.$sRes.$sTextQualifier;
|
|
|
|
return $sRes;
|
|
}
|
|
|
|
/**
|
|
* List the available verbs for 'GetForTemplate'
|
|
*/
|
|
public function EnumTemplateVerbs()
|
|
{
|
|
return array(
|
|
'' => 'Plain text (unlocalized) representation',
|
|
'html' => 'HTML representation (unordered list)',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get various representations of the value, for insertion into a template (e.g. in Notifications)
|
|
*
|
|
* @param mixed $value The current value of the field
|
|
* @param string $sVerb The verb specifying the representation of the value
|
|
* @param DBObject $oHostObject The object
|
|
* @param bool $bLocalize Whether or not to localize the value
|
|
*
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
public function GetForTemplate($value, $sVerb, $oHostObject = null, $bLocalize = true)
|
|
{
|
|
$sRemoteName = $this->IsIndirect() ?
|
|
/** @var AttributeLinkedSetIndirect $this */
|
|
$this->GetExtKeyToRemote().'_friendlyname' : 'friendlyname';
|
|
|
|
$oLinkSet = clone $value; // Workaround/Safety net for Trac #887
|
|
$iLimit = MetaModel::GetConfig()->Get('max_linkset_output');
|
|
$iCount = 0;
|
|
$aNames = array();
|
|
foreach ($oLinkSet as $oItem) {
|
|
if (($iLimit > 0) && ($iCount == $iLimit)) {
|
|
$iTotal = $oLinkSet->Count();
|
|
$aNames[] = '... '.Dict::Format('UI:TruncatedResults', $iCount, $iTotal);
|
|
break;
|
|
}
|
|
$aNames[] = $oItem->Get($sRemoteName);
|
|
$iCount++;
|
|
}
|
|
|
|
switch ($sVerb) {
|
|
case '':
|
|
return implode("\n", $aNames);
|
|
|
|
case 'html':
|
|
return '<ul><li>'.implode("</li><li>", $aNames).'</li></ul>';
|
|
|
|
default:
|
|
throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObject));
|
|
}
|
|
}
|
|
|
|
public function DuplicatesAllowed()
|
|
{
|
|
return false;
|
|
} // No duplicates for 1:n links, never
|
|
|
|
public function GetImportColumns()
|
|
{
|
|
$aColumns = array();
|
|
$aColumns[$this->GetCode()] = 'MEDIUMTEXT'.CMDBSource::GetSqlStringColumnDefinition();
|
|
|
|
return $aColumns;
|
|
}
|
|
|
|
/**
|
|
* @param string $sProposedValue
|
|
* @param bool $bLocalizedValue
|
|
* @param string $sSepItem
|
|
* @param string $sSepAttribute
|
|
* @param string $sSepValue
|
|
* @param string $sAttributeQualifier
|
|
*
|
|
* @return DBObjectSet
|
|
* @throws CSVParserException
|
|
* @throws CoreException
|
|
* @throws CoreUnexpectedValue
|
|
* @throws MissingQueryArgument
|
|
* @throws MySQLException
|
|
* @throws MySQLHasGoneAwayException
|
|
* @throws Exception
|
|
*/
|
|
public function MakeValueFromString(
|
|
$sProposedValue, $bLocalizedValue = false, $sSepItem = null, $sSepAttribute = null, $sSepValue = null,
|
|
$sAttributeQualifier = null
|
|
)
|
|
{
|
|
if (is_null($sSepItem) || empty($sSepItem)) {
|
|
$sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator');
|
|
}
|
|
if (is_null($sSepAttribute) || empty($sSepAttribute)) {
|
|
$sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator');
|
|
}
|
|
if (is_null($sSepValue) || empty($sSepValue)) {
|
|
$sSepValue = MetaModel::GetConfig()->Get('link_set_value_separator');
|
|
}
|
|
if (is_null($sAttributeQualifier) || empty($sAttributeQualifier)) {
|
|
$sAttributeQualifier = MetaModel::GetConfig()->Get('link_set_attribute_qualifier');
|
|
}
|
|
|
|
$sTargetClass = $this->Get('linked_class');
|
|
|
|
$sInput = str_replace($sSepItem, "\n", $sProposedValue);
|
|
$oCSVParser = new CSVParser($sInput, $sSepAttribute, $sAttributeQualifier);
|
|
|
|
$aInput = $oCSVParser->ToArray(0 /* do not skip lines */);
|
|
|
|
$aLinks = array();
|
|
foreach ($aInput as $aRow) {
|
|
// 1st - get the values, split the extkey->searchkey specs, and eventually get the finalclass value
|
|
$aExtKeys = array();
|
|
$aValues = array();
|
|
foreach ($aRow as $sCell) {
|
|
$iSepPos = strpos($sCell, $sSepValue);
|
|
if ($iSepPos === false) {
|
|
// Houston...
|
|
throw new CoreException('Wrong format for link attribute specification', array('value' => $sCell));
|
|
}
|
|
|
|
$sAttCode = trim(substr($sCell, 0, $iSepPos));
|
|
$sValue = substr($sCell, $iSepPos + strlen($sSepValue));
|
|
|
|
if (preg_match('/^(.+)->(.+)$/', $sAttCode, $aMatches)) {
|
|
$sKeyAttCode = $aMatches[1];
|
|
$sRemoteAttCode = $aMatches[2];
|
|
$aExtKeys[$sKeyAttCode][$sRemoteAttCode] = $sValue;
|
|
if (!MetaModel::IsValidAttCode($sTargetClass, $sKeyAttCode)) {
|
|
throw new CoreException('Wrong attribute code for link attribute specification',
|
|
array('class' => $sTargetClass, 'attcode' => $sKeyAttCode));
|
|
}
|
|
/** @var \AttributeExternalKey $oKeyAttDef */
|
|
$oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode);
|
|
$sRemoteClass = $oKeyAttDef->GetTargetClass();
|
|
if (!MetaModel::IsValidAttCode($sRemoteClass, $sRemoteAttCode)) {
|
|
throw new CoreException('Wrong attribute code for link attribute specification',
|
|
array('class' => $sRemoteClass, 'attcode' => $sRemoteAttCode));
|
|
}
|
|
} else {
|
|
if (!MetaModel::IsValidAttCode($sTargetClass, $sAttCode)) {
|
|
throw new CoreException('Wrong attribute code for link attribute specification',
|
|
array('class' => $sTargetClass, 'attcode' => $sAttCode));
|
|
}
|
|
$oAttDef = MetaModel::GetAttributeDef($sTargetClass, $sAttCode);
|
|
$aValues[$sAttCode] = $oAttDef->MakeValueFromString($sValue, $bLocalizedValue, $sSepItem,
|
|
$sSepAttribute, $sSepValue, $sAttributeQualifier);
|
|
}
|
|
}
|
|
|
|
// 2nd - Instanciate the object and set the value
|
|
if (isset($aValues['finalclass'])) {
|
|
$sLinkClass = $aValues['finalclass'];
|
|
if (!is_subclass_of($sLinkClass, $sTargetClass)) {
|
|
throw new CoreException('Wrong class for link attribute specification',
|
|
array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass));
|
|
}
|
|
} elseif (MetaModel::IsAbstract($sTargetClass)) {
|
|
throw new CoreException('Missing finalclass for link attribute specification');
|
|
} else {
|
|
$sLinkClass = $sTargetClass;
|
|
}
|
|
|
|
$oLink = MetaModel::NewObject($sLinkClass);
|
|
foreach ($aValues as $sAttCode => $sValue) {
|
|
$oLink->Set($sAttCode, $sValue);
|
|
}
|
|
|
|
// 3rd - Set external keys from search conditions
|
|
foreach ($aExtKeys as $sKeyAttCode => $aReconciliation) {
|
|
$oKeyAttDef = MetaModel::GetAttributeDef($sTargetClass, $sKeyAttCode);
|
|
$sKeyClass = $oKeyAttDef->GetTargetClass();
|
|
$oExtKeyFilter = new DBObjectSearch($sKeyClass);
|
|
$aReconciliationDesc = array();
|
|
foreach ($aReconciliation as $sRemoteAttCode => $sValue) {
|
|
$oExtKeyFilter->AddCondition($sRemoteAttCode, $sValue, '=');
|
|
$aReconciliationDesc[] = "$sRemoteAttCode=$sValue";
|
|
}
|
|
$oExtKeySet = new DBObjectSet($oExtKeyFilter);
|
|
switch ($oExtKeySet->Count()) {
|
|
case 0:
|
|
$sReconciliationDesc = implode(', ', $aReconciliationDesc);
|
|
throw new CoreException("Found no match",
|
|
array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc));
|
|
break;
|
|
case 1:
|
|
$oRemoteObj = $oExtKeySet->Fetch();
|
|
$oLink->Set($sKeyAttCode, $oRemoteObj->GetKey());
|
|
break;
|
|
default:
|
|
$sReconciliationDesc = implode(', ', $aReconciliationDesc);
|
|
throw new CoreException("Found several matches",
|
|
array('ext_key' => $sKeyAttCode, 'reconciliation' => $sReconciliationDesc));
|
|
// Found several matches, ambiguous
|
|
}
|
|
}
|
|
|
|
// Check (roughly) if such a link is valid
|
|
$aErrors = array();
|
|
foreach (MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) {
|
|
if ($oAttDef->IsExternalKey()) {
|
|
/** @var \AttributeExternalKey $oAttDef */
|
|
if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(),
|
|
$oAttDef->GetTargetClass()))) {
|
|
continue; // Don't check the key to self
|
|
}
|
|
}
|
|
|
|
if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) {
|
|
$aErrors[] = $sAttCode;
|
|
}
|
|
}
|
|
if (count($aErrors) > 0) {
|
|
throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors));
|
|
}
|
|
|
|
$aLinks[] = $oLink;
|
|
}
|
|
$oSet = DBObjectSet::FromArray($sTargetClass, $aLinks);
|
|
|
|
return $oSet;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*
|
|
* @param ormLinkSet $value
|
|
*/
|
|
public function GetForJSON($value)
|
|
{
|
|
$aRet = array();
|
|
if (is_object($value) && ($value instanceof ormLinkSet)) {
|
|
$value->Rewind();
|
|
while ($oObj = $value->Fetch()) {
|
|
$sObjClass = get_class($oObj);
|
|
// Show only relevant information (hide the external key to the current object)
|
|
$aAttributes = array();
|
|
foreach (MetaModel::ListAttributeDefs($sObjClass) as $sAttCode => $oAttDef) {
|
|
if ($sAttCode == 'finalclass') {
|
|
if ($sObjClass == $this->GetLinkedClass()) {
|
|
// Simplify the output if the exact class could be determined implicitely
|
|
continue;
|
|
}
|
|
}
|
|
if ($sAttCode == $this->GetExtKeyToMe()) {
|
|
continue;
|
|
}
|
|
if ($oAttDef->IsExternalField()) {
|
|
continue;
|
|
}
|
|
if (!$oAttDef->IsBasedOnDBColumns()) {
|
|
continue;
|
|
}
|
|
if (!$oAttDef->IsScalar()) {
|
|
continue;
|
|
}
|
|
$attValue = $oObj->Get($sAttCode);
|
|
$aAttributes[$sAttCode] = $oAttDef->GetForJSON($attValue);
|
|
}
|
|
$aRet[] = $aAttributes;
|
|
}
|
|
}
|
|
|
|
return $aRet;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*
|
|
* @return DBObjectSet
|
|
* @throws CoreException
|
|
* @throws CoreUnexpectedValue
|
|
* @throws Exception
|
|
*/
|
|
public function FromJSONToValue($json)
|
|
{
|
|
$sTargetClass = $this->Get('linked_class');
|
|
|
|
$aLinks = array();
|
|
foreach ($json as $aValues) {
|
|
if (isset($aValues['finalclass'])) {
|
|
$sLinkClass = $aValues['finalclass'];
|
|
if (!is_subclass_of($sLinkClass, $sTargetClass)) {
|
|
throw new CoreException('Wrong class for link attribute specification',
|
|
array('requested_class' => $sLinkClass, 'expected_class' => $sTargetClass));
|
|
}
|
|
} elseif (MetaModel::IsAbstract($sTargetClass)) {
|
|
throw new CoreException('Missing finalclass for link attribute specification');
|
|
} else {
|
|
$sLinkClass = $sTargetClass;
|
|
}
|
|
|
|
$oLink = MetaModel::NewObject($sLinkClass);
|
|
foreach ($aValues as $sAttCode => $sValue) {
|
|
$oLink->Set($sAttCode, $sValue);
|
|
}
|
|
|
|
// Check (roughly) if such a link is valid
|
|
$aErrors = array();
|
|
foreach (MetaModel::ListAttributeDefs($sTargetClass) as $sAttCode => $oAttDef) {
|
|
if ($oAttDef->IsExternalKey()) {
|
|
/** @var AttributeExternalKey $oAttDef */
|
|
if (($oAttDef->GetTargetClass() == $this->GetHostClass()) || (is_subclass_of($this->GetHostClass(),
|
|
$oAttDef->GetTargetClass()))) {
|
|
continue; // Don't check the key to self
|
|
}
|
|
}
|
|
|
|
if ($oAttDef->IsWritable() && $oAttDef->IsNull($oLink->Get($sAttCode)) && !$oAttDef->IsNullAllowed()) {
|
|
$aErrors[] = $sAttCode;
|
|
}
|
|
}
|
|
if (count($aErrors) > 0) {
|
|
throw new CoreException("Missing value for mandatory attribute(s): ".implode(', ', $aErrors));
|
|
}
|
|
|
|
$aLinks[] = $oLink;
|
|
}
|
|
$oSet = DBObjectSet::FromArray($sTargetClass, $aLinks);
|
|
|
|
return $oSet;
|
|
}
|
|
|
|
/**
|
|
* @param $proposedValue
|
|
* @param $oHostObj
|
|
*
|
|
* @return mixed
|
|
* @throws Exception
|
|
*/
|
|
public function MakeRealValue($proposedValue, $oHostObj)
|
|
{
|
|
if ($proposedValue === null) {
|
|
$sLinkedClass = $this->GetLinkedClass();
|
|
$aLinkedObjectsArray = array();
|
|
$oSet = DBObjectSet::FromArray($sLinkedClass, $aLinkedObjectsArray);
|
|
|
|
return new ormLinkSet(
|
|
get_class($oHostObj),
|
|
$this->GetCode(),
|
|
$oSet
|
|
);
|
|
}
|
|
|
|
return $proposedValue;
|
|
}
|
|
|
|
/**
|
|
* @param ormLinkSet $val1
|
|
* @param ormLinkSet $val2
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function Equals($val1, $val2)
|
|
{
|
|
if ($val1 === $val2) {
|
|
$bAreEquivalent = true;
|
|
} else {
|
|
$bAreEquivalent = ($val2->HasDelta() === false);
|
|
}
|
|
|
|
return $bAreEquivalent;
|
|
}
|
|
|
|
/**
|
|
* Find the corresponding "link" attribute on the target class, if any
|
|
*
|
|
* @return null | AttributeDefinition
|
|
* @throws Exception
|
|
*/
|
|
public function GetMirrorLinkAttribute()
|
|
{
|
|
$oRemoteAtt = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToMe());
|
|
|
|
return $oRemoteAtt;
|
|
}
|
|
|
|
public static function GetFormFieldClass()
|
|
{
|
|
return '\\Combodo\\iTop\\Form\\Field\\LinkedSetField';
|
|
}
|
|
|
|
/**
|
|
* @param DBObject $oObject
|
|
* @param LinkedSetField $oFormField
|
|
*
|
|
* @return LinkedSetField
|
|
* @throws CoreException
|
|
* @throws DictExceptionMissingString
|
|
* @throws Exception
|
|
*/
|
|
public function MakeFormField(DBObject $oObject, $oFormField = null)
|
|
{
|
|
if ($oFormField === null) {
|
|
$sFormFieldClass = static::GetFormFieldClass();
|
|
$oFormField = new $sFormFieldClass($this->GetCode());
|
|
}
|
|
|
|
// Setting target class
|
|
if (!$this->IsIndirect()) {
|
|
$sTargetClass = $this->GetLinkedClass();
|
|
} else {
|
|
/** @var \AttributeExternalKey $oRemoteAttDef */
|
|
/** @var \AttributeLinkedSetIndirect $this */
|
|
$oRemoteAttDef = MetaModel::GetAttributeDef($this->GetLinkedClass(), $this->GetExtKeyToRemote());
|
|
$sTargetClass = $oRemoteAttDef->GetTargetClass();
|
|
|
|
/** @var \AttributeLinkedSetIndirect $this */
|
|
$oFormField->SetExtKeyToRemote($this->GetExtKeyToRemote());
|
|
}
|
|
$oFormField->SetTargetClass($sTargetClass);
|
|
$oFormField->SetLinkedClass($this->GetLinkedClass());
|
|
$oFormField->SetIndirect($this->IsIndirect());
|
|
// Setting attcodes to display
|
|
$aAttCodesToDisplay = MetaModel::FlattenZList(MetaModel::GetZListItems($sTargetClass, 'list'));
|
|
// - Adding friendlyname attribute to the list is not already in it
|
|
$sTitleAttCode = MetaModel::GetFriendlyNameAttributeCode($sTargetClass);
|
|
if (($sTitleAttCode !== null) && !in_array($sTitleAttCode, $aAttCodesToDisplay)) {
|
|
$aAttCodesToDisplay = array_merge(array($sTitleAttCode), $aAttCodesToDisplay);
|
|
}
|
|
// - Adding attribute properties
|
|
$aAttributesToDisplay = array();
|
|
foreach ($aAttCodesToDisplay as $sAttCodeToDisplay) {
|
|
$oAttDefToDisplay = MetaModel::GetAttributeDef($sTargetClass, $sAttCodeToDisplay);
|
|
$aAttributesToDisplay[$sAttCodeToDisplay] = [
|
|
'att_code' => $sAttCodeToDisplay,
|
|
'label' => $oAttDefToDisplay->GetLabel(),
|
|
];
|
|
}
|
|
$oFormField->SetAttributesToDisplay($aAttributesToDisplay);
|
|
|
|
// Append lnk attributes (filtered from zlist)
|
|
if ($this->IsIndirect()) {
|
|
$aLnkAttDefToDisplay = MetaModel::GetZListAttDefsFilteredForIndirectLinkClass($this->m_sHostClass, $this->m_sCode);
|
|
$aLnkAttributesToDisplay = array();
|
|
foreach ($aLnkAttDefToDisplay as $oLnkAttDefToDisplay) {
|
|
$aLnkAttributesToDisplay[$oLnkAttDefToDisplay->GetCode()] = [
|
|
'att_code' => $oLnkAttDefToDisplay->GetCode(),
|
|
'label' => $oLnkAttDefToDisplay->GetLabel(),
|
|
'mandatory' => !$oLnkAttDefToDisplay->IsNullAllowed(),
|
|
];
|
|
}
|
|
$oFormField->SetLnkAttributesToDisplay($aLnkAttributesToDisplay);
|
|
}
|
|
|
|
parent::MakeFormField($oObject, $oFormField);
|
|
|
|
return $oFormField;
|
|
}
|
|
|
|
public function IsPartOfFingerprint()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*
|
|
* @param ormLinkSet $proposedValue
|
|
*/
|
|
public function HasAValue($proposedValue): bool
|
|
{
|
|
// Protection against wrong value type
|
|
if (false === ($proposedValue instanceof ormLinkSet)) {
|
|
return parent::HasAValue($proposedValue);
|
|
}
|
|
|
|
// We test if there is at least 1 item in the linkset (new or existing), not if an item is being added to it.
|
|
return $proposedValue->Count() > 0;
|
|
}
|
|
|
|
/**
|
|
* SearchSpecificLabel.
|
|
*
|
|
* @param string $sDictEntrySuffix
|
|
* @param string $sDefault
|
|
* @param bool $bUserLanguageOnly
|
|
* @param ...$aArgs
|
|
*
|
|
* @return string
|
|
* @since 3.1.0
|
|
*/
|
|
public function SearchSpecificLabel(string $sDictEntrySuffix, string $sDefault, bool $bUserLanguageOnly, ...$aArgs): string
|
|
{
|
|
try {
|
|
$sNextClass = $this->m_sHostClass;
|
|
|
|
do {
|
|
$sKey = "Class:{$sNextClass}/Attribute:{$this->m_sCode}/{$sDictEntrySuffix}";
|
|
if (Dict::S($sKey, null, $bUserLanguageOnly) !== $sKey) {
|
|
return Dict::Format($sKey, ...$aArgs);
|
|
}
|
|
$sNextClass = MetaModel::GetParentClass($sNextClass);
|
|
} while ($sNextClass !== null);
|
|
|
|
if (Dict::S($sDictEntrySuffix, null, $bUserLanguageOnly) !== $sKey) {
|
|
return Dict::Format($sDictEntrySuffix, ...$aArgs);
|
|
} else {
|
|
return $sDefault;
|
|
}
|
|
}
|
|
catch (Exception $e) {
|
|
ExceptionLog::LogException($e);
|
|
|
|
return $sDefault;
|
|
}
|
|
}
|
|
} |