diff --git a/sources/Application/TwigBase/Controller/Controller.php b/sources/Application/TwigBase/Controller/Controller.php index 1a08df324..d09d47c17 100644 --- a/sources/Application/TwigBase/Controller/Controller.php +++ b/sources/Application/TwigBase/Controller/Controller.php @@ -56,11 +56,14 @@ use ZipArchive; abstract class Controller extends AbstractController { - const ENUM_PAGE_TYPE_HTML = 'html'; - const ENUM_PAGE_TYPE_BASIC_HTML = 'basic_html'; - const ENUM_PAGE_TYPE_AJAX = 'ajax'; + const ENUM_PAGE_TYPE_HTML = 'html'; + const ENUM_PAGE_TYPE_BASIC_HTML = 'basic_html'; + const ENUM_PAGE_TYPE_AJAX = 'ajax'; const ENUM_PAGE_TYPE_TURBO_FORM_AJAX = 'turbo_ajax'; - const ENUM_PAGE_TYPE_SETUP = 'setup'; + const ENUM_PAGE_TYPE_SETUP = 'setup'; + + const TWIG_ERROR = 'error'; + const TWIG_WARNING = 'warning'; /** @var \Twig\Environment */ private $oTwig; @@ -105,12 +108,15 @@ abstract class Controller extends AbstractController /** @var CsrfTokenManager Csrf manager (from Symfony form component @link https://symfony.com/doc/current/security/csrf.html) */ private CsrfTokenManager $oCsrfTokenManager; private ?string $sContentType = null; + private ?string $sPageType = null; /** * Controller constructor. * * @param string $sViewPath Path of the twig files * @param string $sModuleName name of the module (or 'core' if not a module) + * + * @throws \ReflectionException */ public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditionalPaths = []) { @@ -190,12 +196,10 @@ abstract class Controller extends AbstractController $sModulePath = dirname(dirname($this->GetDir())); $this->SetModuleName(basename($sModulePath)); $this->SetViewPath($sModulePath.'/view'); - try - { + try { $this->aDefaultParams = array('sIndexURL' => utils::GetAbsoluteUrlModulePage($this->m_sModule, 'index.php')); } - catch (Exception $e) - { + catch (Exception $e) { IssueLog::Error($e->getMessage()); } } @@ -245,8 +249,7 @@ abstract class Controller extends AbstractController */ public function HandleOperation() { - try - { + try { $this->CheckAccess(); $this->m_sOperation = utils::ReadParam('operation', $this->sDefaultOperation); @@ -261,8 +264,7 @@ abstract class Controller extends AbstractController $this->DisplayBadRequest(); } - catch (Exception $e) - { + catch (Exception $e) { http_response_code(500); $oP = new ErrorPage(Dict::S('UI:PageTitle:FatalError')); $oP->add("

".Dict::S('UI:FatalErrorMessage')."

\n"); @@ -280,8 +282,7 @@ abstract class Controller extends AbstractController */ public function HandleAjaxOperation() { - try - { + try { $this->CheckAccess(); $this->m_sOperation = utils::ReadParam('operation', $this->sDefaultOperation); @@ -296,8 +297,7 @@ abstract class Controller extends AbstractController $this->DisplayPageNotFound(); } - catch (Exception $e) - { + catch (Exception $e) { http_response_code(500); $aResponse = array('sError' => $e->getMessage()); echo json_encode($aResponse); @@ -312,6 +312,7 @@ abstract class Controller extends AbstractController } $this->$sMethodName(); + return true; } @@ -334,13 +335,12 @@ abstract class Controller extends AbstractController } /** - * @since 3.0.0 N°3606 - Adapt TwigBase Controller for combodo-monitoring extension * @throws \Exception + * @since 3.0.0 N°3606 - Adapt TwigBase Controller for combodo-monitoring extension */ protected function CheckAccess() { - if ($this->bCheckDemoMode && MetaModel::GetConfig()->Get('demo_mode')) - { + if ($this->bCheckDemoMode && MetaModel::GetConfig()->Get('demo_mode')) { throw new Exception("Sorry, iTop is in demonstration mode: this feature is disabled."); } @@ -348,31 +348,30 @@ abstract class Controller extends AbstractController $sConfiguredAccessTokenValue = empty($this->sAccessTokenConfigParamId) ? "" : trim(MetaModel::GetConfig()->GetModuleSetting($sExecModule, $this->sAccessTokenConfigParamId)); - if (empty($sExecModule) || empty($sConfiguredAccessTokenValue)){ + if (empty($sExecModule) || empty($sConfiguredAccessTokenValue)) { LoginWebPage::DoLogin($this->bMustBeAdmin); } else { //token mode without login required //N°7147 - Error HTTP 500 due to access_token not URL decoded $sPassedToken = utils::ReadPostedParam($this->sAccessTokenConfigParamId, null, false, 'raw_data'); - if (is_null($sPassedToken)){ + if (is_null($sPassedToken)) { $sPassedToken = utils::ReadParam($this->sAccessTokenConfigParamId, null, false, 'raw_data'); } $sDecodedPassedToken = urldecode($sPassedToken); - if ($sDecodedPassedToken !== $sConfiguredAccessTokenValue){ + if ($sDecodedPassedToken !== $sConfiguredAccessTokenValue) { $sMsg = "Invalid token passed under '$this->sAccessTokenConfigParamId' http param to reach '$sExecModule' page."; IssueLog::Error($sMsg, null, [ 'sHtmlDecodedToken' => $sDecodedPassedToken, - 'conf param ID' => $this->sAccessTokenConfigParamId, + 'conf param ID' => $this->sAccessTokenConfigParamId, ] ); throw new Exception("Invalid token"); } } - if (!empty($this->sMenuId)) - { + if (!empty($this->sMenuId)) { ApplicationMenu::CheckMenuIdEnabled($this->sMenuId); } } @@ -482,9 +481,9 @@ abstract class Controller extends AbstractController $this->DisplayPage($aParams, $sTemplateName, 'setup'); } - public function DisplayTurboAjaxPage ($aParams = array(), $sTemplateName = 'turbo-ajax-update.html.twig') + public function DisplayTurboAjaxPage($aParams = array()) { - $this->DisplayPage($aParams, $sTemplateName, self::ENUM_PAGE_TYPE_TURBO_FORM_AJAX); + $this->DisplayPage($aParams, 'application/forms/turbo-ajax-update', self::ENUM_PAGE_TYPE_TURBO_FORM_AJAX); } /** @@ -498,37 +497,42 @@ abstract class Controller extends AbstractController * * @throws \Exception */ - public function DisplayPage($aParams = array(), $sTemplateName = null, $sPageType = 'html') + public function DisplayPage($aParams = array(), $sTemplateName = null, $sPageType = self::ENUM_PAGE_TYPE_HTML) { if (empty($sTemplateName)) { $sTemplateName = $this->m_sOperation; } + $this->sPageType = $sPageType; + $aParams = array_merge($this->GetDefaultParameters(), $aParams); $this->CreatePage($sPageType); - $sHTMLContent = $this->RenderTemplate($aParams, $sTemplateName, 'html', $sErrorMsg); + $sHTMLContent = $this->RenderTemplate($aParams, $sTemplateName, 'html', $aErrors); if ($sHTMLContent !== false) { $this->AddToPage($sHTMLContent); } - $sJSScript = $this->RenderTemplate($aParams, $sTemplateName, 'js', $sErrorMsg); + $sJSScript = $this->RenderTemplate($aParams, $sTemplateName, 'js', $aErrors); if ($sJSScript !== false) { $this->AddScriptToPage($sJSScript); } - $sReadyScript = $this->RenderTemplate($aParams, $sTemplateName, 'ready.js', $sErrorMsg); + $sReadyScript = $this->RenderTemplate($aParams, $sTemplateName, 'ready.js', $aErrors); if ($sReadyScript !== false) { $this->AddReadyScriptToPage($sReadyScript); } - $sStyle = $this->RenderTemplate($aParams, $sTemplateName, 'css', $sErrorMsg); + $sStyle = $this->RenderTemplate($aParams, $sTemplateName, 'css', $aErrors); if ($sStyle !== false) { $this->AddStyleToPage($sStyle); } if ($sHTMLContent === false && $sJSScript === false && $sReadyScript === false && $sStyle === false) { - if (utils::IsNullOrEmptyString($sErrorMsg)) { - $sErrorMsg = "Missing TWIG template for $sTemplateName"; + if (is_null($aErrors) || count($aErrors) === 0) { + $aErrors[self::TWIG_ERROR] = "Missing TWIG template for $sTemplateName"; } - IssueLog::Error($sErrorMsg); - $this->AddToPage($this->oTwig->render('application/forms/itop_error.html.twig', ['sControllerError' => $sErrorMsg])); + IssueLog::Error(implode("\n",$aErrors[self::TWIG_ERROR] ?? [])."\n".implode("\n",$aErrors[self::TWIG_WARNING] ?? [])); + } else { + // Ignore warnings + $aErrors[self::TWIG_WARNING] = []; } + $this->RenderErrors($aErrors); $this->ManageDebugExtensions($aParams, $sPageType); @@ -553,6 +557,7 @@ abstract class Controller extends AbstractController } $this->SetContentTypeToPage(); $this->OutputPage(); + $this->sPageType = null; } /** @@ -569,8 +574,7 @@ abstract class Controller extends AbstractController $oKpi = new ExecutionKPI(); http_response_code($iResponseCode); header('Content-Type: application/json'); - foreach ($aHeaders as $sHeader) - { + foreach ($aHeaders as $sHeader) { header($sHeader); } $sJSON = json_encode($aParams); @@ -623,16 +627,13 @@ abstract class Controller extends AbstractController $sArchiveFileFullPath = tempnam(SetupUtils::GetTmpDir(), 'itop_download-').'.zip'; $oArchive = new ZipArchive(); $oArchive->open($sArchiveFileFullPath, ZipArchive::CREATE); - foreach ($aFiles as $sFile) - { + foreach ($aFiles as $sFile) { $oArchive->addFile($sFile, basename($sFile)); } $oArchive->close(); - if ($bUnlinkFiles) - { - foreach ($aFiles as $sFile) - { + if ($bUnlinkFiles) { + foreach ($aFiles as $sFile) { unlink($sFile); } } @@ -645,8 +646,7 @@ abstract class Controller extends AbstractController $sFileMimeType = utils::GetFileMimeType($sFilePath); header('Content-Type: '.$sFileMimeType); - if ($bFileTransfer) - { + if ($bFileTransfer) { header('Content-Disposition: attachment; filename="'.$sDownloadArchiveName.'"'); } @@ -662,8 +662,7 @@ abstract class Controller extends AbstractController readfile($sFilePath); - if ($bRemoveFile) - { + if ($bRemoveFile) { unlink($sFilePath); } exit(0); @@ -675,6 +674,7 @@ abstract class Controller extends AbstractController * @api * * @param string $sScript Script path to link + * * @since 3.2.0 $sScript must be absolute URI */ public function AddLinkedScript($sScript) @@ -688,6 +688,7 @@ abstract class Controller extends AbstractController * @api * * @param string $sStylesheet Stylesheet path to link + * * @since 3.2.0 $sScript must be absolute URI */ public function AddLinkedStylesheet($sStylesheet) @@ -710,13 +711,13 @@ abstract class Controller extends AbstractController /** * Add an AJAX tab to the current page * - * @param string $sCode Code of the tab + * @api + * * @param string $sURL URL to call when the tab is activated * @param bool $bCache If true, cache the result for the current web page * @param string $sLabel Label of the tab (if null the code is translated) * - * @api - * + * @param string $sCode Code of the tab */ public function AddAjaxTab($sCode, $sURL, $bCache = true, $sLabel = null) { @@ -728,6 +729,7 @@ abstract class Controller extends AbstractController /** * @param array $aBlockParams + * * @since 3.0.0 */ public function SetBlockParams(array $aBlockParams) @@ -747,18 +749,20 @@ abstract class Controller extends AbstractController } /** - * @since 2.7.7 3.0.1 3.1.0 N°4760 method creation * @see Controller::SetBreadCrumbEntry() to set breadcrumb content (by default will be title) + * @since 2.7.7 3.0.1 3.1.0 N°4760 method creation */ - public function DisableBreadCrumb() { + public function DisableBreadCrumb() + { $this->bIsBreadCrumbEnabled = false; } /** - * @since 2.7.7 3.0.1 3.1.0 N°4760 method creation * @see iTopWebPage::SetBreadCrumbEntry() + * @since 2.7.7 3.0.1 3.1.0 N°4760 method creation */ - public function SetBreadCrumbEntry($sId, $sLabel, $sDescription, $sUrl = '', $sIcon = '') { + public function SetBreadCrumbEntry($sId, $sLabel, $sDescription, $sUrl = '', $sIcon = '') + { $this->aBreadCrumbEntry = [$sId, $sLabel, $sDescription, $sUrl, $sIcon]; } @@ -779,7 +783,7 @@ abstract class Controller extends AbstractController */ public function GetFormBuilder(AbstractFormBlock $oFormBlock, mixed $data = null): FormBuilderInterface { - return $this->oFormFactoryBuilder->getFormFactory()->createNamedBuilder($oFormBlock->GetName(), $oFormBlock->GetFormType(), $data,$oFormBlock->GetOptions()); + return $this->oFormFactoryBuilder->getFormFactory()->createNamedBuilder($oFormBlock->GetName(), $oFormBlock->GetFormType(), $data, $oFormBlock->GetOptions()); } /** @@ -797,7 +801,8 @@ abstract class Controller extends AbstractController if (is_null($data)) { $data = $type::GetDefaultData(); } - return $this->GetFormBuilder($type, $data,$options)->getForm(); + + return $this->GetFormBuilder($type, $data, $options)->getForm(); } /** @@ -808,35 +813,34 @@ abstract class Controller extends AbstractController * @return string|false * @throws \Exception */ - private function RenderTemplate(array $aParams, string $sName, string $sTemplateFileExtension, string &$sErrorMsg = null): string|false + private function RenderTemplate(array $aParams, string $sName, string $sTemplateFileExtension, ?array &$aErrors): string|false { + if (is_null($aErrors)) { + $aErrors = []; + } $sTemplateFile = $sName.'.'.$sTemplateFileExtension.'.twig'; - if (empty($this->oTwig)) - { + if (empty($this->oTwig)) { throw new Exception('Not initialized. Call Controller::InitFromModule() or Controller::SetViewPath() before any display'); } - try - { + try { return $this->oTwig->render($sTemplateFile, $aParams); } catch (SyntaxError $e) { IssueLog::Error($e->getMessage().' - file: '.$e->getFile().'('.$e->getLine().')'); - return $this->oTwig->render('application/forms/itop_error.html.twig', ['sControllerError' => $e->getMessage()]); + $aErrors[self::TWIG_ERROR][] = $e->getMessage(); + return false; } catch (Exception $e) { $sExceptionMessage = $e->getMessage(); if (str_contains($sExceptionMessage, 'at line')) { IssueLog::Error($sExceptionMessage); - return $this->oTwig->render('application/forms/itop_error.html.twig', ['sControllerError' => $sExceptionMessage]); + $aErrors[self::TWIG_ERROR][] = $sExceptionMessage; + return false; } - if (!str_contains($sExceptionMessage, 'Unable to find template')) - { + if (!str_contains($sExceptionMessage, 'Unable to find template')) { IssueLog::Error($sExceptionMessage); } - if (is_null($sErrorMsg)) { - $sErrorMsg = ''; - } - $sErrorMsg .= $sExceptionMessage."\n"; + $aErrors[self::TWIG_WARNING][] = $sExceptionMessage; } return false; @@ -849,8 +853,7 @@ abstract class Controller extends AbstractController */ private function CreatePage(string $sPageType): void { - switch ($sPageType) - { + switch ($sPageType) { case self::ENUM_PAGE_TYPE_HTML: $this->oPage = new iTopWebPage($this->GetOperationTitle(), false); $this->oPage->add_http_headers(); @@ -885,7 +888,7 @@ abstract class Controller extends AbstractController } $this->oTwig->addGlobal('UIBlockParent', [$this->oPage]); $this->oTwig->addGlobal('oPage', $this->oPage); - $this->oTwig->addGlobal('debug', utils::IsDevelopmentEnvironment()); + //$this->oTwig->addGlobal('debug', utils::IsDevelopmentEnvironment()); } /** @@ -959,11 +962,10 @@ abstract class Controller extends AbstractController } } - - /** * @param string $sKey * @param $value + * * @since 3.0.0 */ private function SetBlockParamToPage(string $sKey, $value) @@ -1002,7 +1004,7 @@ abstract class Controller extends AbstractController if (!in_array($sPageType, [self::ENUM_PAGE_TYPE_HTML, self::ENUM_PAGE_TYPE_AJAX, self::ENUM_PAGE_TYPE_TURBO_FORM_AJAX])) { return; } - $sContent = ''; + $aProfilesInfo = []; foreach (InterfaceDiscovery::GetInstance()->FindItopClasses(iProfilerExtension::class) as $sExtension) { /** @var \Combodo\iTop\Application\TwigBase\Controller\iProfilerExtension $oExtensionInstance */ $oExtensionInstance = $sExtension::GetInstance(); @@ -1021,17 +1023,48 @@ abstract class Controller extends AbstractController if (is_array($aSaas)) { $this->aSaas = array_merge($this->aSaas, $aSaas); } - $sContent .= $this->oTwig->render($sDebugTemplate, $aDebugParams); + $aProfilesInfo[] = ['sTemplate' => $sDebugTemplate, 'aProfileData' => $aDebugParams]; } } - if ($sContent === '') { + if (count($aProfilesInfo) === 0) { return; } if ($sPageType === self::ENUM_PAGE_TYPE_HTML || $sPageType === self::ENUM_PAGE_TYPE_AJAX) { - $this->AddToPage($this->oTwig->render('application/forms/itop_debug.html.twig', ['sProfilerContent' => $sContent])); + $this->AddToPage($this->oTwig->render('application/forms/itop_debug.html.twig', ['aProfilesInfo' => $aProfilesInfo])); } elseif ($sPageType === self::ENUM_PAGE_TYPE_TURBO_FORM_AJAX) { - $this->AddToPage($this->oTwig->render('application/forms/itop_debug_update.html.twig', ['sProfilerContent' => $sContent])); + $this->AddToPage($this->oTwig->render('application/forms/itop_debug_update.html.twig', ['aProfilesInfo' => $aProfilesInfo])); } } + + /** + * render error message + * + * @param array $aErrors + * + * @return string + * @throws \Twig\Error\LoaderError + * @throws \Twig\Error\RuntimeError + * @throws \Twig\Error\SyntaxError + */ + public function RenderErrors(array $aErrors): void + { + if (is_null($this->sPageType)) { + return; + } + $sErrorMsg = ''; + if (count($aErrors[self::TWIG_ERROR] ?? []) > 0) { + $sErrorMsg .= implode("\n", $aErrors[self::TWIG_ERROR]); + $sErrorMsg .= "\n"; + } + if (count($aErrors[self::TWIG_WARNING] ?? []) > 0) { + $sErrorMsg .= implode("\n", $aErrors[self::TWIG_WARNING]); + } + + if ($this->sPageType === self::ENUM_PAGE_TYPE_TURBO_FORM_AJAX) { + $this->AddToPage($this->oTwig->render('application/forms/itop_error_update.html.twig', ['sControllerError' => $sErrorMsg])); + } + + $this->AddToPage($this->oTwig->render('application/forms/itop_error.html.twig', ['sControllerError' => $sErrorMsg])); + } } diff --git a/sources/Application/WebPage/iTopWebPage.php b/sources/Application/WebPage/iTopWebPage.php index a3c738fae..5692ff562 100644 --- a/sources/Application/WebPage/iTopWebPage.php +++ b/sources/Application/WebPage/iTopWebPage.php @@ -33,11 +33,9 @@ use Combodo\iTop\Renderer\Console\ConsoleBlockRenderer; use ContextTag; use DateTimeFormat; use DBSearch; -use DeprecatedCallsLog; use Dict; use ExecutionKPI; use InlineImage; -use iPageUIBlockExtension; use MetaModel; use UserRights; use utils; @@ -756,7 +754,7 @@ HTML; } } - // Render HTKL content + // Render HTML content $sHtml = $this->RenderContent(); // Echo global HTML diff --git a/sources/Forms/Block/AbstractFormBlock.php b/sources/Forms/Block/AbstractFormBlock.php index 3acde74e4..826602ed1 100644 --- a/sources/Forms/Block/AbstractFormBlock.php +++ b/sources/Forms/Block/AbstractFormBlock.php @@ -134,8 +134,7 @@ abstract class AbstractFormBlock */ public function AddInput(string $sName, string $sType): void { - $oFormInput = new FormInput($sName, $sType); - $oFormInput->SetOwnerBlock($this); + $oFormInput = new FormInput($sName, $sType, $this); $this->aFormInputs[$oFormInput->GetName()] = $oFormInput; } @@ -167,8 +166,7 @@ abstract class AbstractFormBlock */ public function AddOutput(string $sName, string $sType, AbstractConverter $oConverter = null): void { - $oFormOutput = new FormOutput($sName, $sType, $oConverter); - $oFormOutput->SetOwnerBlock($this); + $oFormOutput = new FormOutput($sName, $sType, $this, $oConverter); $this->aFormOutputs[$oFormOutput->GetName()] = $oFormOutput; } @@ -296,6 +294,7 @@ abstract class AbstractFormBlock return true; } } + return false; } @@ -333,6 +332,7 @@ abstract class AbstractFormBlock $aBindings[$oFormOutput->GetName()] = $oFormOutput->GetBinding(); } } + return $aBindings; } @@ -389,11 +389,11 @@ abstract class AbstractFormBlock /** Iterate throw output @var FormOutput $oFormOutput */ foreach ($this->aFormOutputs as $oFormOutput) { - try{ + try { // Compute the output value $oFormOutput->ComputeValue($sEventType, $oData); } - catch(IOException $oException){ + catch (IOException $oException) { IssueLog::Exception(sprintf('Unable to compute values for output %s of block %s', $oFormOutput->GetName(), $this->GetName()), $oException); } diff --git a/sources/Forms/Block/Base/ChoiceFormBlock.php b/sources/Forms/Block/Base/ChoiceFormBlock.php index 6ee4c535d..a6fea1b67 100644 --- a/sources/Forms/Block/Base/ChoiceFormBlock.php +++ b/sources/Forms/Block/Base/ChoiceFormBlock.php @@ -15,13 +15,13 @@ use Combodo\iTop\Forms\Block\FormType\ChoiceFormType; */ class ChoiceFormBlock extends AbstractFormBlock { - /** @inheritdoc */ + /** @inheritdoc */ public function GetFormType(): string { return ChoiceFormType::class; } - /** @inheritdoc */ + /** @inheritdoc */ public function InitOptions(): array { return [ diff --git a/sources/Forms/Block/Base/CollectionBlock.php b/sources/Forms/Block/Base/CollectionBlock.php index 2f49762dc..daab9711c 100644 --- a/sources/Forms/Block/Base/CollectionBlock.php +++ b/sources/Forms/Block/Base/CollectionBlock.php @@ -22,13 +22,13 @@ class CollectionBlock extends AbstractFormBlock } - /** @inheritdoc */ + /** @inheritdoc */ public function GetFormType(): string { return CollectionType::class; } - /** @inheritdoc */ + /** @inheritdoc */ public function InitOptions(): array { $sBlockEntryType = $this->GetOptions()['block_entry_type']; @@ -39,7 +39,7 @@ class CollectionBlock extends AbstractFormBlock $oBlock = new ($sBlockEntryType)('prototype', $sBlockEntryOptions); return [ - 'entry_type' => $oBlock->GetFormType() + 'entry_type' => $oBlock->GetFormType(), ]; } diff --git a/sources/Forms/Block/Base/FormBlock.php b/sources/Forms/Block/Base/FormBlock.php index b8a83ffce..3661b1930 100644 --- a/sources/Forms/Block/Base/FormBlock.php +++ b/sources/Forms/Block/Base/FormBlock.php @@ -8,6 +8,7 @@ namespace Combodo\iTop\Forms\Block\Base; use Combodo\iTop\Forms\Block\AbstractFormBlock; use Combodo\iTop\Forms\FormsException; +use Exception; use Symfony\Component\Form\Extension\Core\Type\FormType; /** @@ -40,17 +41,17 @@ class FormBlock extends AbstractFormBlock } } - /** @inheritdoc */ + /** @inheritdoc */ public function GetFormType(): string { return FormType::class; } - /** @inheritdoc */ + /** @inheritdoc */ public function InitOptions(): array { return [ - 'compound' => true + 'compound' => true, ]; } @@ -68,6 +69,7 @@ class FormBlock extends AbstractFormBlock $oSubFormBlock = new ($sType)($sName, $aOptions); $this->aChildrenBlocks[$sName] = $oSubFormBlock; $oSubFormBlock->SetParent($this); + return $oSubFormBlock; } diff --git a/sources/Forms/Block/Base/TextAreaFormBlock.php b/sources/Forms/Block/Base/TextAreaFormBlock.php index c9f837c59..a3c648644 100644 --- a/sources/Forms/Block/Base/TextAreaFormBlock.php +++ b/sources/Forms/Block/Base/TextAreaFormBlock.php @@ -15,13 +15,13 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType; */ class TextAreaFormBlock extends AbstractFormBlock { - /** @inheritdoc */ + /** @inheritdoc */ public function GetFormType(): string { return TextareaType::class; } - /** @inheritdoc */ + /** @inheritdoc */ public function InitOptions(): array { return []; diff --git a/sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php b/sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php index 1df307414..afbca1988 100644 --- a/sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php +++ b/sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php @@ -8,10 +8,9 @@ namespace Combodo\iTop\Forms\Block\DataModel; use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock; use Combodo\iTop\Forms\Block\FormBlockException; +use Combodo\iTop\Forms\Block\IO\Converter\StringToAttributeConverter; use Combodo\iTop\Forms\Block\IO\Format\AttributeIOFormat; use Combodo\iTop\Forms\Block\IO\Format\ClassIOFormat; -use Combodo\iTop\Forms\Block\IO\Converter\StringToAttributeConverter; -use Combodo\iTop\Forms\FormType\AttributeFormType; use CoreException; use MetaModel; @@ -28,22 +27,23 @@ class AttributeChoiceFormBlock extends ChoiceFormBlock // outputs public const OUTPUT_ATTRIBUTE = 'attribute'; - /** @inheritdoc */ + /** @inheritdoc */ public function InitOptions(): array { $aOptions = parent::InitOptions(); + // $aOptions['placeholder'] = 'Select an attribute...'; return $aOptions; } - /** @inheritdoc */ + /** @inheritdoc */ public function InitInputs(): void { parent::InitInputs(); $this->AddInput(self::INPUT_CLASS_NAME, ClassIOFormat::class); } - /** @inheritdoc */ + /** @inheritdoc */ public function InitOutputs(): void { parent::InitOutputs(); @@ -67,11 +67,12 @@ class AttributeChoiceFormBlock extends ChoiceFormBlock $aOptions = parent::GetOptions(); $oValue = $this->GetInput(self::INPUT_CLASS_NAME)->Value(); - if($oValue == '') + if ($oValue == '') { return $aOptions; + } $aAttributeCodes = MetaModel::GetAttributesList($oValue); - $aAttributeCodes = array_combine($aAttributeCodes, $aAttributeCodes) ; + $aAttributeCodes = array_combine($aAttributeCodes, $aAttributeCodes); $aOptions['choices'] = $aAttributeCodes; return $aOptions; diff --git a/sources/Forms/Block/DataModel/AttributeValueChoiceFormBlock.php b/sources/Forms/Block/DataModel/AttributeValueChoiceFormBlock.php index 19c485358..833babc0d 100644 --- a/sources/Forms/Block/DataModel/AttributeValueChoiceFormBlock.php +++ b/sources/Forms/Block/DataModel/AttributeValueChoiceFormBlock.php @@ -11,7 +11,6 @@ use Combodo\iTop\Forms\Block\FormBlockException; use Combodo\iTop\Forms\Block\IO\Format\AttributeIOFormat; use Combodo\iTop\Forms\Block\IO\Format\ClassIOFormat; use Combodo\iTop\Forms\Block\IO\Format\RawFormat; -use Combodo\iTop\Forms\FormType\AttributeValueFormType; use Exception; use MetaModel; @@ -27,21 +26,21 @@ class AttributeValueChoiceFormBlock extends ChoiceFormBlock public const INPUT_ATTRIBUTE = 'attribute'; // Outputs - public const OUTPUT_VALUE = 'value'; + public const OUTPUT_VALUE = 'value'; - /** @inheritdoc */ + /** @inheritdoc */ public function InitOptions(array &$aOptions = []): array { $aOptions['multiple'] = true; $aOptions['attr'] = [ - 'size' => 5, - 'style' => 'height: auto;' + 'size' => 5, + 'style' => 'height: auto;', ]; return $aOptions; } - /** @inheritdoc */ + /** @inheritdoc */ public function InitInputs(): void { parent::InitInputs(); @@ -49,7 +48,7 @@ class AttributeValueChoiceFormBlock extends ChoiceFormBlock $this->AddInput(self::INPUT_ATTRIBUTE, AttributeIOFormat::class); } - /** @inheritdoc */ + /** @inheritdoc */ public function InitOutputs(): void { parent::InitOutputs(); @@ -64,12 +63,12 @@ class AttributeValueChoiceFormBlock extends ChoiceFormBlock { $bDependentOk = $this->GetInput(self::INPUT_CLASS_NAME)->Value() != '' && $this->GetInput(self::INPUT_ATTRIBUTE)->Value() != ''; - if($bDependentOk) { + if ($bDependentOk) { $oAttDef = MetaModel::GetAttributeDef($this->GetInput(self::INPUT_CLASS_NAME)->Value()->__toString(), $this->GetInput(self::INPUT_ATTRIBUTE)->Value()->__toString()); $aValues = $oAttDef->GetAllowedValues(); + return $aValues != null; - } - else{ + } else { return false; } } @@ -82,18 +81,21 @@ class AttributeValueChoiceFormBlock extends ChoiceFormBlock $aOptions = parent::GetOptions(); $oClassName = $this->GetInput(self::INPUT_CLASS_NAME)->Value(); - if($oClassName == '') + if ($oClassName == '') { return $aOptions; + } $oAttribute = $this->GetInput(self::INPUT_ATTRIBUTE)->Value(); - if($oAttribute == '') + if ($oAttribute == '') { return $aOptions; + } $oAttDef = MetaModel::GetAttributeDef(strval($oClassName), strval($oAttribute)); $aValues = $oAttDef->GetAllowedValues(); - if($aValues === null) + if ($aValues === null) { return $aOptions; + } $aOptions['choices'] = array_flip($aValues ?? []); diff --git a/sources/Forms/Block/DataModel/OqlFormBlock.php b/sources/Forms/Block/DataModel/OqlFormBlock.php index 95258ebd3..48cf7877e 100644 --- a/sources/Forms/Block/DataModel/OqlFormBlock.php +++ b/sources/Forms/Block/DataModel/OqlFormBlock.php @@ -7,9 +7,9 @@ namespace Combodo\iTop\Forms\Block\DataModel; use Combodo\iTop\Forms\Block\Base\TextAreaFormBlock; -use Combodo\iTop\Forms\Block\IO\Format\ClassIOFormat; -use Combodo\iTop\Forms\Block\IO\Converter\OqlToClassConverter; use Combodo\iTop\Forms\Block\FormType\OqlFormType; +use Combodo\iTop\Forms\Block\IO\Converter\OqlToClassConverter; +use Combodo\iTop\Forms\Block\IO\Format\ClassIOFormat; /** * Form block for oql expression. @@ -21,27 +21,27 @@ class OqlFormBlock extends TextAreaFormBlock // outputs public const OUTPUT_SELECTED_CLASS = 'selected_class'; - /** @inheritdoc */ + /** @inheritdoc */ public function GetFormType(): string { return OqlFormType::class; } - /** @inheritdoc */ + /** @inheritdoc */ public function InitOutputs(): void { parent::InitOutputs(); $this->AddOutput(self::OUTPUT_SELECTED_CLASS, ClassIOFormat::class, new OqlToClassConverter()); } - /** @inheritdoc */ + /** @inheritdoc */ public function InitOptions(): array { $aOptions = parent::InitOptions(); $aOptions['with_ai_button'] = true; + return $aOptions; } - } \ No newline at end of file diff --git a/sources/Forms/Block/FormType/AttributeValueFormType.php b/sources/Forms/Block/FormType/AttributeValueFormType.php index e49e9ae99..2bc5bf174 100644 --- a/sources/Forms/Block/FormType/AttributeValueFormType.php +++ b/sources/Forms/Block/FormType/AttributeValueFormType.php @@ -6,6 +6,7 @@ namespace Combodo\iTop\Forms\Block\FormType; +use MetaModel; use Symfony\Component\Form\AbstractType; class AttributeValueFormType extends AbstractType @@ -19,14 +20,14 @@ class AttributeValueFormType extends AbstractType { $aValues = []; - if(!empty($inputs['attribute'])){ - $oAttDef = \MetaModel::GetAttributeDef($inputs['object_class'], $inputs['attribute']); + if (!empty($inputs['attribute'])) { + $oAttDef = MetaModel::GetAttributeDef($inputs['object_class'], $inputs['attribute']); $aValues = $oAttDef->GetAllowedValues(); $aValues = $aValues !== null ? array_combine($aValues, $aValues) : []; } return [ - 'choices' => $aValues + 'choices' => $aValues, ]; } } \ No newline at end of file diff --git a/sources/Forms/Block/FormType/ChoiceFormType.php b/sources/Forms/Block/FormType/ChoiceFormType.php index 009fc9f97..ccb9a31a3 100644 --- a/sources/Forms/Block/FormType/ChoiceFormType.php +++ b/sources/Forms/Block/FormType/ChoiceFormType.php @@ -19,22 +19,22 @@ use Symfony\Component\Form\FormEvents; */ class ChoiceFormType extends AbstractType { - /** @inheritdoc */ + /** @inheritdoc */ public function getParent(): string { return ChoiceType::class; } - /** @inheritdoc */ + /** @inheritdoc */ public function buildForm(FormBuilderInterface $builder, array $options): void { // on preset data - $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) use ($options){ + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) use ($options) { - if($options['multiple'] === false && $options['required'] === true) { + if ($options['multiple'] === false && $options['required'] === true) { if ($event->getData() === null) { $FirstElement = array_shift($options['choices']); - if($FirstElement !== null){ + if ($FirstElement !== null) { $event->setData($FirstElement); } } @@ -43,10 +43,10 @@ class ChoiceFormType extends AbstractType }); // on pre submit - $builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) use ($options){ + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $event) use ($options) { // reset value if not in available choices - if(!empty($event->getData()) && !$this->CheckValue($event->getData(), $options)){ + if (!empty($event->getData()) && !$this->CheckValue($event->getData(), $options)) { $event->getForm()->addError(new FormError("The value has been reset because it is not part of the available choices anymore.")); $event->setData(null); } @@ -63,16 +63,15 @@ class ChoiceFormType extends AbstractType private function CheckValue($oValue, $options): bool { // Check multi selection values - if($options['multiple'] === true){ - foreach ($oValue as $v){ - if(!in_array($v, $options['choices'])){ + if ($options['multiple'] === true) { + foreach ($oValue as $v) { + if (!in_array($v, $options['choices'])) { return false; } } - } - // Check single selection values - else{ - if(!in_array($oValue, $options['choices'])){ + } // Check single selection values + else { + if (!in_array($oValue, $options['choices'])) { return false; } } @@ -81,5 +80,4 @@ class ChoiceFormType extends AbstractType } - } \ No newline at end of file diff --git a/sources/Forms/Block/FormType/CollectionFormType.php b/sources/Forms/Block/FormType/CollectionFormType.php index 3626a8bab..0bc5151b4 100644 --- a/sources/Forms/Block/FormType/CollectionFormType.php +++ b/sources/Forms/Block/FormType/CollectionFormType.php @@ -7,13 +7,7 @@ namespace Combodo\iTop\Forms\Block\FormType; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Event\PreSetDataEvent; -use Symfony\Component\Form\Event\PreSubmitEvent; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormError; -use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -21,7 +15,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; */ class CollectionFormType extends AbstractType { - /** @inheritdoc */ + /** @inheritdoc */ public function getParent(): string { return CollectionType::class; diff --git a/sources/Forms/Block/FormType/OqlFormType.php b/sources/Forms/Block/FormType/OqlFormType.php index 38f8dcd2f..d47fcec87 100644 --- a/sources/Forms/Block/FormType/OqlFormType.php +++ b/sources/Forms/Block/FormType/OqlFormType.php @@ -34,38 +34,40 @@ class OqlFormType extends AbstractType ]); $resolver->setDefault('outputs', array( - 'selected_class' => function($oData) { - if($oData === null) + 'selected_class' => function ($oData) { + if ($oData === null) { return null; + } // extract selected class preg_match('/SELECT\s+(\w+)/', $oData, $aMatches); + return $aMatches[1] ?? null; - } + }, )); $resolver->setDefined('with_ai_button'); } - /** @inheritdoc */ + /** @inheritdoc */ public function buildForm(FormBuilderInterface $builder, array $options): void { parent::buildForm($builder, $options); // on pre submit - $builder->addEventListener(FormEvents::POST_SUBMIT, function (PostSubmitEvent $event) use ($options){ + $builder->addEventListener(FormEvents::POST_SUBMIT, function (PostSubmitEvent $event) use ($options) { - try{ + try { $oClassConverter = new OqlToClassConverter(); $oClassConverter->Convert($event->getData()); } - catch(Exception $e){ + catch (Exception $e) { $event->getForm()->addError(new FormError($e->getMessage())); } }); } - /** @inheritdoc */ + /** @inheritdoc */ public function buildView(FormView $view, FormInterface $form, array $options): void { parent::buildView($view, $form, $options); diff --git a/sources/Forms/Block/IO/AbstractFormIO.php b/sources/Forms/Block/IO/AbstractFormIO.php index cb960bdd1..4ffbf0932 100644 --- a/sources/Forms/Block/IO/AbstractFormIO.php +++ b/sources/Forms/Block/IO/AbstractFormIO.php @@ -38,10 +38,11 @@ class AbstractFormIO * @param string $sName name of the IO * @param string $sType type of the IO */ - public function __construct(string $sName, string $sType) + public function __construct(string $sName, string $sType, AbstractFormBlock $oOwnerBlock) { $this->sName = $sName; $this->sType = $sType; + $this->oOwnerBlock = $oOwnerBlock; } /** @@ -54,19 +55,6 @@ class AbstractFormIO return $this->oOwnerBlock; } - /** - * Set the owner block. - * - * @param AbstractFormBlock $oOwnerBlock - * - * @return $this - */ - public function SetOwnerBlock(AbstractFormBlock $oOwnerBlock): self - { - $this->oOwnerBlock = $oOwnerBlock; - return $this; - } - /** * Get the IO name. * @@ -87,6 +75,7 @@ class AbstractFormIO public function SetName(string $sName): self { $this->sName = $sName; + return $this; } @@ -136,6 +125,7 @@ class AbstractFormIO { $PostSetDataExist = array_key_exists(FormEvents::POST_SET_DATA, $this->aValues) && $this->aValues[FormEvents::POST_SET_DATA] !== null; $PostSubmitExist = array_key_exists(FormEvents::POST_SUBMIT, $this->aValues) && $this->aValues[FormEvents::POST_SUBMIT] !== null; + return $PostSetDataExist || $PostSubmitExist; } @@ -170,12 +160,13 @@ class AbstractFormIO */ public function Value(): mixed { - if(array_key_exists(FormEvents::POST_SUBMIT, $this->aValues) ){ + if (array_key_exists(FormEvents::POST_SUBMIT, $this->aValues)) { return $this->aValues[FormEvents::POST_SUBMIT]; } - if(array_key_exists(FormEvents::POST_SET_DATA, $this->aValues) ){ + if (array_key_exists(FormEvents::POST_SET_DATA, $this->aValues)) { return $this->aValues[FormEvents::POST_SET_DATA]; } + return null; } diff --git a/sources/Forms/Block/IO/Converter/OqlToClassConverter.php b/sources/Forms/Block/IO/Converter/OqlToClassConverter.php index 7e934c232..b9f77a43e 100644 --- a/sources/Forms/Block/IO/Converter/OqlToClassConverter.php +++ b/sources/Forms/Block/IO/Converter/OqlToClassConverter.php @@ -15,24 +15,25 @@ use Symfony\Component\Filesystem\Exception\IOException; */ class OqlToClassConverter extends AbstractConverter { - /** @inheritdoc */ + /** @inheritdoc */ public function Convert(mixed $oData): ?ClassIOFormat { - if($oData === null) + if ($oData === null) { return null; + } // Extract OQL information preg_match('/SELECT\s+(\w+)/', $oData, $aMatches); // Selected class - if(isset($aMatches[1])){ + if (isset($aMatches[1])) { $sSelectedClass = $aMatches[1]; - if(!MetaModel::IsValidClass($sSelectedClass)){ - throw new IOException('Incorrect OQL select class name ' . $sSelectedClass); + if (!MetaModel::IsValidClass($sSelectedClass)) { + throw new IOException('Incorrect OQL select class name '.$sSelectedClass); } + return new ClassIOFormat($aMatches[1]); - } - else{ + } else { throw new IOException('Incorrect OQL sentence'); } diff --git a/sources/Forms/Block/IO/Converter/StringToAttributeConverter.php b/sources/Forms/Block/IO/Converter/StringToAttributeConverter.php index bbc7bf2bb..6d8272418 100644 --- a/sources/Forms/Block/IO/Converter/StringToAttributeConverter.php +++ b/sources/Forms/Block/IO/Converter/StringToAttributeConverter.php @@ -13,11 +13,13 @@ use Combodo\iTop\Forms\Block\IO\Format\AttributeIOFormat; */ class StringToAttributeConverter extends AbstractConverter { - /** @inheritdoc */ + /** @inheritdoc */ public function Convert(mixed $oData): ?AttributeIOFormat { - if($oData === null) + if ($oData === null) { return null; + } + return new AttributeIOFormat($oData); } } \ No newline at end of file diff --git a/sources/Forms/Block/IO/FormBinding.php b/sources/Forms/Block/IO/FormBinding.php index 8664b8657..15e348881 100644 --- a/sources/Forms/Block/IO/FormBinding.php +++ b/sources/Forms/Block/IO/FormBinding.php @@ -11,12 +11,17 @@ namespace Combodo\iTop\Forms\Block\IO; */ class FormBinding { + public readonly AbstractFormIO $oSourceIO; + public readonly AbstractFormIO $oDestinationIO; + /** * @param AbstractFormIO $oSourceIO * @param AbstractFormIO $oDestinationIO */ - public function __construct(public readonly AbstractFormIO $oSourceIO, public readonly AbstractFormIO $oDestinationIO) + public function __construct(AbstractFormIO $oSourceIO, AbstractFormIO $oDestinationIO) { + $this->oDestinationIO = $oDestinationIO; + $this->oSourceIO = $oSourceIO; } diff --git a/sources/Forms/Block/IO/FormOutput.php b/sources/Forms/Block/IO/FormOutput.php index 4a5581fe8..55cd68508 100644 --- a/sources/Forms/Block/IO/FormOutput.php +++ b/sources/Forms/Block/IO/FormOutput.php @@ -6,6 +6,7 @@ namespace Combodo\iTop\Forms\Block\IO; +use Combodo\iTop\Forms\Block\AbstractFormBlock; use Combodo\iTop\Forms\Block\IO\Converter\AbstractConverter; /** @@ -13,11 +14,11 @@ use Combodo\iTop\Forms\Block\IO\Converter\AbstractConverter; */ class FormOutput extends AbstractFormIO { - /** @var AbstractConverter|null */ + /** @var AbstractConverter|null */ private null|AbstractConverter $oConverter; - /** @var array */ + /** @var array */ private array $aBindingsToOutputs = []; /** @@ -27,9 +28,9 @@ class FormOutput extends AbstractFormIO * @param string $sType * @param AbstractConverter|null $oConverter */ - public function __construct(string $sName, string $sType, AbstractConverter $oConverter = null) + public function __construct(string $sName, string $sType, AbstractFormBlock $oOwnerBlock, AbstractConverter $oConverter = null) { - parent::__construct($sName, $sType); + parent::__construct($sName, $sType, $oOwnerBlock); $this->oConverter = $oConverter; } @@ -45,6 +46,7 @@ class FormOutput extends AbstractFormIO if (is_null($this->oConverter)) { return $oData; } + return $this->oConverter->Convert($oData); } @@ -83,7 +85,6 @@ class FormOutput extends AbstractFormIO } - /** * Bind to output. * @@ -103,7 +104,6 @@ class FormOutput extends AbstractFormIO } - /** * Get the bindings. * diff --git a/sources/Forms/Block/IO/Format/AttributeIOFormat.php b/sources/Forms/Block/IO/Format/AttributeIOFormat.php index c62a342b5..2e16d42a8 100644 --- a/sources/Forms/Block/IO/Format/AttributeIOFormat.php +++ b/sources/Forms/Block/IO/Format/AttributeIOFormat.php @@ -6,8 +6,11 @@ use JsonSerializable; class AttributeIOFormat implements JsonSerializable { - public function __construct(public string $sAttributeName) + public string $sAttributeName; + + public function __construct(string $sAttributeName) { + $this->sAttributeName = $sAttributeName; // validation du format sinon exception } diff --git a/sources/Forms/Block/IO/Format/ClassIOFormat.php b/sources/Forms/Block/IO/Format/ClassIOFormat.php index f8d9b64bf..ceedbf5ea 100644 --- a/sources/Forms/Block/IO/Format/ClassIOFormat.php +++ b/sources/Forms/Block/IO/Format/ClassIOFormat.php @@ -6,8 +6,11 @@ use JsonSerializable; class ClassIOFormat implements JsonSerializable { - public function __construct(public string $sClassName) + public string $sClassName; + + public function __construct(string $sClassName) { + $this->sClassName = $sClassName; // validation du format sinon exception } diff --git a/sources/Forms/Block/IO/Format/RawFormat.php b/sources/Forms/Block/IO/Format/RawFormat.php index 04ab08f97..ec8e3895e 100644 --- a/sources/Forms/Block/IO/Format/RawFormat.php +++ b/sources/Forms/Block/IO/Format/RawFormat.php @@ -4,8 +4,11 @@ namespace Combodo\iTop\Forms\Block\IO\Format; class RawFormat { - public function __construct(public string $oValue) + public string $oValue; + + public function __construct(string $oValue) { + $this->oValue = $oValue; // validation du format sinon exception } diff --git a/sources/Forms/FormBuilder/DependencyHandler.php b/sources/Forms/FormBuilder/DependencyHandler.php index 3bc70b537..d16960753 100644 --- a/sources/Forms/FormBuilder/DependencyHandler.php +++ b/sources/Forms/FormBuilder/DependencyHandler.php @@ -27,6 +27,11 @@ class DependencyHandler /** @var array Debug data */ private array $aDebugData = []; + private readonly string $sName; + private readonly AbstractFormBlock $oFormBlock; + private readonly FormBuilder $oFormBuilder; + private readonly array $aSubBlocks; + private readonly array $aDependentBlocks; /** * Constructor. @@ -37,8 +42,13 @@ class DependencyHandler * @param array $aSubBlocks Sub blocks * @param array $aDependentBlocks Dependants blocks */ - public function __construct(private readonly string $sName, private readonly AbstractFormBlock $oFormBlock, private readonly FormBuilder $oFormBuilder, private readonly array $aSubBlocks, private readonly array $aDependentBlocks) + public function __construct(string $sName, AbstractFormBlock $oFormBlock, FormBuilder $oFormBuilder, array $aSubBlocks, array $aDependentBlocks) { + $this->aDependentBlocks = $aDependentBlocks; + $this->aSubBlocks = $aSubBlocks; + $this->oFormBuilder = $oFormBuilder; + $this->oFormBlock = $oFormBlock; + $this->sName = $sName; // dependencies map $this->oDependenciesMap = new DependencyMap($aDependentBlocks); @@ -79,15 +89,15 @@ class DependencyHandler foreach ($this->oDependenciesMap->GetListenedOutputBlockNames() as $sOutputBlockName) { // inner binding - if($sOutputBlockName === $this->oFormBlock->getName()) { + if ($sOutputBlockName === $this->oFormBlock->getName()) { continue; } $this->aDebugData[] = [ 'builder' => $this->oFormBuilder->getName(), - 'event' => 'form.listen', - 'form' => $sOutputBlockName, - 'value' => 'NA' + 'event' => 'form.listen', + 'form' => $sOutputBlockName, + 'value' => 'NA', ]; // Listen the output block POST_SET_DATA & POST_SUBMIT @@ -111,9 +121,9 @@ class DependencyHandler $this->aDebugData[] = [ 'builder' => $this->oFormBuilder->getName(), - 'event' => $sEventType, - 'form' => $oEvent->getForm()->getName(), - 'value' => $oEvent->getData() + 'event' => $sEventType, + 'form' => $oEvent->getForm()->getName(), + 'value' => $oEvent->getData(), ]; // Get the form @@ -123,7 +133,7 @@ class DependencyHandler $oFormBlock = $this->aSubBlocks[$oForm->getName()]; // Compute the block outputs with the data - if(!$oFormBlock instanceof FormBlock) { + if (!$oFormBlock instanceof FormBlock) { $oFormBlock->ComputeOutputs($sEventType, $oEvent->getData()); } @@ -142,16 +152,15 @@ class DependencyHandler private function CheckDependencies(FormInterface|FormBuilderInterface $oForm): void { /** Iterate throw dependencies... @var AbstractFormBlock $oDependentBlock */ - foreach ($this->aDependentBlocks as $qBlockName => $oDependentBlock) - { + foreach ($this->aDependentBlocks as $qBlockName => $oDependentBlock) { // When dependencies met, add the dependent field if not already done - if(!$oDependentBlock->IsAdded() && $oDependentBlock->IsInputsDataReady()) { + if (!$oDependentBlock->IsAdded() && $oDependentBlock->IsInputsDataReady()) { // Get the dependent field options $aOptions = $oDependentBlock->UpdateOptions(); // Add the listener callback to the dependent field if it is also a dependency for another field - if($this->oDependenciesMap->IsTheBlockInDependencies($oDependentBlock->getName())) { + if ($this->oDependenciesMap->IsTheBlockInDependencies($oDependentBlock->getName())) { // Pass the listener call back to be registered by the dependency form builder $aOptions = array_merge($aOptions, [ @@ -159,22 +168,23 @@ class DependencyHandler ]); } - if($oDependentBlock->AllowAdd()) { + if ($oDependentBlock->AllowAdd()) { $this->aDebugData[] = [ 'builder' => $this->oFormBuilder->getName(), - 'event' => 'form.add', - 'form' => $oDependentBlock->getName(), - 'value' => 'NA' + 'event' => 'form.add', + 'form' => $oDependentBlock->getName(), + 'value' => 'NA', ]; - if(array_key_exists('builder_listener', $aOptions)) - $this->aDebugData[] = [ - 'builder' => $this->oFormBuilder->getName(), - 'event' => 'form.listen', - 'form' => $oDependentBlock->getName(), - 'value' => 'NA' - ]; + if (array_key_exists('builder_listener', $aOptions)) { + $this->aDebugData[] = [ + 'builder' => $this->oFormBuilder->getName(), + 'event' => 'form.listen', + 'form' => $oDependentBlock->getName(), + 'value' => 'NA', + ]; + } // Mark the dependency as added $oDependentBlock->SetAdded(true); @@ -186,11 +196,11 @@ class DependencyHandler } - if($oDependentBlock->IsAdded() && !$oDependentBlock->IsInputsDataReady()) { + if ($oDependentBlock->IsAdded() && !$oDependentBlock->IsInputsDataReady()) { $oForm->add($oDependentBlock->GetName(), HiddenType::class, [ - 'form_block' => $oDependentBlock, + 'form_block' => $oDependentBlock, 'prevent_form_build' => true, - ]); + ]); } } diff --git a/sources/Forms/FormBuilder/DependencyMap.php b/sources/Forms/FormBuilder/DependencyMap.php index a5b9e43d5..88c05deac 100644 --- a/sources/Forms/FormBuilder/DependencyMap.php +++ b/sources/Forms/FormBuilder/DependencyMap.php @@ -6,7 +6,6 @@ namespace Combodo\iTop\Forms\FormBuilder; -use Combodo\iTop\Forms\Block\FormBlock; use Combodo\iTop\Forms\Block\IO\FormBinding; use Combodo\iTop\Forms\Block\IO\FormInput; use Combodo\iTop\Forms\Block\IO\FormOutput; @@ -25,14 +24,16 @@ class DependencyMap /** @var array output to outputs */ private array $aOutputToOutputsMap = []; + private readonly array $aDependentBlocks; /** * Constructor. * * @param array $aDependentBlocks */ - public function __construct(private readonly array $aDependentBlocks) + public function __construct(array $aDependentBlocks) { + $this->aDependentBlocks = $aDependentBlocks; // Initialization $this->Init(); } @@ -44,31 +45,31 @@ class DependencyMap */ private function Init(): void { - /** Iterate throw blocks with dependencies... @var FormBlock $oDependentBlock */ + /** Iterate throw blocks with dependencies... @var \Combodo\iTop\Forms\Block\Base\FormBlock $oDependentBlock */ foreach ($this->aDependentBlocks as $sBlockName => $oDependentBlock) { - /** Iterate throw the block inputs bindings... @var FormBinding $oBinding**/ + /** Iterate throw the block inputs bindings... @var FormBinding $oBinding * */ foreach ($oDependentBlock->GetInputsBindings() as $oBinding) { // Output to inputs map - if($oBinding->oSourceIO instanceof FormOutput - && $oBinding->oDestinationIO instanceof FormInput){ + if ($oBinding->oSourceIO instanceof FormOutput + && $oBinding->oDestinationIO instanceof FormInput) { $this->AddBindingToMap($this->aOutputToInputsMap, $oBinding); } // Input to inputs map - if($oBinding->oSourceIO instanceof FormInput - && $oBinding->oDestinationIO instanceof FormInput){ + if ($oBinding->oSourceIO instanceof FormInput + && $oBinding->oDestinationIO instanceof FormInput) { $this->AddBindingToMap($this->aInputToInputsMap, $oBinding); } } - /** Iterate throw the block inputs connections... @var FormBinding $oBinding**/ + /** Iterate throw the block inputs connections... @var FormBinding $oBinding * */ foreach ($oDependentBlock->GetOutputBindings() as $oBinding) { // Output to outputs map - if($oBinding->oSourceIO instanceof FormOutput - && $oBinding->oDestinationIO instanceof FormOutput){ + if ($oBinding->oSourceIO instanceof FormOutput + && $oBinding->oDestinationIO instanceof FormOutput) { $this->AddBindingToMap($this->aOutputToOutputsMap, $oBinding); } @@ -111,8 +112,8 @@ class DependencyMap { $aResult = []; - foreach(array_keys($this->aOutputToInputsMap) as $sOutputBlockName) { - if(!array_key_exists($sOutputBlockName, $this->aDependentBlocks)){ + foreach (array_keys($this->aOutputToInputsMap) as $sOutputBlockName) { + if (!array_key_exists($sOutputBlockName, $this->aDependentBlocks)) { $aResult[] = $sOutputBlockName; } } @@ -120,6 +121,19 @@ class DependencyMap return $aResult; } + public function GetImpacted(string $sBlockName): array + { + $aImpacted = []; + foreach ($this->aOutputToInputsMap[$sBlockName] as $aBindings) { + foreach ($aBindings as $oBinding) { + $oDestBlock = $oBinding->oDestinationIO->GetOwnerBlock(); + $aImpacted[] = $oDestBlock; + } + } + + return $aImpacted; + } + /** * @param string $sBlockName * @@ -147,9 +161,8 @@ class DependencyMap public function IsTheBlockInDependencies(string $sBlockName): bool { - foreach ($this->aDependentBlocks as $oDependentBlock) - { - if($oDependentBlock->getName() === $sBlockName) { + foreach ($this->aDependentBlocks as $oDependentBlock) { + if ($oDependentBlock->getName() === $sBlockName) { return true; } } diff --git a/sources/Forms/FormBuilder/FormBuilder.php b/sources/Forms/FormBuilder/FormBuilder.php index 148009e3d..4b2c4d7b8 100644 --- a/sources/Forms/FormBuilder/FormBuilder.php +++ b/sources/Forms/FormBuilder/FormBuilder.php @@ -8,6 +8,7 @@ namespace Combodo\iTop\Forms\FormBuilder; use Combodo\iTop\Forms\Block\AbstractFormBlock; use Combodo\iTop\Forms\Block\Base\FormBlock; +use IteratorAggregate; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\DataMapperInterface; @@ -22,9 +23,9 @@ use Symfony\Component\Form\ResolvedFormTypeInterface; use Symfony\Component\PropertyAccess\PropertyPathInterface; use Traversable; -class FormBuilder implements FormBuilderInterface, \IteratorAggregate +class FormBuilder implements FormBuilderInterface, IteratorAggregate { - /** @var DependencyHandler|null */ + /** @var DependencyHandler|null */ private ?DependencyHandler $oDependencyHandler = null; /** @var AbstractFormBlock */ @@ -32,6 +33,7 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate /** @var array sub blocks */ private array $aSubFormBlocks = []; + private readonly FormBuilderInterface $builder; /** * Constructor. @@ -39,13 +41,14 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate * @param FormBuilderInterface $builder * */ - public function __construct(private readonly FormBuilderInterface $builder) + public function __construct(FormBuilderInterface $builder) { + $this->builder = $builder; /** Get the corresponding form block @var AbstractFormBlock $oFormBlock */ $oFormBlock = $this->builder->getOption('form_block'); // Build the form - if($oFormBlock instanceof FormBlock) { + if ($oFormBlock instanceof FormBlock) { $this->BuildForm($oFormBlock); } } @@ -61,7 +64,7 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate { // Hidden (ignore) $aOptions = $this->builder->getOptions(); - if(array_key_exists('prevent_form_build', $aOptions) && $aOptions['prevent_form_build']) { + if (array_key_exists('prevent_form_build', $aOptions) && $aOptions['prevent_form_build']) { return; } @@ -76,7 +79,7 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate $bHasDependency = $this->HandleSubBlock($oSubFormBlock); // Add to the dependencies array - if($bHasDependency){ + if ($bHasDependency) { $aDependentBlocks[$oSubFormBlock->GetName()] = $oSubFormBlock; } @@ -103,17 +106,15 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate // Insert a hidden type to save the place $this->builder->add($oSubFormBlock->GetName(), HiddenType::class, [ - 'form_block' => $oSubFormBlock, + 'form_block' => $oSubFormBlock, 'prevent_form_build' => true, -// 'mapped' => false, -// 'disabled' => true, + // 'mapped' => false, + // 'disabled' => true, ]); return true; - } - else { - + } else { // Directly insert the block corresponding form type $this->add($oSubFormBlock->GetName(), $oSubFormBlock->GetFormType(), $oSubFormBlock->UpdateOptions()); $oSubFormBlock->SetAdded(true); @@ -145,11 +146,17 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate return $this->oDependencyHandler; } + public function GetDependencyMap(): ?DependencyMap + { + return $this->oDependencyHandler?->GetMap(); + } + // pure decoration of FormBuilderInterface public function add(string|FormBuilderInterface $child, ?string $type = null, array $options = []): static { $this->builder->add($child, $type, $options); + return $this; } @@ -162,6 +169,7 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate { return $this->builder->count(); } + public function create(string $name, ?string $type = null, array $options = []): FormBuilderInterface { return $this->builder->create($name, $type, $options); @@ -175,6 +183,7 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate public function remove(string $name): static { $this->builder->remove($name); + return $this; } @@ -196,126 +205,147 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate public function addEventListener(string $eventName, callable $listener, int $priority = 0): static { $this->builder->addEventListener($eventName, $listener, $priority); + return $this; } public function addEventSubscriber(EventSubscriberInterface $subscriber): static { $this->builder->addEventSubscriber($subscriber); + return $this; } public function addViewTransformer(DataTransformerInterface $viewTransformer, bool $forcePrepend = false): static { $this->builder->addViewTransformer($viewTransformer, $forcePrepend); + return $this; } public function resetViewTransformers(): static { $this->builder->resetViewTransformers(); + return $this; } public function addModelTransformer(DataTransformerInterface $modelTransformer, bool $forceAppend = false): static { $this->builder->addModelTransformer($modelTransformer, $forceAppend); + return $this; } public function resetModelTransformers(): static { $this->builder->resetModelTransformers(); + return $this; } public function setAttribute(string $name, mixed $value): static { $this->builder->setAttribute($name, $value); + return $this; } public function setAttributes(array $attributes): static { $this->builder->setAttributes($attributes); + return $this; } public function setDataMapper(?DataMapperInterface $dataMapper): static { $this->builder->setDataMapper($dataMapper); + return $this; } public function setDisabled(bool $disabled): static { $this->builder->setDisabled($disabled); + return $this; } public function setEmptyData(mixed $emptyData): static { $this->builder->setEmptyData($emptyData); + return $this; } public function setErrorBubbling(bool $errorBubbling): static { $this->builder->setErrorBubbling($errorBubbling); + return $this; } public function setRequired(bool $required): static { $this->builder->setRequired($required); + return $this; } public function setPropertyPath(PropertyPathInterface|string|null $propertyPath): static { $this->builder->setPropertyPath($propertyPath); + return $this; } public function setMapped(bool $mapped): static { $this->builder->setMapped($mapped); + return $this; } public function setByReference(bool $byReference): static { $this->builder->setByReference($byReference); + return $this; } public function setInheritData(bool $inheritData): static { $this->builder->setInheritData($inheritData); + return $this; } public function setCompound(bool $compound): static { $this->builder->setCompound($compound); + return $this; } public function setType(ResolvedFormTypeInterface $type): static { $this->builder->setType($type); + return $this; } public function setData(mixed $data): static { $this->builder->setData($data); + return $this; } public function setDataLocked(bool $locked): static { $this->builder->setDataLocked($locked); + return $this; } @@ -327,24 +357,28 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate public function setAction(string $action): static { $this->builder->setAction($action); + return $this; } public function setMethod(string $method): static { $this->builder->setMethod($method); + return $this; } public function setRequestHandler(RequestHandlerInterface $requestHandler): static { $this->builder->setRequestHandler($requestHandler); + return $this; } public function setAutoInitialize(bool $initialize): static { $this->builder->setAutoInitialize($initialize); + return $this; } @@ -356,6 +390,7 @@ class FormBuilder implements FormBuilderInterface, \IteratorAggregate public function setIsEmptyCallback(?callable $isEmptyCallback): static { $this->builder->setIsEmptyCallback($isEmptyCallback); + return $this; } diff --git a/sources/Forms/FormBuilder/FormHelper.php b/sources/Forms/FormBuilder/FormHelper.php index bc6dd45c9..276da8e10 100644 --- a/sources/Forms/FormBuilder/FormHelper.php +++ b/sources/Forms/FormBuilder/FormHelper.php @@ -24,8 +24,7 @@ class FormHelper return FormEvents::POST_SET_DATA; } else if ($event instanceof PostSubmitEvent) { return FormEvents::POST_SUBMIT; - } - else if ($event instanceof PreSubmitEvent) { + } else if ($event instanceof PreSubmitEvent) { return FormEvents::PRE_SUBMIT; } diff --git a/sources/Forms/FormBuilder/FormTypeExtension.php b/sources/Forms/FormBuilder/FormTypeExtension.php index 67dfec8fa..51d9a9dd2 100644 --- a/sources/Forms/FormBuilder/FormTypeExtension.php +++ b/sources/Forms/FormBuilder/FormTypeExtension.php @@ -6,7 +6,6 @@ namespace Combodo\iTop\Forms\FormBuilder; -use Combodo\iTop\Forms\Block\FormBlock; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormBuilderInterface; @@ -22,38 +21,38 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class FormTypeExtension extends AbstractTypeExtension { - /** @inheritdoc */ + /** @inheritdoc */ public static function getExtendedTypes(): iterable { return [ - FormType::class + FormType::class, ]; } - /** @inheritdoc */ + /** @inheritdoc */ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefined([ 'form_block', 'builder_listener', - 'prevent_form_build' + 'prevent_form_build', ]); } - /** @inheritdoc */ + /** @inheritdoc */ public function buildForm(FormBuilderInterface $builder, array $options): void { - if(array_key_exists('builder_listener', $options)) { + if (array_key_exists('builder_listener', $options)) { $builder->addEventListener(FormEvents::POST_SET_DATA, $options['builder_listener']); $builder->addEventListener(FormEvents::POST_SUBMIT, $options['builder_listener']); } } - /** @inheritdoc */ + /** @inheritdoc */ public function buildView(FormView $view, FormInterface $form, array $options): void { - if(array_key_exists('form_block', $options)) { + if (array_key_exists('form_block', $options)) { $view->vars['form_block'] = $options['form_block']; $oFormBlock = $options['form_block']; diff --git a/sources/Forms/FormBuilder/ResolvedFormType.php b/sources/Forms/FormBuilder/ResolvedFormType.php index 65503730c..64807d491 100644 --- a/sources/Forms/FormBuilder/ResolvedFormType.php +++ b/sources/Forms/FormBuilder/ResolvedFormType.php @@ -16,6 +16,7 @@ class ResolvedFormType extends SymfonyResolvedFormType implements ResolvedFormTy protected function newBuilder(string $name, ?string $dataClass, FormFactoryInterface $factory, array $options): FormBuilderInterface { $builder = parent::newBuilder($name, $dataClass, $factory, $options); + return new FormBuilder($builder); } } \ No newline at end of file diff --git a/sources/Forms/Twig/Extension/FormCompatibilityExtension.php b/sources/Forms/Twig/Extension/FormCompatibilityExtension.php index ad7ca0eb2..7edc45a47 100644 --- a/sources/Forms/Twig/Extension/FormCompatibilityExtension.php +++ b/sources/Forms/Twig/Extension/FormCompatibilityExtension.php @@ -25,7 +25,7 @@ class FormCompatibilityExtension extends AbstractExtension // Alias of dict_s, to be compatible with Symfony/Twig standard new TwigFilter('trans', function ($sStringCode, $aData = null, $sTransDomain = false) { return Dict::S($sStringCode); - }) + }), ]; } diff --git a/templates/application/forms/itop_debug.html.twig b/templates/application/forms/itop_debug.html.twig index df7755e7a..a7e449cd9 100644 --- a/templates/application/forms/itop_debug.html.twig +++ b/templates/application/forms/itop_debug.html.twig @@ -1,6 +1,9 @@ {# @copyright Copyright (C) 2010-2025 Combodo SARL #} {# @license http://opensource.org/licenses/AGPL-3.0 #} -
- {{ sProfilerContent|raw }} +
+ {% for aProfileInfo in aProfilesInfo %} + {% set aProfileData = aProfileInfo.aProfileData %} + {{ include(aProfileInfo.sTemplate) }} + {% endfor %}
\ No newline at end of file diff --git a/templates/application/forms/itop_debug_update.html.twig b/templates/application/forms/itop_debug_update.html.twig index 3c3a928fe..f1f099c87 100644 --- a/templates/application/forms/itop_debug_update.html.twig +++ b/templates/application/forms/itop_debug_update.html.twig @@ -1,8 +1,11 @@ {# @copyright Copyright (C) 2010-2025 Combodo SARL #} {# @license http://opensource.org/licenses/AGPL-3.0 #} - + \ No newline at end of file diff --git a/templates/application/forms/itop_error.html.twig b/templates/application/forms/itop_error.html.twig index 9d6e832c5..df1cbe936 100644 --- a/templates/application/forms/itop_error.html.twig +++ b/templates/application/forms/itop_error.html.twig @@ -1,6 +1,14 @@ {# @copyright Copyright (C) 2010-2025 Combodo SARL #} {# @license http://opensource.org/licenses/AGPL-3.0 #} -{% if sControllerError %} - {% UIAlert ForDanger { sTitle:'UI:Error:TwigController'|dict_s, sContent:sControllerError } %}{% EndUIAlert %} -{% endif %} +
+ {% if sControllerError %} +
+
+
+
+
{{ 'UI:Error:TwigController'|dict_s }}
+
{{ sControllerError }}
+
+ {% endif %} +
\ No newline at end of file diff --git a/templates/application/forms/itop_error_update.html.twig b/templates/application/forms/itop_error_update.html.twig new file mode 100644 index 000000000..aa6a339bb --- /dev/null +++ b/templates/application/forms/itop_error_update.html.twig @@ -0,0 +1,16 @@ +{# @copyright Copyright (C) 2010-2025 Combodo SARL #} +{# @license http://opensource.org/licenses/AGPL-3.0 #} + + + + \ No newline at end of file diff --git a/tests/php-unit-tests/unitary-tests/sources/Forms/Block/IO/AbstractFormIOTest.php b/tests/php-unit-tests/unitary-tests/sources/Forms/Block/IO/AbstractFormIOTest.php new file mode 100644 index 000000000..1ef1fc479 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/sources/Forms/Block/IO/AbstractFormIOTest.php @@ -0,0 +1,87 @@ +GivenFormBlock('test'); + + self::assertEquals([], $oBlock->GetInputs(), 'Row form block must not have input by default'); + self::assertEquals([], $oBlock->GetOutputs(), 'Row form block must not have output by default'); + } + + public function testAddingOneInputToABlock_StoresIt(): void + { + $oBlock = $this->GivenFormBlock('test', [], [ + ['io_type' => FormInput::class, 'name' => 'input', 'data_type' => RawFormat::class,], + ]); + + self::assertCount(1, $oBlock->GetInputs(), 'Inputs must be saved in block forms'); + $aInputs = $oBlock->GetInputs(); + self::assertEquals('input', array_shift($aInputs)->getName(), 'Inputs must be saved in block forms'); + self::assertEquals(RawFormat::class, $oBlock->GetInput('input')->GetDataType(), 'Format must be kept in inputs saved in block forms'); + } + + public function testAddingOneOutputToABlock_StoresIt(): void + { + $oBlock = $this->GivenFormBlock('test', [], [ + ['io_type' => FormOutput::class, 'name' => 'output', 'data_type' => RawFormat::class], + ]); + + self::assertCount(1, $oBlock->GetOutputs(), 'Outputs must be saved in block forms'); + $aInputs = $oBlock->GetOutputs(); + self::assertEquals('output', array_shift($aInputs)->getName(), 'Outputs must be saved in block forms'); + self::assertEquals(RawFormat::class, $oBlock->GetOutput('output')->GetDataType(), 'Format must be kept in outputs saved in block forms'); + } + + public function testAddingMultipleInputsAndOutputsToABlock_StoresThem(): void + { + $oBlock = $this->GivenFormBlock('test', [], [ + ['io_type' => FormInput::class, 'name' => 'input1', 'data_type' => RawFormat::class,], + ['io_type' => FormInput::class, 'name' => 'input2', 'data_type' => RawFormat::class,], + ['io_type' => FormInput::class, 'name' => 'input3', 'data_type' => RawFormat::class,], + ['io_type' => FormOutput::class, 'name' => 'output1', 'data_type' => RawFormat::class], + ['io_type' => FormOutput::class, 'name' => 'output2', 'data_type' => RawFormat::class], + ]); + + self::assertCount(3, $oBlock->GetInputs(), 'Inputs must be saved in block forms'); + self::assertCount(2, $oBlock->GetOutputs(), 'Outputs must be saved in block forms'); + } + + + /////////////////////// + /// GIVEN methods + /// + private function GivenFormBlock(string $sName, array $aOptions = [], array $aIOs = []): AbstractFormBlock + { + $oBlock = new FormBlock($sName, $aOptions); + + foreach ($aIOs as $aIO) { + if ($aIO['io_type'] === FormInput::class) { + $oBlock->AddInput($aIO['name'], $aIO['data_type']); + } else { + if (isset($aIO['converter_class'])) { + $oBlock->AddOutput($aIO['name'], $aIO['data_type'], new $aIO['converter_class']); + } else { + $oBlock->AddOutput($aIO['name'], $aIO['data_type']); + } + } + } + + return $oBlock; + } +}