mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
N°8772 - Form dependencies manager implementation
- Form SDK implementation - Basic Forms - Dynamics Forms - Basic Blocks + Data Model Block - Form Compilation - Turbo integration
This commit is contained in:
@@ -9,6 +9,8 @@ namespace Combodo\iTop\Application\TwigBase\Controller;
|
||||
|
||||
abstract class AbstractProfilerExtension implements iProfilerExtension
|
||||
{
|
||||
protected bool $bDebugForced;
|
||||
|
||||
public function Init()
|
||||
{
|
||||
}
|
||||
@@ -35,4 +37,9 @@ abstract class AbstractProfilerExtension implements iProfilerExtension
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function SetDebugForced(bool $bDebugForced): void
|
||||
{
|
||||
$this->bDebugForced = $bDebugForced;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ use Combodo\iTop\Application\WebPage\ErrorPage;
|
||||
use Combodo\iTop\Application\WebPage\iTopWebPage;
|
||||
use Combodo\iTop\Application\WebPage\WebPage;
|
||||
use Combodo\iTop\Controller\AbstractController;
|
||||
use Combodo\iTop\Forms\Block\AbstractFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use Combodo\iTop\Forms\Forms;
|
||||
use Combodo\iTop\Forms\FormType\FormTypeHelper;
|
||||
use Combodo\iTop\Service\InterfaceDiscovery\InterfaceDiscovery;
|
||||
use Dict;
|
||||
use Exception;
|
||||
@@ -39,14 +43,12 @@ use SetupPage;
|
||||
use SetupUtils;
|
||||
use Symfony\Bridge\Twig\Extension\FormExtension;
|
||||
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
|
||||
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormFactoryBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormRenderer;
|
||||
use Symfony\Component\Form\Forms;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManager;
|
||||
use Twig\Error\SyntaxError;
|
||||
@@ -56,10 +58,14 @@ use ZipArchive;
|
||||
|
||||
abstract class Controller extends AbstractController
|
||||
{
|
||||
public const ENUM_PAGE_TYPE_HTML = 'html';
|
||||
public const ENUM_PAGE_TYPE_BASIC_HTML = 'basic_html';
|
||||
public const ENUM_PAGE_TYPE_AJAX = 'ajax';
|
||||
public const ENUM_PAGE_TYPE_SETUP = 'setup';
|
||||
public const ENUM_PAGE_TYPE_HTML = 'html';
|
||||
public const ENUM_PAGE_TYPE_BASIC_HTML = 'basic_html';
|
||||
public const ENUM_PAGE_TYPE_AJAX = 'ajax';
|
||||
public const ENUM_PAGE_TYPE_TURBO_FORM_AJAX = 'turbo_ajax';
|
||||
public const ENUM_PAGE_TYPE_SETUP = 'setup';
|
||||
|
||||
public const TWIG_ERROR = 'error';
|
||||
public const TWIG_WARNING = 'warning';
|
||||
|
||||
/** @var \Twig\Environment */
|
||||
private $oTwig;
|
||||
@@ -103,14 +109,23 @@ 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;
|
||||
private bool $bDebugAllowed = true;
|
||||
protected bool $bDebugForced;
|
||||
|
||||
/**
|
||||
* Controller constructor.
|
||||
*
|
||||
* @param string $sViewPath Path of the twig files
|
||||
* @param string $sModuleName name of the module (or 'core' if not a module)
|
||||
* @param array $aAdditionalPaths for twig templates
|
||||
* @param array $aThemes for default form templates
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
* @throws \Twig\Error\LoaderError
|
||||
*/
|
||||
public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditionalPaths = [])
|
||||
public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditionalPaths = [], array $aThemes = ['application/forms/itop_console_layout.html.twig', 'application/forms/wip_form_demonstrator.html.twig'])
|
||||
{
|
||||
$this->aLinkedScripts = [];
|
||||
$this->aLinkedStylesheets = [];
|
||||
@@ -121,7 +136,7 @@ abstract class Controller extends AbstractController
|
||||
$this->SetModuleName($sModuleName);
|
||||
|
||||
// Initialize Symfony components
|
||||
$this->InitSymfonyComponents($sViewPath, $sModuleName, $aAdditionalPaths);
|
||||
$this->InitSymfonyComponents($sViewPath, $sModuleName, $aAdditionalPaths, $aThemes);
|
||||
$this->InitDebugExtensions();
|
||||
}
|
||||
|
||||
@@ -131,11 +146,13 @@ abstract class Controller extends AbstractController
|
||||
* @param string $sViewPath
|
||||
* @param string $sModuleName
|
||||
* @param array $aAdditionalPaths
|
||||
* @param array $aThemes
|
||||
*
|
||||
* @return void
|
||||
* @throws \ReflectionException
|
||||
* @throws \Twig\Error\LoaderError
|
||||
*/
|
||||
private function InitSymfonyComponents(string $sViewPath, string $sModuleName, array $aAdditionalPaths): void
|
||||
private function InitSymfonyComponents(string $sViewPath, string $sModuleName, array $aAdditionalPaths, array $aThemes): void
|
||||
{
|
||||
// Twig environment
|
||||
$aAdditionalPaths[] = APPROOT.'lib/symfony/twig-bridge/Resources/views/Form';
|
||||
@@ -157,10 +174,10 @@ abstract class Controller extends AbstractController
|
||||
}
|
||||
}
|
||||
if (strlen($sViewPath) > 0) {
|
||||
$this->SetViewPath($sViewPath, $aAdditionalPaths);
|
||||
$this->SetViewPath($sViewPath, $aAdditionalPaths, $aThemes);
|
||||
if ($sModuleName != 'core') {
|
||||
try {
|
||||
$this->aDefaultParams = ['sIndexURL' => utils::GetAbsoluteUrlModulePage($this->m_sModule, 'index.php')];
|
||||
$this->aDefaultParams = ['sIndexURL' => utils::GetAbsoluteUrlModulePage($this->m_sModule, 'index.php')];
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Error($e->getMessage());
|
||||
}
|
||||
@@ -169,6 +186,7 @@ abstract class Controller extends AbstractController
|
||||
|
||||
// PHP Request object representation from PHP request globals
|
||||
$this->oRequest = Request::createFromGlobals();
|
||||
$this->bDebugForced = $this->oRequest->query->has('debug');
|
||||
|
||||
// Initialize the CSRF token manager
|
||||
$this->oCsrfTokenManager = new CsrfTokenManager();
|
||||
@@ -188,7 +206,7 @@ abstract class Controller extends AbstractController
|
||||
$this->SetModuleName(basename($sModulePath));
|
||||
$this->SetViewPath($sModulePath.'/view');
|
||||
try {
|
||||
$this->aDefaultParams = ['sIndexURL' => utils::GetAbsoluteUrlModulePage($this->m_sModule, 'index.php')];
|
||||
$this->aDefaultParams = ['sIndexURL' => utils::GetAbsoluteUrlModulePage($this->m_sModule, 'index.php')];
|
||||
} catch (Exception $e) {
|
||||
IssueLog::Error($e->getMessage());
|
||||
}
|
||||
@@ -198,12 +216,15 @@ abstract class Controller extends AbstractController
|
||||
* Indicates the path of the view directory (containing the twig templates)
|
||||
*
|
||||
* @param string $sViewPath
|
||||
* @param array $aAdditionalPaths
|
||||
*
|
||||
* @throws \Twig\Error\LoaderError
|
||||
*/
|
||||
public function SetViewPath($sViewPath, $aAdditionalPaths = [])
|
||||
public function SetViewPath($sViewPath, $aAdditionalPaths = [], array $aThemes = ['application/forms/itop_console_layout.html.twig', 'application/forms/wip_form_demonstrator.html.twig']): void
|
||||
{
|
||||
$oTwig = TwigHelper::GetTwigEnvironment($sViewPath, $aAdditionalPaths);
|
||||
/** @link https://github.com/symfony/twig-bridge/blob/6.4/CHANGELOG.md#320 */
|
||||
$formEngine = new TwigRendererEngine(['application/forms/itop_console_layout.html.twig'], $oTwig);
|
||||
$formEngine = new TwigRendererEngine($aThemes, $oTwig);
|
||||
$oTwig->addRuntimeLoader(new FactoryRuntimeLoader([
|
||||
FormRenderer::class => function () use ($formEngine): FormRenderer {
|
||||
return new FormRenderer($formEngine, $this->oCsrfTokenManager);
|
||||
@@ -237,7 +258,7 @@ abstract class Controller extends AbstractController
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function HandleOperation()
|
||||
public function HandleOperation(): void
|
||||
{
|
||||
try {
|
||||
$this->CheckAccess();
|
||||
@@ -260,16 +281,16 @@ abstract class Controller extends AbstractController
|
||||
$oP->add(get_class($e).' : '.utils::EscapeHtml($e->GetMessage()));
|
||||
$oP->output();
|
||||
|
||||
IssueLog::Error($e->getMessage());
|
||||
IssueLog::Exception('HandleOperation failed for '.json_encode($this->m_sOperation), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point to handle requests
|
||||
* Entry point to handle requests for ajax pages
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function HandleAjaxOperation()
|
||||
public function HandleAjaxOperation(): void
|
||||
{
|
||||
try {
|
||||
$this->CheckAccess();
|
||||
@@ -300,13 +321,14 @@ abstract class Controller extends AbstractController
|
||||
}
|
||||
|
||||
$this->$sMethodName();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridable "page not found" which is more an "operation not found"
|
||||
*/
|
||||
public function DisplayBadRequest()
|
||||
public function DisplayBadRequest(): void
|
||||
{
|
||||
http_response_code(400);
|
||||
die('Operation not found');
|
||||
@@ -315,17 +337,17 @@ abstract class Controller extends AbstractController
|
||||
/**
|
||||
* Overridable "page not found" which is more an "operation not found"
|
||||
*/
|
||||
public function DisplayPageNotFound()
|
||||
public function DisplayPageNotFound(): void
|
||||
{
|
||||
http_response_code(404);
|
||||
die("Page not found");
|
||||
}
|
||||
|
||||
/**
|
||||
* @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()
|
||||
protected function CheckAccess(): void
|
||||
{
|
||||
if ($this->bCheckDemoMode && MetaModel::GetConfig()->Get('demo_mode')) {
|
||||
throw new Exception("Sorry, iTop is in <b>demonstration mode</b>: this feature is disabled.");
|
||||
@@ -353,7 +375,7 @@ abstract class Controller extends AbstractController
|
||||
null,
|
||||
[
|
||||
'sHtmlDecodedToken' => $sDecodedPassedToken,
|
||||
'conf param ID' => $this->sAccessTokenConfigParamId,
|
||||
'conf param ID' => $this->sAccessTokenConfigParamId,
|
||||
]
|
||||
);
|
||||
throw new Exception("Invalid token");
|
||||
@@ -369,7 +391,7 @@ abstract class Controller extends AbstractController
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function GetDefaultParameters()
|
||||
private function GetDefaultParameters(): array
|
||||
{
|
||||
return $this->aDefaultParams;
|
||||
}
|
||||
@@ -379,7 +401,7 @@ abstract class Controller extends AbstractController
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function DisableInDemoMode()
|
||||
public function DisableInDemoMode(): void
|
||||
{
|
||||
$this->bCheckDemoMode = true;
|
||||
}
|
||||
@@ -389,7 +411,7 @@ abstract class Controller extends AbstractController
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function AllowOnlyAdmin()
|
||||
public function AllowOnlyAdmin(): void
|
||||
{
|
||||
$this->bMustBeAdmin = true;
|
||||
}
|
||||
@@ -423,7 +445,7 @@ abstract class Controller extends AbstractController
|
||||
*
|
||||
* @param string $sMenuId
|
||||
*/
|
||||
public function SetMenuId($sMenuId)
|
||||
public function SetMenuId($sMenuId): void
|
||||
{
|
||||
$this->sMenuId = $sMenuId;
|
||||
}
|
||||
@@ -435,13 +457,13 @@ abstract class Controller extends AbstractController
|
||||
*
|
||||
* @param string $sDefaultOperation
|
||||
*/
|
||||
public function SetDefaultOperation($sDefaultOperation)
|
||||
public function SetDefaultOperation($sDefaultOperation): void
|
||||
{
|
||||
$this->sDefaultOperation = $sDefaultOperation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an AJAX page (AjaxPage)
|
||||
* Display an AJAX (html) page (AjaxPage)
|
||||
*
|
||||
* @api
|
||||
*
|
||||
@@ -450,7 +472,7 @@ abstract class Controller extends AbstractController
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function DisplayAjaxPage($aParams = [], $sTemplateName = null)
|
||||
public function DisplayAjaxPage($aParams = [], $sTemplateName = null): void
|
||||
{
|
||||
$this->DisplayPage($aParams, $sTemplateName, 'ajax');
|
||||
}
|
||||
@@ -461,58 +483,77 @@ abstract class Controller extends AbstractController
|
||||
* @api
|
||||
*
|
||||
* @param array $aParams Params used by the twig template
|
||||
* @param null $sTemplateName Name of the twig template, ie MyTemplate for MyTemplate.html.twig
|
||||
* @param string|null $sTemplateName Name of the twig template, ie MyTemplate for MyTemplate.html.twig
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function DisplaySetupPage($aParams = [], $sTemplateName = null)
|
||||
public function DisplaySetupPage(array $aParams = [], ?string $sTemplateName = null): void
|
||||
{
|
||||
$this->DisplayPage($aParams, $sTemplateName, 'setup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a page to update only the impacted fields of a form
|
||||
*
|
||||
* @param array $aParams
|
||||
*
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function DisplayTurboAjaxPage(array $aParams = []): void
|
||||
{
|
||||
$this->DisplayPage($aParams, 'application/forms/turbo-ajax-update', self::ENUM_PAGE_TYPE_TURBO_FORM_AJAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the twig page based on the name or the operation
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param array $aParams Params used by the twig template
|
||||
* @param string $sTemplateName Name of the twig template, ie MyTemplate for MyTemplate.html.twig
|
||||
* @param string $sPageType ('html' or 'ajax')
|
||||
* @param string|null $sTemplateName Name of the twig template, ie MyTemplate for MyTemplate.html.twig
|
||||
* @param string $sPageType ('html', 'basic_html', 'ajax', 'turbo_ajax', 'setup')
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function DisplayPage($aParams = [], $sTemplateName = null, $sPageType = 'html')
|
||||
public function DisplayPage(array $aParams = [], ?string $sTemplateName = null, string $sPageType = self::ENUM_PAGE_TYPE_HTML): void
|
||||
{
|
||||
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);
|
||||
$this->ManageDebugExtensions($aParams, $sPageType);
|
||||
|
||||
if (!empty($this->aAjaxTabs)) {
|
||||
$this->oPage->AddTabContainer('TwigBaseTabContainer');
|
||||
@@ -533,7 +574,9 @@ abstract class Controller extends AbstractController
|
||||
foreach ($this->aBlockParams as $sKey => $value) {
|
||||
$this->SetBlockParamToPage($sKey, $value);
|
||||
}
|
||||
$this->SetContentTypeToPage();
|
||||
$this->OutputPage();
|
||||
$this->sPageType = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -545,7 +588,7 @@ abstract class Controller extends AbstractController
|
||||
* @param int $iResponseCode HTTP response code
|
||||
* @param array $aHeaders additional HTTP headers
|
||||
*/
|
||||
public function DisplayJSONPage($aParams = [], $iResponseCode = 200, $aHeaders = [])
|
||||
public function DisplayJSONPage(array $aParams = [], int $iResponseCode = 200, array $aHeaders = []): void
|
||||
{
|
||||
$oKpi = new ExecutionKPI();
|
||||
http_response_code($iResponseCode);
|
||||
@@ -573,7 +616,7 @@ abstract class Controller extends AbstractController
|
||||
*
|
||||
* @since 3.0.1 3.1.0 Add $sReportFileName parameter
|
||||
*/
|
||||
public function DownloadZippedPage($aParams = [], $sTemplateName = null, $sReportFileName = 'itop-system-information-report')
|
||||
public function DownloadZippedPage(array $aParams = [], ?string $sTemplateName = null, string $sReportFileName = 'itop-system-information-report'): void
|
||||
{
|
||||
if (empty($sTemplateName)) {
|
||||
$sTemplateName = $this->m_sOperation;
|
||||
@@ -598,7 +641,7 @@ abstract class Controller extends AbstractController
|
||||
* @param string $sDownloadArchiveName file name to download, without the extension (.zip is automatically added)
|
||||
* @param bool $bUnlinkFiles if true then will unlink each source file
|
||||
*/
|
||||
final protected function ZipDownloadRemoveFile($aFiles, $sDownloadArchiveName, $bUnlinkFiles = false)
|
||||
final protected function ZipDownloadRemoveFile(array $aFiles, string $sDownloadArchiveName, bool $bUnlinkFiles = false): void
|
||||
{
|
||||
$sArchiveFileFullPath = tempnam(SetupUtils::GetTmpDir(), 'itop_download-').'.zip';
|
||||
$oArchive = new ZipArchive();
|
||||
@@ -617,7 +660,7 @@ abstract class Controller extends AbstractController
|
||||
$this->SendFileContent($sArchiveFileFullPath, $sDownloadArchiveName.'.zip', true, true);
|
||||
}
|
||||
|
||||
final protected function SendFileContent($sFilePath, $sDownloadArchiveName = null, $bFileTransfer = true, $bRemoveFile = false, $aHeaders = [])
|
||||
final protected function SendFileContent($sFilePath, $sDownloadArchiveName = null, $bFileTransfer = true, $bRemoveFile = false, $aHeaders = []): void
|
||||
{
|
||||
$sFileMimeType = utils::GetFileMimeType($sFilePath);
|
||||
header('Content-Type: '.$sFileMimeType);
|
||||
@@ -650,9 +693,10 @@ 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)
|
||||
public function AddLinkedScript($sScript): void
|
||||
{
|
||||
$this->aLinkedScripts[] = $sScript;
|
||||
}
|
||||
@@ -663,21 +707,22 @@ 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)
|
||||
public function AddLinkedStylesheet($sStylesheet): void
|
||||
{
|
||||
$this->aLinkedStylesheets[] = $sStylesheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an linked stylesheet to the current Page
|
||||
* Add a linked stylesheet to the current Page
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param string $sSaasRelPath SCSS Stylesheet relative path to link
|
||||
*/
|
||||
public function AddSaas($sSaasRelPath)
|
||||
public function AddSaas(string $sSaasRelPath): void
|
||||
{
|
||||
$this->aSaas[] = $sSaasRelPath;
|
||||
}
|
||||
@@ -685,15 +730,15 @@ abstract class Controller extends AbstractController
|
||||
/**
|
||||
* Add an AJAX tab to the current page
|
||||
*
|
||||
* @param string $sCode Code of the tab
|
||||
* @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 $sURL URL to call when the tab is activated
|
||||
* @param bool $bCache If true, cache the result for the current web page
|
||||
* @param string|null $sLabel Label of the tab (if null the code is translated)
|
||||
*
|
||||
* @param string $sCode Code of the tab
|
||||
*/
|
||||
public function AddAjaxTab($sCode, $sURL, $bCache = true, $sLabel = null)
|
||||
public function AddAjaxTab(string $sCode, string $sURL, bool $bCache = true, string $sLabel = null): void
|
||||
{
|
||||
if (is_null($sLabel)) {
|
||||
$sLabel = Dict::S($sCode);
|
||||
@@ -703,31 +748,62 @@ abstract class Controller extends AbstractController
|
||||
|
||||
/**
|
||||
* @param array $aBlockParams
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public function SetBlockParams(array $aBlockParams)
|
||||
public function SetBlockParams(array $aBlockParams): void
|
||||
{
|
||||
$this->aBlockParams = $aBlockParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)
|
||||
* Allow to set manually the content type of the page
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param string $sContentType
|
||||
*
|
||||
* @return void
|
||||
* @since 3.3.0
|
||||
*/
|
||||
public function DisableBreadCrumb()
|
||||
public function SetContentType(string $sContentType): void
|
||||
{
|
||||
$this->sContentType = $sContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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(): void
|
||||
{
|
||||
$this->bIsBreadCrumbEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.7.7 3.0.1 3.1.0 N°4760 method creation
|
||||
* @see iTopWebPage::SetBreadCrumbEntry()
|
||||
*
|
||||
* @param string $sId
|
||||
* @param string $sLabel
|
||||
* @param string $sDescription
|
||||
* @param string $sUrl
|
||||
* @param string $sIcon
|
||||
*
|
||||
* @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(string $sId, string $sLabel, string $sDescription, string $sUrl = '', string $sIcon = ''): void
|
||||
{
|
||||
$this->aBreadCrumbEntry = [$sId, $sLabel, $sDescription, $sUrl, $sIcon];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current incoming request
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Request
|
||||
* @since 3.3.0
|
||||
*/
|
||||
public function GetRequest(): Request
|
||||
{
|
||||
return $this->oRequest;
|
||||
@@ -737,45 +813,33 @@ abstract class Controller extends AbstractController
|
||||
* Get a form builder.
|
||||
* This form builder can be used to create a form or to add fields to an existing form.
|
||||
*
|
||||
* @param string $type
|
||||
* @param mixed|null $data
|
||||
* @param array $options
|
||||
* @api
|
||||
*
|
||||
* @return FormBuilderInterface
|
||||
* @param \Combodo\iTop\Forms\Block\AbstractFormBlock $oFormBlock
|
||||
* @param mixed|null $data
|
||||
*
|
||||
* @return \Symfony\Component\Form\FormBuilderInterface
|
||||
* @since 3.3.0
|
||||
*/
|
||||
public function GetFormBuilder(string $type = FormType::class, mixed $data = null, array $options = []): FormBuilderInterface
|
||||
public function GetFormBuilder(AbstractFormBlock $oFormBlock, mixed $data = null): FormBuilderInterface
|
||||
{
|
||||
return $this->oFormFactoryBuilder->getFormFactory()->createBuilder($type, $data, $options);
|
||||
return $this->oFormFactoryBuilder->getFormFactory()->createNamedBuilder($oFormBlock->GetName(), $oFormBlock->GetFormType(), $data, $oFormBlock->GetOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a form.
|
||||
* This form can be directly used in a twig template.
|
||||
*
|
||||
* @param string $type
|
||||
* @param mixed|null $data
|
||||
* @param array $options
|
||||
*
|
||||
* @return FormInterface
|
||||
*/
|
||||
public function GetForm(string $type = FormType::class, mixed $data = null, array $options = []): FormInterface
|
||||
{
|
||||
if (is_null($data)) {
|
||||
$data = $type::GetDefaultData();
|
||||
}
|
||||
return $this->GetFormBuilder($type, $data, $options)->getForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $aParams
|
||||
* @param $sName
|
||||
* @param $sTemplateFileExtension
|
||||
* @param array $aParams
|
||||
* @param string $sName
|
||||
* @param string $sTemplateFileExtension
|
||||
* @param array|null $aErrors
|
||||
*
|
||||
* @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)) {
|
||||
throw new Exception('Not initialized. Call Controller::InitFromModule() or Controller::SetViewPath() before any display');
|
||||
@@ -784,20 +848,21 @@ abstract class Controller extends AbstractController
|
||||
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 '';
|
||||
} 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 '';
|
||||
}
|
||||
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;
|
||||
@@ -834,13 +899,17 @@ abstract class Controller extends AbstractController
|
||||
$this->oPage = new AjaxPage($this->GetOperationTitle());
|
||||
break;
|
||||
|
||||
case self::ENUM_PAGE_TYPE_TURBO_FORM_AJAX:
|
||||
$this->oPage = new AjaxPage($this->GetOperationTitle());
|
||||
$this->SetContentType('text/vnd.turbo-stream.html');
|
||||
break;
|
||||
|
||||
case self::ENUM_PAGE_TYPE_SETUP:
|
||||
$this->oPage = new SetupPage($this->GetOperationTitle());
|
||||
break;
|
||||
}
|
||||
$this->oTwig->addGlobal('UIBlockParent', [$this->oPage]);
|
||||
$this->oTwig->addGlobal('oPage', $this->oPage);
|
||||
$this->oTwig->addGlobal('debug', utils::IsDevelopmentEnvironment());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -848,7 +917,7 @@ abstract class Controller extends AbstractController
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function GetOperationTitle()
|
||||
public function GetOperationTitle(): string
|
||||
{
|
||||
return Dict::S($this->m_sModule.'/Operation:'.$this->m_sOperation.'/Title');
|
||||
}
|
||||
@@ -867,52 +936,60 @@ abstract class Controller extends AbstractController
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function AddToPage($sContent)
|
||||
private function AddToPage($sContent): void
|
||||
{
|
||||
$this->oPage->add($sContent);
|
||||
}
|
||||
|
||||
private function AddReadyScriptToPage($sScript)
|
||||
private function AddReadyScriptToPage($sScript): void
|
||||
{
|
||||
$this->oPage->add_ready_script($sScript);
|
||||
}
|
||||
|
||||
private function AddScriptToPage($sScript)
|
||||
private function AddScriptToPage($sScript): void
|
||||
{
|
||||
$this->oPage->add_script($sScript);
|
||||
}
|
||||
|
||||
private function AddLinkedScriptToPage($sLinkedScript)
|
||||
private function AddLinkedScriptToPage($sLinkedScript): void
|
||||
{
|
||||
$this->oPage->LinkScriptFromURI($sLinkedScript);
|
||||
}
|
||||
|
||||
private function AddLinkedStylesheetToPage($sLinkedStylesheet)
|
||||
private function AddLinkedStylesheetToPage($sLinkedStylesheet): void
|
||||
{
|
||||
$this->oPage->LinkStylesheetFromURI($sLinkedStylesheet);
|
||||
}
|
||||
|
||||
private function AddStyleToPage($sStyle)
|
||||
private function AddStyleToPage($sStyle): void
|
||||
{
|
||||
$this->oPage->add_style($sStyle);
|
||||
}
|
||||
|
||||
private function AddSaasToPage($sSaasRelPath)
|
||||
private function AddSaasToPage($sSaasRelPath): void
|
||||
{
|
||||
$this->oPage->add_saas($sSaasRelPath);
|
||||
}
|
||||
|
||||
private function AddAjaxTabToPage($sCode, $sTitle, $sURL, $bCache)
|
||||
private function AddAjaxTabToPage($sCode, $sTitle, $sURL, $bCache): void
|
||||
{
|
||||
$this->oPage->AddAjaxTab($sCode, $sURL, $bCache, $sTitle);
|
||||
}
|
||||
|
||||
private function SetContentTypeToPage(): void
|
||||
{
|
||||
if (!is_null($this->sContentType)) {
|
||||
$this->oPage->SetContentType($this->sContentType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sKey
|
||||
* @param $value
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
private function SetBlockParamToPage(string $sKey, $value)
|
||||
private function SetBlockParamToPage(string $sKey, $value): void
|
||||
{
|
||||
$this->oPage->SetBlockParam($sKey, $value);
|
||||
}
|
||||
@@ -920,12 +997,12 @@ abstract class Controller extends AbstractController
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function OutputPage()
|
||||
private function OutputPage(): void
|
||||
{
|
||||
$this->oPage->output();
|
||||
}
|
||||
|
||||
private function InitDebugExtensions()
|
||||
private function InitDebugExtensions(): void
|
||||
{
|
||||
foreach (InterfaceDiscovery::GetInstance()->FindItopClasses(iProfilerExtension::class) as $sExtension) {
|
||||
/** @var \Combodo\iTop\Application\TwigBase\Controller\iProfilerExtension $oExtensionInstance */
|
||||
@@ -936,18 +1013,28 @@ abstract class Controller extends AbstractController
|
||||
|
||||
/**
|
||||
* @param array $aParams
|
||||
* @param string $sPageType
|
||||
*
|
||||
* @return void
|
||||
* @throws \ReflectionException
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function ManageDebugExtensions(array $aParams): void
|
||||
private function ManageDebugExtensions(array $aParams, string $sPageType): void
|
||||
{
|
||||
if (!in_array($sPageType, [self::ENUM_PAGE_TYPE_HTML, self::ENUM_PAGE_TYPE_AJAX, self::ENUM_PAGE_TYPE_TURBO_FORM_AJAX])) {
|
||||
return;
|
||||
}
|
||||
if (!$this->bDebugAllowed && !$this->bDebugForced) {
|
||||
return;
|
||||
}
|
||||
$aProfilesInfo = [];
|
||||
foreach (InterfaceDiscovery::GetInstance()->FindItopClasses(iProfilerExtension::class) as $sExtension) {
|
||||
/** @var \Combodo\iTop\Application\TwigBase\Controller\iProfilerExtension $oExtensionInstance */
|
||||
$oExtensionInstance = $sExtension::GetInstance();
|
||||
$oExtensionInstance->SetDebugForced($this->bDebugForced);
|
||||
if ($oExtensionInstance->IsEnabled()) {
|
||||
$sDebugTemplate = $oExtensionInstance->GetDebugTemplate();
|
||||
$aDebugParams = $oExtensionInstance->GetDebugParams($aParams);
|
||||
@@ -963,8 +1050,81 @@ abstract class Controller extends AbstractController
|
||||
if (is_array($aSaas)) {
|
||||
$this->aSaas = array_merge($this->aSaas, $aSaas);
|
||||
}
|
||||
$this->AddToPage($this->oTwig->render($sDebugTemplate, $aDebugParams));
|
||||
$aProfilesInfo[] = ['sTemplate' => $sDebugTemplate, 'aProfileData' => $aDebugParams];
|
||||
}
|
||||
}
|
||||
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', ['aProfilesInfo' => $aProfilesInfo]));
|
||||
} elseif ($sPageType === self::ENUM_PAGE_TYPE_TURBO_FORM_AJAX) {
|
||||
$this->AddToPage($this->oTwig->render('application/forms/itop_debug_update.html.twig', ['aProfilesInfo' => $aProfilesInfo]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* render error message
|
||||
*
|
||||
* @param array $aErrors
|
||||
*
|
||||
* @return void
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
* @throws \Exception
|
||||
* @since 3.3.0
|
||||
*/
|
||||
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) {
|
||||
if (utils::IsNotNullOrEmptyString($sErrorMsg)) {
|
||||
$this->AddToPage($this->oTwig->render('application/forms/itop_error_update.html.twig', ['sControllerError' => $sErrorMsg]));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->AddToPage($this->oTwig->render('application/forms/itop_error.html.twig', ['sControllerError' => $sErrorMsg]));
|
||||
}
|
||||
|
||||
public function SetDebugAllowed(bool $bDebugAllowed): void
|
||||
{
|
||||
$this->bDebugAllowed = $bDebugAllowed;
|
||||
}
|
||||
|
||||
protected function HandleFormSubmitted(FormBlock $oFormBlock, FormInterface $oForm): bool
|
||||
{
|
||||
$sTrigger = $this->GetRequest()->get($oFormBlock->GetName())['_turbo_trigger'];
|
||||
|
||||
if (!empty($sTrigger)) {
|
||||
|
||||
// Compute blocks to redraw
|
||||
$aBlocksToRedraw = FormTypeHelper::ComputeBlocksToRedraw($oFormBlock, $oForm, $sTrigger);
|
||||
|
||||
// Display turbo response
|
||||
$this->DisplayTurboAjaxPage($aBlocksToRedraw);
|
||||
|
||||
} else {
|
||||
|
||||
// Display turbo response
|
||||
$this->DisplayTurboAjaxPage(['current_form' => $oForm->createView()]);
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ interface iProfilerExtension
|
||||
public function GetLinkedScripts(): null|array;
|
||||
public function GetLinkedStylesheets(): null|array;
|
||||
public function GetSaas(): null|array;
|
||||
public function SetDebugForced(bool $bDebugForced): void;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use Exception;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
use Twig\Attribute\YieldReady;
|
||||
use Twig\Compiler;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Node\Node;
|
||||
@@ -23,6 +24,7 @@ use utils;
|
||||
* @author Eric Espie <eric.espie@combodo.com>
|
||||
* @since 3.0.0
|
||||
*/
|
||||
#[YieldReady]
|
||||
class UIBlockNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
@@ -33,10 +35,10 @@ class UIBlockNode extends Node
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct(string $sFactoryClass, string $sBlockClass, string $sType, $oParams, $oBody, int $iLineNo = 0, ?string $sTag = null)
|
||||
public function __construct(string $sFactoryClass, string $sBlockClass, string $sType, $oParams, $oBody, int $iLineNo = 0)
|
||||
{
|
||||
$aNodes = is_null($oBody) ? [] : ['body' => $oBody];
|
||||
parent::__construct($aNodes, ['type' => $sType, 'params' => $oParams], $iLineNo, $sTag);
|
||||
parent::__construct($aNodes, ['type' => $sType, 'params' => $oParams], $iLineNo);
|
||||
$this->sFactoryClass = $sFactoryClass;
|
||||
$this->sBlockClass = $sBlockClass;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Application\UI\Base\Component\TurboForm;
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||
use Symfony\Component\Form\FormView;
|
||||
|
||||
class TurboForm extends UIContentBlock
|
||||
{
|
||||
// Overloaded constants
|
||||
public const BLOCK_CODE = 'ibo-form';
|
||||
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/turbo-form/layout';
|
||||
|
||||
/** @var string|null */
|
||||
protected ?string $sOnSubmitJsCode;
|
||||
/** @var string|null */
|
||||
protected ?string $sAction;
|
||||
private FormView $oFormView;
|
||||
|
||||
public function __construct(FormView $oFormView, string $sId = null)
|
||||
{
|
||||
parent::__construct($sId);
|
||||
$this->oFormView = $oFormView;
|
||||
$this->sOnSubmitJsCode = null;
|
||||
$this->sAction = null;
|
||||
}
|
||||
|
||||
public function SetOnSubmitJsCode(string $sJsCode)
|
||||
{
|
||||
$this->sOnSubmitJsCode = $sJsCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function GetOnSubmitJsCode(): ?string
|
||||
{
|
||||
return $this->sOnSubmitJsCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function GetAction(): ?string
|
||||
{
|
||||
return $this->sAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sAction
|
||||
*
|
||||
* @return TurboForm
|
||||
*/
|
||||
public function SetAction(string $sAction): TurboForm
|
||||
{
|
||||
$this->sAction = $sAction;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function GetFormView(): FormView
|
||||
{
|
||||
return $this->oFormView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Application\UI\Base\Component\TurboForm;
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\AbstractUIBlockFactory;
|
||||
use Combodo\iTop\Forms\Block\FormBlockService;
|
||||
use Combodo\iTop\Forms\Controller\FormsController;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* Class TurboFormUIBlockFactory
|
||||
*
|
||||
* @api
|
||||
* @since 3.3.0
|
||||
* @package UIBlockAPI
|
||||
*/
|
||||
class TurboFormUIBlockFactory extends AbstractUIBlockFactory
|
||||
{
|
||||
/** @inheritDoc */
|
||||
public const TWIG_TAG_NAME = 'UITurboForm';
|
||||
/** @inheritDoc */
|
||||
public const UI_BLOCK_CLASS_NAME = TurboForm::class;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @param \Symfony\Component\Form\FormView $oFormView
|
||||
* @param string|null $sAction
|
||||
* @param string|null $sId
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\TurboForm\TurboForm An HTML form in which you can add UIBlocks
|
||||
*/
|
||||
public static function MakeStandard(FormView $oFormView, string $sAction = null, string $sId = null): TurboForm
|
||||
{
|
||||
$oTurboForm = new TurboForm($oFormView, $sId);
|
||||
if (!is_null($sAction)) {
|
||||
$oTurboForm->setAction($sAction);
|
||||
}
|
||||
|
||||
return $oTurboForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* For dashlet configuration forms
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @param string $sDashletId
|
||||
* @param string|null $sId
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\TurboForm\TurboForm
|
||||
* @throws \Combodo\iTop\Forms\Block\FormBlockException
|
||||
*/
|
||||
public static function MakeForDashletConfiguration(string $sDashletId, array $aData = [], string $sId = null): TurboForm
|
||||
{
|
||||
$oBlockForm = FormBlockService::GetInstance()->GetFormBlockById($sDashletId, 'Dashlet');
|
||||
$oController = new FormsController();
|
||||
$oBuilder = $oController->GetFormBuilder($oBlockForm, $aData);
|
||||
$oForm = $oBuilder->getForm();
|
||||
|
||||
$oTurboForm = new TurboForm($oForm->createView(), $sId);
|
||||
$oTurboForm->SetAction(utils::GetAbsoluteUrlAppRoot().'pages/UI.php?route=forms.dashlet_configuration&dashlet_code='.urlencode($sDashletId));
|
||||
|
||||
return $oTurboForm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Application\UI\Base\Component\TurboUpdate;
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock;
|
||||
|
||||
class TurboStream extends UIContentBlock
|
||||
{
|
||||
// Overloaded constants
|
||||
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/components/turbo-stream/layout';
|
||||
private string $sTarget;
|
||||
private string $sAction;
|
||||
|
||||
public function __construct(string $sTarget, string $sAction, string $sId = null)
|
||||
{
|
||||
parent::__construct($sId);
|
||||
$this->sTarget = $sTarget;
|
||||
$this->sAction = $sAction;
|
||||
}
|
||||
|
||||
public function GetTarget(): string
|
||||
{
|
||||
return $this->sTarget;
|
||||
}
|
||||
|
||||
public function GetAction(): string
|
||||
{
|
||||
return $this->sAction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Application\UI\Base\Component\TurboUpdate;
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\AbstractUIBlockFactory;
|
||||
|
||||
/**
|
||||
* Class TurboUpdateUIBlockFactory
|
||||
*
|
||||
* @api
|
||||
* @since 3.3.0
|
||||
* @package UIBlockAPI
|
||||
*/
|
||||
class TurboStreamUIBlockFactory extends AbstractUIBlockFactory
|
||||
{
|
||||
/** @inheritDoc */
|
||||
public const TWIG_TAG_NAME = 'UITurboStream';
|
||||
/** @inheritDoc */
|
||||
public const UI_BLOCK_CLASS_NAME = TurboStream::class;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @param string $sTarget Id of the block to update
|
||||
* @param string|null $sId
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\TurboUpdate\TurboStream An HTML form in which you can add UIBlocks
|
||||
*/
|
||||
public static function MakeUpdate(string $sTarget, string $sId = null): TurboStream
|
||||
{
|
||||
return new TurboStream($sTarget, 'update', $sId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @param string $sTarget Id of the block to update
|
||||
* @param string|null $sId
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\TurboUpdate\TurboStream An HTML form in which you can add UIBlocks
|
||||
*/
|
||||
public static function MakeReplace(string $sTarget, string $sId = null): TurboStream
|
||||
{
|
||||
return new TurboStream($sTarget, 'replace', $sId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @param string $sTarget Id of the block to update
|
||||
* @param string|null $sId
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\TurboUpdate\TurboStream An HTML form in which you can add UIBlocks
|
||||
*/
|
||||
public static function MakePrepend(string $sTarget, string $sId = null): TurboStream
|
||||
{
|
||||
return new TurboStream($sTarget, 'prepend', $sId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @param string $sTarget Id of the block to update
|
||||
* @param string|null $sId
|
||||
*
|
||||
* @return \Combodo\iTop\Application\UI\Base\Component\TurboUpdate\TurboStream An HTML form in which you can add UIBlocks
|
||||
*/
|
||||
public static function MakeAppend(string $sTarget, string $sId = null): TurboStream
|
||||
{
|
||||
return new TurboStream($sTarget, 'append', $sId);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ class ErrorPage extends NiceWebPage
|
||||
$this->log_warning($sText);
|
||||
}
|
||||
|
||||
public function error($sText)
|
||||
public function error($sText, \Throwable $oException = null)
|
||||
{
|
||||
$this->add("<div class=\"message message-error\">$sText</div>");
|
||||
if (utils::IsEasterEggAllowed()) {
|
||||
@@ -59,6 +59,10 @@ class ErrorPage extends NiceWebPage
|
||||
$this->add('<img src="'.utils::GetAbsoluteUrlAppRoot().'images/alpha-fatal-error.gif">');
|
||||
$this->add('<div class="message message-valid">'.nl2br(Dict::S('UI:ErrorPage:KittyDisclaimer')).'</div>');
|
||||
}
|
||||
if (!is_null($oException)) {
|
||||
$this->log_exception($oException->getMessage(), $oException);
|
||||
return;
|
||||
}
|
||||
$this->log_error($sText);
|
||||
}
|
||||
|
||||
@@ -78,6 +82,10 @@ class ErrorPage extends NiceWebPage
|
||||
|
||||
return parent::output();
|
||||
}
|
||||
public static function log_exception($sText, \Throwable $oException)
|
||||
{
|
||||
IssueLog::Exception($sText, $oException);
|
||||
}
|
||||
|
||||
public static function log_error($sText)
|
||||
{
|
||||
|
||||
@@ -34,11 +34,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;
|
||||
@@ -181,6 +179,13 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
|
||||
$this->LinkScriptFromAppRoot('js/selectize.min.js');
|
||||
$this->LinkScriptFromAppRoot('node_modules/selectize-plugin-a11y/selectize-plugin-a11y.js');
|
||||
$this->LinkScriptFromAppRoot('js/jquery.multiselect.js');
|
||||
$this->LinkScriptFromAppRoot('node_modules/tom-select/dist/js/tom-select.complete.min.js');
|
||||
$this->LinkScriptFromAppRoot('js/forms/form_element.js');
|
||||
$this->LinkScriptFromAppRoot('js/forms/choices_element.js');
|
||||
$this->LinkScriptFromAppRoot('js/forms/oql_element.js');
|
||||
$this->LinkScriptFromAppRoot('js/forms/collection_element.js');
|
||||
$this->LinkScriptFromAppRoot('js/forms/collection_entry_element.js');
|
||||
$this->LinkScriptFromAppRoot('js/forms/turbo_stream_event_element.js');
|
||||
|
||||
// Used by inline image, CKEditor and other places
|
||||
$this->LinkScriptFromAppRoot('node_modules/magnific-popup/dist/jquery.magnific-popup.min.js');
|
||||
@@ -267,6 +272,8 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
|
||||
|
||||
// Used by external keys and other drop down lists
|
||||
$this->LinkStylesheetFromAppRoot('css/selectize.default.css');
|
||||
$this->LinkStylesheetFromAppRoot('node_modules/tom-select/dist/css/tom-select.bootstrap5.css');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -747,7 +754,7 @@ HTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Render HTKL content
|
||||
// Render HTML content
|
||||
$sHtml = $this->RenderContent();
|
||||
|
||||
// Echo global HTML
|
||||
|
||||
43
sources/Controller/Base/Layout/OqlController.php
Normal file
43
sources/Controller/Base/Layout/OqlController.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2024 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Controller\Base\Layout;
|
||||
|
||||
use Combodo\iTop\Controller\AbstractController;
|
||||
use Exception;
|
||||
use Combodo\iTop\Application\WebPage\JsonPage;
|
||||
use ModelReflection;
|
||||
use ModelReflectionRuntime;
|
||||
|
||||
class OqlController extends AbstractController
|
||||
{
|
||||
public const ROUTE_NAMESPACE = 'oql';
|
||||
|
||||
public function OperationValidateQuery()
|
||||
{
|
||||
$oPage = new JsonPage();
|
||||
$oPage->SetOutputDataOnly(true);
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$sOql = $data['query'];
|
||||
|
||||
try {
|
||||
/** @var ModelReflection $oModelReflection */
|
||||
$oModelReflexion = new ModelReflectionRuntime();
|
||||
$oModelReflexion->GetQuery($sOql);
|
||||
} catch (Exception $e) {
|
||||
|
||||
}
|
||||
|
||||
$oPage->SetData([
|
||||
'is_valid' => !isset($e),
|
||||
]);
|
||||
|
||||
$oPage->output();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -101,6 +101,7 @@ class iTopComposer extends AbstractFolderAnalyzer
|
||||
'symfony/var-dumper/Test',
|
||||
'symfony/var-dumper/Tests/Test',
|
||||
'symfony/var-dumper/Tests',
|
||||
'symfony/validator/Test',
|
||||
'symfony/web-profiler-bundle/Tests',
|
||||
'symfony/yaml/Tests',
|
||||
|
||||
|
||||
@@ -301,6 +301,7 @@ class iTopNPM extends AbstractFolderAnalyzer
|
||||
'selectize-plugin-a11y/examples',
|
||||
'tiny-emitter/test',
|
||||
'toastify-js/example',
|
||||
'tom-select/src',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
479
sources/Forms/Block/AbstractFormBlock.php
Normal file
479
sources/Forms/Block/AbstractFormBlock.php
Normal file
@@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\CollectionBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use Combodo\iTop\Forms\IO\AbstractFormIO;
|
||||
use Combodo\iTop\Forms\IO\Converter\AbstractConverter;
|
||||
use Combodo\iTop\Forms\IO\FormBlockIOException;
|
||||
use Combodo\iTop\Forms\IO\FormInput;
|
||||
use Combodo\iTop\Forms\IO\FormOutput;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Combodo\iTop\Forms\Register\RegisterException;
|
||||
|
||||
/**
|
||||
* Abstract form block.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block
|
||||
* @since 3.3.0
|
||||
*/
|
||||
abstract class AbstractFormBlock implements IFormBlock
|
||||
{
|
||||
/** @var null|FormBlock|CollectionBlock */
|
||||
private FormBlock|CollectionBlock|null $oParent = null;
|
||||
|
||||
/** @var OptionsRegister */
|
||||
private OptionsRegister $oOptionsRegister;
|
||||
|
||||
/** @var IORegister */
|
||||
private IORegister $oIORegister;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $sName
|
||||
* @param array $aOptions
|
||||
*
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function __construct(private readonly string $sName, array $aOptions = [])
|
||||
{
|
||||
// Register options
|
||||
$this->RegisterOptions($this->oOptionsRegister = new OptionsRegister());
|
||||
$this->SetOptions($aOptions);
|
||||
$this->AfterOptionsRegistered($this->oOptionsRegister);
|
||||
|
||||
// Register IO
|
||||
$this->RegisterIO($this->oIORegister = new IORegister($this));
|
||||
$this->AfterIORegistered($this->oIORegister);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the form block name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function GetName(): string
|
||||
{
|
||||
return $this->sName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parent block.
|
||||
*
|
||||
* @param FormBlock|CollectionBlock $oParent
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function SetParent(FormBlock|CollectionBlock $oParent): void
|
||||
{
|
||||
$this->oParent = $oParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent block.
|
||||
*
|
||||
* @return FormBlock|CollectionBlock|null
|
||||
*/
|
||||
public function GetParent(): FormBlock|CollectionBlock|null
|
||||
{
|
||||
return $this->oParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this block is root.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsRootBlock(): bool
|
||||
{
|
||||
return $this->oParent === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register options.
|
||||
*
|
||||
* @param OptionsRegister $oOptionsRegister
|
||||
*
|
||||
* @throws RegisterException
|
||||
*/
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
$oOptionsRegister->SetOption('form_block', $this);
|
||||
$oOptionsRegister->SetOption('form_block_class', get_class($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aOptions
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function SetOptions(array $aOptions): void
|
||||
{
|
||||
foreach ($aOptions as $sOptionName => $mOptionValue) {
|
||||
$this->oOptionsRegister->SetOption($sOptionName, $mOptionValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OptionsRegister $oOptionsRegister
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function AfterOptionsRegistered(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the form block options.
|
||||
* Options will be passed to FormType for building.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetOptions(): array
|
||||
{
|
||||
return $this->oOptionsRegister->GetOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sOption
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function GetOption(string $sOption): mixed
|
||||
{
|
||||
return $this->oOptionsRegister->GetOption($sOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OptionsRegister $oOptionsRegister
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function UpdateOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetBoundInputsBindings(): array
|
||||
{
|
||||
return $this->oIORegister->GetBoundInputsBindings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetBoundOutputBindings(): array
|
||||
{
|
||||
return $this->oIORegister->GetBoundOutputBindings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an input.
|
||||
*
|
||||
* @param string $sName the input name
|
||||
* @param string $sType the type of the input
|
||||
* @param bool $bIsArray whether the input is an array
|
||||
*
|
||||
* @return AbstractFormBlock
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function AddInput(string $sName, string $sType, bool $bIsArray = false): AbstractFormBlock
|
||||
{
|
||||
$this->oIORegister->AddInput($sName, $sType, $bIsArray);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an input connected to another block.
|
||||
*
|
||||
* @param string $sName the input name
|
||||
* @param string $sOutputBlockName
|
||||
* @param string $sOutputName
|
||||
*
|
||||
* @return AbstractFormBlock
|
||||
* @throws FormBlockException
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function AddInputDependsOn(string $sName, string $sOutputBlockName, string $sOutputName): AbstractFormBlock
|
||||
{
|
||||
$this->oIORegister->AddInputDependsOn($sName, $sOutputBlockName, $sOutputName);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an input.
|
||||
*
|
||||
* @param string $sName
|
||||
*
|
||||
* @return FormInput
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function GetInput(string $sName): FormInput
|
||||
{
|
||||
return $this->oIORegister->GetInput($sName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an input value.
|
||||
*
|
||||
* @param string $sName
|
||||
*
|
||||
* @return mixed
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function GetInputValue(string $sName): mixed
|
||||
{
|
||||
return $this->oIORegister->GetInput($sName)->GetValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an output.
|
||||
*
|
||||
* @param string $sName
|
||||
* @param string $sType
|
||||
* @param bool $bIsArray
|
||||
* @param AbstractConverter|null $oConverter
|
||||
*
|
||||
* @return AbstractFormBlock
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function AddOutput(string $sName, string $sType, bool $bIsArray = false, AbstractConverter $oConverter = null): AbstractFormBlock
|
||||
{
|
||||
$this->oIORegister->AddOutput($sName, $sType, $bIsArray, $oConverter);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an output.
|
||||
*
|
||||
* @param string $sName output name
|
||||
*
|
||||
* @return FormOutput
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function GetOutput(string $sName): FormOutput
|
||||
{
|
||||
return $this->oIORegister->GetOutput($sName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the inputs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetInputs(): array
|
||||
{
|
||||
return $this->oIORegister->GetInputs();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetBoundInputs(): array
|
||||
{
|
||||
return $this->oIORegister->GetBoundInputs();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetBoundOutputs(): array
|
||||
{
|
||||
return $this->oIORegister->GetBoundOutputs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the outputs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetOutputs(): array
|
||||
{
|
||||
return $this->oIORegister->GetOutputs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an input to a block output.
|
||||
*
|
||||
* @param string $sInputName the input name
|
||||
* @param string $sOutputBlockName the dependency block name
|
||||
* @param string $sOutputName the dependency output name
|
||||
*
|
||||
* @return $this
|
||||
* @throws FormBlockException
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function InputDependsOn(string $sInputName, string $sOutputBlockName, string $sOutputName): AbstractFormBlock
|
||||
{
|
||||
$this->oIORegister->InputDependsOn($sInputName, $sOutputBlockName, $sOutputName);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an input value.
|
||||
*
|
||||
* @param string $sInputName
|
||||
* @param mixed $oValue
|
||||
*
|
||||
* @return $this
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function SetInputValue(string $sInputName, mixed $oValue): AbstractFormBlock
|
||||
{
|
||||
$this->oIORegister->GetInput($sInputName)->SetValue(AbstractFormIO::EVENT_FORM_STATIC, $oValue);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an input to a parent block input.
|
||||
*
|
||||
* @param string $sInputName input name
|
||||
* @param string $sParentInputName parent input name
|
||||
*
|
||||
* @return $this
|
||||
* @throws FormBlockException
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function InputDependsOnParent(string $sInputName, string $sParentInputName): AbstractFormBlock
|
||||
{
|
||||
$this->oIORegister->InputDependsOnParent($sInputName, $sParentInputName);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an output to a parent block output.
|
||||
*
|
||||
* @param string $sOutputName output name
|
||||
* @param string $sParentOutputName parent output name
|
||||
*
|
||||
* @return $this
|
||||
* @throws FormBlockException
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function OutputImpactParent(string $sOutputName, string $sParentOutputName): AbstractFormBlock
|
||||
{
|
||||
$this->oIORegister->OutputImpactParent($sOutputName, $sParentOutputName);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check existence of one or more dependencies.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function HasDependenciesBlocks(): bool
|
||||
{
|
||||
return $this->oIORegister->HasDependenciesBlocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check existence of one or more dependents blocks.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsImpactingBlocks(): bool
|
||||
{
|
||||
return $this->oIORegister->IsImpactingBlocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dependencies blocks.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetImpactedBlocks(): array
|
||||
{
|
||||
return $this->oIORegister->GetImpactedBlocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inputs data ready.
|
||||
*
|
||||
* @param string|null $sType
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsInputsDataReady(string $sType = null): bool
|
||||
{
|
||||
return $this->oIORegister->IsInputsDataReady($sType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute outputs values.
|
||||
*
|
||||
* @param string $sEventType
|
||||
* @param mixed $oData
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ComputeOutputs(string $sEventType, mixed $oData): void
|
||||
{
|
||||
$this->oIORegister->ComputeOutputs($sEventType, $oData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register IO.
|
||||
*
|
||||
* @param IORegister $oIORegister
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IORegister $oIORegister
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function AfterIORegistered(IORegister $oIORegister): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a binding value has been transmitted.
|
||||
*
|
||||
* @param AbstractFormIO $oBlockIO
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function BindingReceivedEvent(AbstractFormIO $oBlockIO): void
|
||||
{
|
||||
$this->UpdateOptions($this->oOptionsRegister);
|
||||
if ($this->IsInputsDataReady()) {
|
||||
$this->AllInputsReadyEvent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when all inputs are ready.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function AllInputsReadyEvent(): void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
111
sources/Forms/Block/AbstractTypeFormBlock.php
Normal file
111
sources/Forms/Block/AbstractTypeFormBlock.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block;
|
||||
|
||||
use Combodo\iTop\Forms\IO\Format\BooleanIOFormat;
|
||||
use Combodo\iTop\Forms\IO\FormBlockIOException;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Combodo\iTop\Forms\Register\RegisterException;
|
||||
|
||||
/**
|
||||
* Abstract type form block.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block
|
||||
* @since 3.3.0
|
||||
*/
|
||||
abstract class AbstractTypeFormBlock extends AbstractFormBlock
|
||||
{
|
||||
// Inputs
|
||||
public const INPUT_VISIBLE = 'visible';
|
||||
public const INPUT_ENABLE = 'enable';
|
||||
|
||||
/** @var bool flag indicating the form insertion */
|
||||
private bool $bIsAddedToForm = false;
|
||||
|
||||
/**
|
||||
* Return the form type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function GetFormType(): string;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @throws FormBlockIOException
|
||||
*/
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddInput(self::INPUT_VISIBLE, BooleanIOFormat::class);
|
||||
$oIORegister->AddInput(self::INPUT_ENABLE, BooleanIOFormat::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $sEventType
|
||||
*
|
||||
* @return bool
|
||||
* @throws FormBlockException
|
||||
*/
|
||||
public function IsVisible(string $sEventType = null): bool
|
||||
{
|
||||
$oInput = $this->GetInput(self::INPUT_VISIBLE);
|
||||
if (!$oInput->IsBound()) {
|
||||
return true;
|
||||
} elseif (!$oInput->HasEventValue($sEventType)) {
|
||||
return false;
|
||||
} else {
|
||||
return $oInput->GetValue($sEventType)->IsTrue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true
|
||||
*/
|
||||
public function AllowAdd(string $sEventType = null): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The block has been added to its parent.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsAdded(): bool
|
||||
{
|
||||
return $this->bIsAddedToForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the block has been added to its parent.
|
||||
*
|
||||
* @param bool $bIsAdded
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function SetAdded(bool $bIsAdded): void
|
||||
{
|
||||
$this->bIsAddedToForm = $bIsAdded;
|
||||
}
|
||||
|
||||
/** @inheritdoc
|
||||
* @throws RegisterException
|
||||
* @throws FormBlockException
|
||||
*/
|
||||
public function UpdateOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::UpdateOptions($oOptionsRegister);
|
||||
|
||||
if ($this->GetInput(self::INPUT_ENABLE)->IsBound()) {
|
||||
$test = $this->GetInputValue(self::INPUT_ENABLE)->IsTrue();
|
||||
$oOptionsRegister->SetOption('disabled', !$this->GetInputValue(self::INPUT_ENABLE)->IsTrue());
|
||||
}
|
||||
}
|
||||
}
|
||||
47
sources/Forms/Block/Base/CheckboxFormBlock.php
Normal file
47
sources/Forms/Block/Base/CheckboxFormBlock.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\IO\Format\BooleanIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
|
||||
/**
|
||||
* A block to manage a checkbox.
|
||||
* This block expose one output: whether the checkbox is checked or not.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class CheckboxFormBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
// outputs
|
||||
public const OUTPUT_CHECKED = 'checked';
|
||||
|
||||
/** @inheritdoc */
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return CheckboxType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::RegisterOptions($oOptionsRegister);
|
||||
$oOptionsRegister->SetOption('required', false);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddOutput(self::OUTPUT_CHECKED, BooleanIOFormat::class);
|
||||
}
|
||||
}
|
||||
53
sources/Forms/Block/Base/ChoiceFormBlock.php
Normal file
53
sources/Forms/Block/Base/ChoiceFormBlock.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\FormType\Base\ChoiceFormType;
|
||||
use Combodo\iTop\Forms\IO\Converter\ChoiceValueToLabelConverter;
|
||||
use Combodo\iTop\Forms\IO\Format\StringIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
|
||||
/**
|
||||
* A block to manage a list of choices.
|
||||
* This block expose two outputs: the label and the value of the selected choice.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ChoiceFormBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
// Outputs
|
||||
public const OUTPUT_LABEL = 'label';
|
||||
public const OUTPUT_VALUE = 'value';
|
||||
|
||||
/** @inheritdoc */
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return ChoiceFormType::class;
|
||||
}
|
||||
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::RegisterOptions($oOptionsRegister);
|
||||
|
||||
$oOptionsRegister->SetOption('multiple', false);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
|
||||
$bMultiple = $this->GetOption('multiple');
|
||||
|
||||
$oIORegister->AddOutput(self::OUTPUT_LABEL, StringIOFormat::class, $bMultiple, new ChoiceValueToLabelConverter($this));
|
||||
$oIORegister->AddOutput(self::OUTPUT_VALUE, StringIOFormat::class, $bMultiple);
|
||||
}
|
||||
}
|
||||
36
sources/Forms/Block/Base/ChoiceFromInputsBlock.php
Normal file
36
sources/Forms/Block/Base/ChoiceFromInputsBlock.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\IO\FormInput;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
|
||||
/**
|
||||
* A block to manage a list of choices given by forms inputs current values.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ChoiceFromInputsBlock extends ChoiceFormBlock
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public function UpdateOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::UpdateOptions($oOptionsRegister);
|
||||
|
||||
// Compute options based on inputs values
|
||||
$aChoices = [];
|
||||
/** @var FormInput $oInput */
|
||||
foreach ($this->GetInputs() as $oInput) {
|
||||
if ($oInput->HasValue()) {
|
||||
$aChoices[strval($oInput->GetValue())] = $oInput->GetName();
|
||||
}
|
||||
}
|
||||
$oOptionsRegister->SetOption('choices', $aChoices);
|
||||
}
|
||||
}
|
||||
92
sources/Forms/Block/Base/CollectionBlock.php
Normal file
92
sources/Forms/Block/Base/CollectionBlock.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractFormBlock;
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\FormType\Base\CollectionFormType;
|
||||
use Combodo\iTop\Forms\IO\Converter\CollectionToCountConverter;
|
||||
use Combodo\iTop\Forms\IO\Format\IntegerIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Combodo\iTop\Forms\Register\RegisterException;
|
||||
|
||||
/**
|
||||
* A block to manage collections of form blocks.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class CollectionBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
/** @var AbstractTypeFormBlock Prototype block */
|
||||
private AbstractTypeFormBlock $oPrototypeBlock;
|
||||
public const OUTPUT_COUNT = 'count';
|
||||
|
||||
/** @inheritdoc */
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return CollectionFormType::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AbstractFormBlock
|
||||
*/
|
||||
public function GetPrototypeBlock(): AbstractFormBlock
|
||||
{
|
||||
return $this->oPrototypeBlock;
|
||||
}
|
||||
|
||||
public function EntryDependsOnParent(string $sInputName, string $sParentInputName): AbstractFormBlock
|
||||
{
|
||||
$this->oPrototypeBlock->InputDependsOnParent($sInputName, $sParentInputName);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddOutput(self::OUTPUT_COUNT, IntegerIOFormat::class, false, new CollectionToCountConverter());
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::RegisterOptions($oOptionsRegister);
|
||||
|
||||
$oOptionsRegister->SetOption('block_entry_type', FormBlock::class, false);
|
||||
$oOptionsRegister->SetOption('block_entry_options', [], false);
|
||||
$oOptionsRegister->SetOption('prototype', true);
|
||||
$oOptionsRegister->SetOption('allow_add', true);
|
||||
$oOptionsRegister->SetOption('prototype_options', [
|
||||
'label' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function AfterOptionsRegistered(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::AfterOptionsRegistered($oOptionsRegister);
|
||||
|
||||
$oBlockEntryType = $this->GetOption('block_entry_type');
|
||||
$oBlockEntryOptions = $this->GetOption('block_entry_options');
|
||||
$this->oPrototypeBlock = new $oBlockEntryType('prototype', array_merge($this->GetOption('prototype_options'), $oBlockEntryOptions));
|
||||
$this->oPrototypeBlock->SetParent($this);
|
||||
|
||||
try {
|
||||
$oOptionsRegister->SetOption('entry_type', $this->oPrototypeBlock->GetFormType());
|
||||
$oOptionsRegister->SetOption('entry_options', $this->oPrototypeBlock->GetOptions());
|
||||
} catch (RegisterException $e) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
34
sources/Forms/Block/Base/DateFormBlock.php
Normal file
34
sources/Forms/Block/Base/DateFormBlock.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateType;
|
||||
|
||||
/**
|
||||
* A block to manage a date field.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class DateFormBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return DateType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::RegisterOptions($oOptionsRegister);
|
||||
$oOptionsRegister->SetOption('widget', 'single_text');
|
||||
}
|
||||
}
|
||||
34
sources/Forms/Block/Base/DateTimeFormBlock.php
Normal file
34
sources/Forms/Block/Base/DateTimeFormBlock.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
|
||||
/**
|
||||
* A block to manage a date and time field
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class DateTimeFormBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return DateTimeType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::RegisterOptions($oOptionsRegister);
|
||||
$oOptionsRegister->SetOption('widget', 'single_text');
|
||||
}
|
||||
}
|
||||
172
sources/Forms/Block/Base/FormBlock.php
Normal file
172
sources/Forms/Block/Base/FormBlock.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractFormBlock;
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\Block\FormBlockException;
|
||||
use Combodo\iTop\Forms\FormBuilder\DependencyMap;
|
||||
use Combodo\iTop\Forms\FormsException;
|
||||
use Combodo\iTop\Forms\FormType\Base\FormType;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Exception;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
/**
|
||||
* A block to manage a form with children.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
/** @var AbstractFormBlock[] children blocks */
|
||||
private array $aChildrenBlocks = [];
|
||||
public ?DependencyMap $oDependencyMap = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $sName block name
|
||||
* @param array $aOptions options
|
||||
*
|
||||
* @throws FormsException
|
||||
*/
|
||||
public function __construct(string $sName, array $aOptions = [])
|
||||
{
|
||||
parent::__construct($sName, $aOptions);
|
||||
|
||||
try {
|
||||
// Build the form
|
||||
$this->BuildForm();
|
||||
} catch (FormsException $e) {
|
||||
throw $e;
|
||||
} catch (Exception $e) {
|
||||
throw new FormBlockException('Unable to construct form', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return FormType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::RegisterOptions($oOptionsRegister);
|
||||
$oOptionsRegister->SetOption('compound', true);
|
||||
$oOptionsRegister->SetOptionArrayValue('attr', 'class', 'form');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a child form.
|
||||
*
|
||||
* @param string $sName block name
|
||||
* @param string $sBlockClass block class name
|
||||
* @param array $aOptions options
|
||||
*
|
||||
* @return $this
|
||||
* @throws ReflectionException
|
||||
* @throws FormBlockException
|
||||
*/
|
||||
public function Add(string $sName, string $sBlockClass, array $aOptions = []): AbstractFormBlock
|
||||
{
|
||||
$this->VerifyBlockName($sName);
|
||||
$this->VerifyBlockClassName($sBlockClass);
|
||||
|
||||
$aOptions['priority'] = -count($this->aChildrenBlocks);
|
||||
$oSubFormBlock = new ($sBlockClass)($sName, $aOptions);
|
||||
$this->aChildrenBlocks[$sName] = $oSubFormBlock;
|
||||
$oSubFormBlock->SetParent($this);
|
||||
|
||||
return $oSubFormBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sBlockName
|
||||
*
|
||||
* @return void
|
||||
* @throws FormBlockException
|
||||
*/
|
||||
private function VerifyBlockName(string $sBlockName): void
|
||||
{
|
||||
if (!ctype_alnum(str_replace(['-', '_'], '', $sBlockName))) {
|
||||
throw new FormBlockException("Block name '$sBlockName' is not valid. Only alphanumeric characters, hyphens and underscores are allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sBlockClass
|
||||
*
|
||||
* @return void
|
||||
* @throws FormBlockException
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
private function VerifyBlockClassName(string $sBlockClass): void
|
||||
{
|
||||
if (!is_a($sBlockClass, AbstractFormBlock::class, true)) {
|
||||
throw new FormBlockException('The block type '.json_encode($sBlockClass).' is not a subclass of '.json_encode(AbstractFormBlock::class));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the children forms.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetChildren(): array
|
||||
{
|
||||
return $this->aChildrenBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a child block.
|
||||
*
|
||||
* @param string $sName name of the block
|
||||
*
|
||||
* @return AbstractFormBlock
|
||||
* @throws \Combodo\iTop\Forms\Block\FormBlockException
|
||||
*/
|
||||
public function Get(string $sName): AbstractFormBlock
|
||||
{
|
||||
if (!array_key_exists($sName, $this->aChildrenBlocks)) {
|
||||
throw new FormBlockException('Block does not exist '.json_encode($sName));
|
||||
}
|
||||
return $this->aChildrenBlocks[$sName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the form.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function BuildForm(): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function GetSubFormBlock(string $sBlockTurboTriggerName): ?AbstractFormBlock
|
||||
{
|
||||
$oBlock = $this;
|
||||
if (preg_match_all('/\[(?<level>[^\[]+)\]/', $sBlockTurboTriggerName, $aMatches)) {
|
||||
foreach ($aMatches['level'] as $level) {
|
||||
$oBlock = $oBlock->Get($level);
|
||||
}
|
||||
}
|
||||
|
||||
return $oBlock;
|
||||
}
|
||||
|
||||
public function GetDependenciesMap(): ?DependencyMap
|
||||
{
|
||||
return $this->oDependencyMap;
|
||||
}
|
||||
}
|
||||
28
sources/Forms/Block/Base/HiddenFormBlock.php
Normal file
28
sources/Forms/Block/Base/HiddenFormBlock.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
|
||||
/**
|
||||
* A block to manage a hidden field
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class HiddenFormBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return HiddenType::class;
|
||||
}
|
||||
}
|
||||
38
sources/Forms/Block/Base/IntegerFormBlock.php
Normal file
38
sources/Forms/Block/Base/IntegerFormBlock.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\IO\Format\IntegerIOFormat;
|
||||
use Combodo\iTop\Forms\IO\Format\NumberIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
|
||||
/**
|
||||
* A block to manage an integer
|
||||
* This block exposes a single output: the integer value.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class IntegerFormBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
public const OUTPUT_INTEGER = 'integer';
|
||||
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return IntegerType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddOutput(self::OUTPUT_INTEGER, IntegerIOFormat::class);
|
||||
}
|
||||
}
|
||||
39
sources/Forms/Block/Base/NumberFormBlock.php
Normal file
39
sources/Forms/Block/Base/NumberFormBlock.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\IO\Format\NumberIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
|
||||
/**
|
||||
* A block to manage a number input.
|
||||
* This block exposes a single output: the number value.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class NumberFormBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
// Outputs
|
||||
public const OUTPUT_NUMBER = "number";
|
||||
|
||||
/** @inheritdoc */
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return NumberType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddOutput(self::OUTPUT_NUMBER, NumberIOFormat::class);
|
||||
}
|
||||
}
|
||||
27
sources/Forms/Block/Base/TextAreaFormBlock.php
Normal file
27
sources/Forms/Block/Base/TextAreaFormBlock.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
|
||||
/**
|
||||
* A block to manage a textarea.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class TextAreaFormBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return TextareaType::class;
|
||||
}
|
||||
|
||||
}
|
||||
39
sources/Forms/Block/Base/TextFormBlock.php
Normal file
39
sources/Forms/Block/Base/TextFormBlock.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\IO\Format\StringIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
|
||||
/**
|
||||
* A block to manage a text input.
|
||||
* This block exposes a single text output.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class TextFormBlock extends AbstractTypeFormBlock
|
||||
{
|
||||
// Outputs
|
||||
public const OUTPUT_TEXT = "text";
|
||||
|
||||
/** @inheritdoc */
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return TextType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddOutput(self::OUTPUT_TEXT, StringIOFormat::class);
|
||||
}
|
||||
}
|
||||
155
sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php
Normal file
155
sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\DataModel;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
|
||||
use Combodo\iTop\Forms\Block\FormBlockException;
|
||||
use Combodo\iTop\Forms\IO\Format\AttributeIOFormat;
|
||||
use Combodo\iTop\Forms\IO\Format\AttributeTypeIOFormat;
|
||||
use Combodo\iTop\Forms\IO\Format\ClassIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Combodo\iTop\Forms\Register\RegisterException;
|
||||
use Combodo\iTop\Service\DependencyInjection\DIException;
|
||||
use Combodo\iTop\Service\DependencyInjection\DIService;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* A block to choose an attribute from a given class.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\DataModel
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class AttributeChoiceFormBlock extends ChoiceFormBlock
|
||||
{
|
||||
// inputs
|
||||
public const INPUT_CLASS_NAME = 'class';
|
||||
public const INPUT_CATEGORY = 'category';
|
||||
|
||||
// outputs
|
||||
public const OUTPUT_ATTRIBUTE = 'attribute';
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::RegisterOptions($oOptionsRegister);
|
||||
$oOptionsRegister->SetOption('placeholder', 'Select an attribute...');
|
||||
$oOptionsRegister->SetOption('choices', []);
|
||||
}
|
||||
|
||||
/** @inheritdoc
|
||||
* @throws RegisterException
|
||||
*/
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
|
||||
$bMultiple = $this->GetOption('multiple');
|
||||
|
||||
$oIORegister->AddInput(self::INPUT_CLASS_NAME, ClassIOFormat::class);
|
||||
$oIORegister->AddInput(self::INPUT_CATEGORY, AttributeTypeIOFormat::class);
|
||||
// Default value
|
||||
$this->SetInputValue(self::INPUT_CATEGORY, '');
|
||||
$oIORegister->AddOutput(self::OUTPUT_ATTRIBUTE, AttributeIOFormat::class, $bMultiple);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @throws DIException
|
||||
* @throws FormBlockException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function UpdateOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::UpdateOptions($oOptionsRegister);
|
||||
|
||||
// Get class name
|
||||
$sClass = strval($this->GetInputValue(self::INPUT_CLASS_NAME));
|
||||
$sCategory = strval($this->GetInputValue(self::INPUT_CATEGORY));
|
||||
|
||||
// Empty class => no choices
|
||||
if (utils::IsNullOrEmptyString($sClass)) {
|
||||
$oOptionsRegister->SetOption('choices', []);
|
||||
return;
|
||||
}
|
||||
|
||||
$aAttributeCodes = self::ListAttributeCodesByCategory($sClass, $sCategory);
|
||||
|
||||
$oOptionsRegister->SetOption('choices', $aAttributeCodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sClass
|
||||
* @param string|null $sCategory
|
||||
*
|
||||
* @return array
|
||||
* @throws \Combodo\iTop\Service\DependencyInjection\DIException
|
||||
*/
|
||||
public static function ListAttributeCodesByCategory(string $sClass, string $sCategory = ''): array
|
||||
{
|
||||
$oModelReflection = DIService::GetInstance()->GetService('ModelReflection');
|
||||
$aAttributeCodes = [];
|
||||
|
||||
switch ($sCategory) {
|
||||
case 'numeric':
|
||||
foreach ($oModelReflection->ListAttributes($sClass, 'AttributeDecimal,AttributeDuration,AttributeInteger,AttributePercentage,AttributeSubItem') as $sAttCode => $sAttType) {
|
||||
$sLabel = $oModelReflection->GetLabel($sClass, $sAttCode);
|
||||
$aAttributeCodes[$sLabel] = $sAttCode;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'groupable':
|
||||
$aForbiddenAttType = [
|
||||
'AttributeLinkedSet',
|
||||
'AttributeFriendlyName',
|
||||
'iAttributeNoGroupBy', //we cannot only use iAttributeNoGroupBy since this method is also used by the designer who do not have access to the classes' PHP reflection API. So the known classes has to be listed altogether
|
||||
'AttributeOneWayPassword',
|
||||
'AttributeEncryptedString',
|
||||
'AttributePassword',
|
||||
];
|
||||
foreach ($oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) {
|
||||
foreach ($aForbiddenAttType as $sForbiddenAttType) {
|
||||
if (is_a($sAttType, $sForbiddenAttType, true)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$sLabel = $oModelReflection->GetLabel($sClass, $sAttCode);
|
||||
$aAttributeCodes[$sLabel] = $sAttCode;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'enum':
|
||||
foreach ($oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) {
|
||||
if (is_a($sAttType, 'AttributeEnum', true)) {
|
||||
$sLabel = $oModelReflection->GetLabel($sClass, $sAttCode);
|
||||
$aAttributeCodes[$sLabel] = $sAttCode;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
foreach ($oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) {
|
||||
if (is_a($sAttType, 'AttributeDateTime', true)) {
|
||||
$sLabel = $oModelReflection->GetLabel($sClass, $sAttCode);
|
||||
$aAttributeCodes[$sLabel] = $sAttCode;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '':
|
||||
foreach ($oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) {
|
||||
$sLabel = $oModelReflection->GetLabel($sClass, $sAttCode);
|
||||
$aAttributeCodes[$sLabel] = $sAttCode;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $aAttributeCodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\DataModel;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
|
||||
use Combodo\iTop\Forms\IO\Format\AttributeTypeIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
|
||||
/**
|
||||
* A block to choose an attribute type.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\DataModel
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class AttributeTypeChoiceFormBlock extends ChoiceFormBlock
|
||||
{
|
||||
// outputs
|
||||
public const OUTPUT_ATTRIBUTE_TYPE = 'output_type';
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::RegisterOptions($oOptionsRegister);
|
||||
$oOptionsRegister->SetOption('placeholder', 'Select a type...');
|
||||
$oOptionsRegister->SetOption('choices', [
|
||||
'numeric' => 'numeric',
|
||||
'group_by' => 'group_by',
|
||||
'date' => 'date',
|
||||
'enum' => 'enum',
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddOutput(self::OUTPUT_ATTRIBUTE_TYPE, AttributeTypeIOFormat::class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\DataModel;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
|
||||
use Combodo\iTop\Forms\Block\FormBlockException;
|
||||
use Combodo\iTop\Forms\IO\Format\AttributeIOFormat;
|
||||
use Combodo\iTop\Forms\IO\Format\ClassIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Combodo\iTop\Service\DependencyInjection\DIService;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* A block to choose some values from attribute of a given class.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\DataModel
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class AttributeValueChoiceFormBlock extends ChoiceFormBlock
|
||||
{
|
||||
// inputs
|
||||
public const INPUT_CLASS_NAME = 'class';
|
||||
public const INPUT_ATTRIBUTE = 'attribute';
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::RegisterOptions($oOptionsRegister);
|
||||
$oOptionsRegister->SetOption('multiple', true);
|
||||
$oOptionsRegister->SetOptionArrayValue('attr', 'size', 5);
|
||||
$oOptionsRegister->SetOptionArrayValue('attr', 'style', 'height: auto;');
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddInput(self::INPUT_CLASS_NAME, ClassIOFormat::class);
|
||||
$oIORegister->AddInput(self::INPUT_ATTRIBUTE, AttributeIOFormat::class, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @throws FormBlockException
|
||||
*/
|
||||
public function UpdateOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::UpdateOptions($oOptionsRegister);
|
||||
|
||||
$sClass = strval($this->GetInputValue(self::INPUT_CLASS_NAME));
|
||||
$sAttCode = strval($this->GetInputValue(self::INPUT_ATTRIBUTE));
|
||||
|
||||
try {
|
||||
/** @var \ModelReflection $oModelReflection */
|
||||
$oModelReflection = DIService::GetInstance()->GetService('ModelReflection');
|
||||
$aValues = $oModelReflection->GetAllowedValues_att($sClass, $sAttCode);
|
||||
|
||||
$oOptionsRegister->SetOption('choices', array_flip($aValues ?? []));
|
||||
} catch (Exception $e) {
|
||||
// throw new FormBlockException('Update option failed for '.json_encode($this->GetName()), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\DataModel\Dashlet;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
|
||||
use Combodo\iTop\Forms\Block\DataModel\AttributeChoiceFormBlock;
|
||||
use Combodo\iTop\Forms\IO\Format\ClassIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Dict;
|
||||
|
||||
/**
|
||||
* A block to manage an aggregation function list
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\DataModel\Dashlet
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class AggregateFunctionFormBlock extends ChoiceFormBlock
|
||||
{
|
||||
public const INPUT_CLASS_NAME = 'class';
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddInput(self::INPUT_CLASS_NAME, ClassIOFormat::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Combodo\iTop\Forms\Register\OptionsRegister $oOptionsRegister
|
||||
*
|
||||
* @throws \Combodo\iTop\Forms\Block\FormBlockException
|
||||
* @throws \Combodo\iTop\Forms\Register\RegisterException
|
||||
* @throws \Combodo\iTop\Service\DependencyInjection\DIException
|
||||
*/
|
||||
public function UpdateOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::UpdateOptions($oOptionsRegister);
|
||||
|
||||
$sClass = strval($this->GetInputValue(self::INPUT_CLASS_NAME));
|
||||
$aFunctionAttributes = AttributeChoiceFormBlock::ListAttributeCodesByCategory($sClass, 'numeric');
|
||||
$aFunctions = $this->GetAllowedFunctions($aFunctionAttributes);
|
||||
|
||||
$oOptionsRegister->SetOption('choices', $aFunctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $aFunctionAttributes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function GetAllowedFunctions(array $aFunctionAttributes): array
|
||||
{
|
||||
$aFunctions = [];
|
||||
|
||||
$aFunctions[Dict::S('UI:GroupBy:count')] = 'count';
|
||||
|
||||
if (!empty($aFunctionAttributes)) {
|
||||
$aFunctions[Dict::S('UI:GroupBy:sum')] = 'sum';
|
||||
$aFunctions[Dict::S('UI:GroupBy:avg')] = 'avg';
|
||||
$aFunctions[Dict::S('UI:GroupBy:min')] = 'min';
|
||||
$aFunctions[Dict::S('UI:GroupBy:max')] = 'max';
|
||||
}
|
||||
|
||||
return $aFunctions;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\DataModel\Dashlet;
|
||||
|
||||
use Combodo\iTop\Forms\Block\DataModel\AttributeChoiceFormBlock;
|
||||
use Combodo\iTop\Forms\Block\FormBlockException;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
use Combodo\iTop\Service\DependencyInjection\DIService;
|
||||
use Dict;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* A block to manage an attribute of a data model class for grouping purpose
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\DataModel\Dashlet
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ClassAttributeGroupByFormBlock extends AttributeChoiceFormBlock
|
||||
{
|
||||
/**
|
||||
* @param \Combodo\iTop\Forms\Register\OptionsRegister $oOptionsRegister
|
||||
*
|
||||
* @throws \Combodo\iTop\Forms\Block\FormBlockException
|
||||
* @throws \Combodo\iTop\Forms\Register\RegisterException
|
||||
* @throws \Combodo\iTop\Service\DependencyInjection\DIException
|
||||
*/
|
||||
public function UpdateOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::UpdateOptions($oOptionsRegister);
|
||||
$oModelReflection = DIService::GetInstance()->GetService('ModelReflection');
|
||||
|
||||
$aGroupBy = [];
|
||||
try {
|
||||
$sClass = strval($this->GetInputValue(self::INPUT_CLASS_NAME));
|
||||
if ($oModelReflection->IsValidClass($sClass)) {
|
||||
foreach ($oModelReflection->ListAttributes($sClass) as $sAttCode => $sAttType) {
|
||||
// For external fields, find the real type of the target
|
||||
$sExtFieldAttCode = $sAttCode;
|
||||
$sTargetClass = $sClass;
|
||||
while (is_a($sAttType, 'AttributeExternalField', true)) {
|
||||
$sExtKeyAttCode = $oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'extkey_attcode');
|
||||
$sTargetAttCode = $oModelReflection->GetAttributeProperty($sTargetClass, $sExtFieldAttCode, 'target_attcode');
|
||||
$sTargetClass = $oModelReflection->GetAttributeProperty($sTargetClass, $sExtKeyAttCode, 'targetclass');
|
||||
// $aTargetAttCodes = AttributeChoiceFormBlock::ListAttributeCodesByCategory($sTargetClass, 'group_by');
|
||||
$sAttType = $sTargetAttCode;
|
||||
$sExtFieldAttCode = $sTargetAttCode;
|
||||
}
|
||||
|
||||
$sLabel = $oModelReflection->GetLabel($sClass, $sAttCode);
|
||||
if (!in_array($sLabel, $aGroupBy)) {
|
||||
$aGroupBy[$sLabel] = $sAttCode;
|
||||
|
||||
if (is_a($sAttType, 'AttributeDateTime', true)) {
|
||||
$aGroupBy[Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Hour', $sLabel)] = $sAttCode.':hour';
|
||||
$aGroupBy[Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Month', $sLabel)] = $sAttCode.':month';
|
||||
$aGroupBy[Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-Year', $sLabel)] = $sAttCode.':year';
|
||||
$aGroupBy[Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfWeek', $sLabel)] = $sAttCode.':day_of_week';
|
||||
$aGroupBy[Dict::Format('UI:DashletGroupBy:Prop-GroupBy:Select-DayOfMonth', $sLabel)] = $sAttCode.':day_of_month';
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($aGroupBy);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
throw new FormBlockException(__METHOD__.': block issue', 0, $e);
|
||||
}
|
||||
|
||||
$oOptionsRegister->SetOption('choices', $aGroupBy);
|
||||
}
|
||||
}
|
||||
14
sources/Forms/Block/DataModel/LabelFormBlock.php
Normal file
14
sources/Forms/Block/DataModel/LabelFormBlock.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\DataModel;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\TextFormBlock;
|
||||
|
||||
class LabelFormBlock extends TextFormBlock
|
||||
{
|
||||
}
|
||||
50
sources/Forms/Block/DataModel/OqlFormBlock.php
Normal file
50
sources/Forms/Block/DataModel/OqlFormBlock.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\DataModel;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\TextAreaFormBlock;
|
||||
use Combodo\iTop\Forms\FormType\DataModel\OqlFormType;
|
||||
use Combodo\iTop\Forms\IO\Converter\OqlToClassConverter;
|
||||
use Combodo\iTop\Forms\IO\Format\ClassIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Combodo\iTop\Forms\Register\OptionsRegister;
|
||||
|
||||
/**
|
||||
* A block to manage OQL expression input.
|
||||
* This block exposes an output providing the selected class from the OQL.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\DataModel
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class OqlFormBlock extends TextAreaFormBlock
|
||||
{
|
||||
// outputs
|
||||
public const OUTPUT_SELECTED_CLASS = 'selected_class';
|
||||
|
||||
/** @inheritdoc */
|
||||
public function GetFormType(): string
|
||||
{
|
||||
return OqlFormType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterOptions(OptionsRegister $oOptionsRegister): void
|
||||
{
|
||||
parent::RegisterOptions($oOptionsRegister);
|
||||
$oOptionsRegister->SetOption('with_ai_button', false);
|
||||
$oOptionsRegister->SetOptionArrayValue('attr', 'placeholder', 'SELECT Contact');
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddOutput(self::OUTPUT_SELECTED_CLASS, ClassIOFormat::class, false, new OqlToClassConverter());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Expression;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractFormBlock;
|
||||
use Combodo\iTop\Forms\Block\FormBlockException;
|
||||
use Combodo\iTop\Forms\IO\Format\BooleanIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
use Exception;
|
||||
use Expression;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
/**
|
||||
* An abstract block to manage an expression.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Expression
|
||||
* @since 3.3.0
|
||||
*/
|
||||
abstract class AbstractExpressionFormBlock extends AbstractFormBlock
|
||||
{
|
||||
public const EXPRESSION_PATTERN = "/:(?<input>\w+)/";
|
||||
|
||||
/** @inheritdoc
|
||||
* @throws FormBlockException
|
||||
*/
|
||||
public function AllInputsReadyEvent(): void
|
||||
{
|
||||
parent::AllInputsReadyEvent();
|
||||
$this->ComputeExpression(FormEvents::POST_SET_DATA);
|
||||
$this->ComputeExpression(FormEvents::POST_SUBMIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the expression and set the output values.
|
||||
*
|
||||
* @param string $sEventType
|
||||
*
|
||||
* @return mixed
|
||||
* @throws FormBlockException
|
||||
*/
|
||||
public function ComputeExpression(string $sEventType): mixed
|
||||
{
|
||||
$sExpression = $this->GetOption('expression');
|
||||
try {
|
||||
$oExpression = Expression::FromOQL($sExpression);
|
||||
$aFieldsToResolve = $oExpression->ListRequiredFields();
|
||||
$aResolvedParams = [];
|
||||
foreach ($aFieldsToResolve as $sFieldToResolve) {
|
||||
$aResolvedParams[$sFieldToResolve] = strval($this->GetInputValue($sFieldToResolve));
|
||||
}
|
||||
return $oExpression->Evaluate($aResolvedParams);
|
||||
} catch (Exception $e) {
|
||||
throw new FormBlockException('Compute expression '.json_encode($sExpression).' block issue: '.$e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Expression;
|
||||
|
||||
use Combodo\iTop\Forms\Block\FormBlockException;
|
||||
use Combodo\iTop\Forms\IO\Format\BooleanIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
|
||||
/**
|
||||
* An abstract block to manage an expression.
|
||||
* This block expose two boolean outputs: the result of the expression and its negation.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Expression
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class BooleanExpressionFormBlock extends AbstractExpressionFormBlock
|
||||
{
|
||||
// Outputs
|
||||
public const OUTPUT_RESULT = "result";
|
||||
public const OUTPUT_NOT_RESULT = "not_result";
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddOutput(self::OUTPUT_RESULT, BooleanIOFormat::class);
|
||||
$oIORegister->AddOutput(self::OUTPUT_NOT_RESULT, BooleanIOFormat::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the expression and set the output values.
|
||||
*
|
||||
* @param string $sEventType
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Combodo\iTop\Forms\Block\FormBlockException
|
||||
* @throws \Combodo\iTop\Forms\Register\RegisterException
|
||||
*/
|
||||
public function ComputeExpression(string $sEventType): mixed
|
||||
{
|
||||
$oResult = parent::ComputeExpression($sEventType);
|
||||
|
||||
// Update output
|
||||
$bResult = boolval($oResult);
|
||||
$this->GetOutput(self::OUTPUT_RESULT)->SetValue($sEventType, new BooleanIOFormat($bResult));
|
||||
$this->GetOutput(self::OUTPUT_NOT_RESULT)->SetValue($sEventType, new BooleanIOFormat(!$bResult));
|
||||
|
||||
return $oResult;
|
||||
}
|
||||
|
||||
}
|
||||
52
sources/Forms/Block/Expression/NumberExpressionFormBlock.php
Normal file
52
sources/Forms/Block/Expression/NumberExpressionFormBlock.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Expression;
|
||||
|
||||
use Combodo\iTop\Forms\Block\FormBlockException;
|
||||
use Combodo\iTop\Forms\IO\Format\NumberIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
|
||||
/**
|
||||
* A block to manage an number expression.
|
||||
* This block expose a number output: the result of the expression.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block\Expression
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class NumberExpressionFormBlock extends AbstractExpressionFormBlock
|
||||
{
|
||||
// Outputs
|
||||
public const OUTPUT_RESULT = "result";
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddOutput(self::OUTPUT_RESULT, NumberIOFormat::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the expression and set the output values.
|
||||
*
|
||||
* @param string $sEventType
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Combodo\iTop\Forms\Block\FormBlockException
|
||||
* @throws \Combodo\iTop\Forms\Register\RegisterException
|
||||
*/
|
||||
public function ComputeExpression(string $sEventType): mixed
|
||||
{
|
||||
$oResult = parent::ComputeExpression($sEventType);
|
||||
|
||||
// Update output
|
||||
$this->GetOutput(self::OUTPUT_RESULT)->SetValue($sEventType, new NumberIOFormat($oResult));
|
||||
|
||||
return $oResult;
|
||||
}
|
||||
|
||||
}
|
||||
43
sources/Forms/Block/Expression/StringExpressionFormBlock.php
Normal file
43
sources/Forms/Block/Expression/StringExpressionFormBlock.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block\Expression;
|
||||
|
||||
use Combodo\iTop\Forms\IO\Format\StringIOFormat;
|
||||
use Combodo\iTop\Forms\Register\IORegister;
|
||||
|
||||
class StringExpressionFormBlock extends AbstractExpressionFormBlock
|
||||
{
|
||||
// Outputs
|
||||
public const OUTPUT_RESULT = 'result';
|
||||
|
||||
/** @inheritdoc */
|
||||
protected function RegisterIO(IORegister $oIORegister): void
|
||||
{
|
||||
parent::RegisterIO($oIORegister);
|
||||
$oIORegister->AddOutput(self::OUTPUT_RESULT, StringIOFormat::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the expression and set the output values.
|
||||
*
|
||||
* @param string $sEventType
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \Combodo\iTop\Forms\Block\FormBlockException
|
||||
* @throws \Combodo\iTop\Forms\Register\RegisterException
|
||||
*/
|
||||
public function ComputeExpression(string $sEventType): mixed
|
||||
{
|
||||
$oResult = parent::ComputeExpression($sEventType);
|
||||
|
||||
// Update output
|
||||
$this->GetOutput(self::OUTPUT_RESULT)->SetValue($sEventType, new StringIOFormat($oResult));
|
||||
|
||||
return $oResult;
|
||||
}
|
||||
}
|
||||
20
sources/Forms/Block/FormBlockException.php
Normal file
20
sources/Forms/Block/FormBlockException.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block;
|
||||
|
||||
use Combodo\iTop\Forms\FormsException;
|
||||
|
||||
/**
|
||||
* Form block exception.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormBlockException extends FormsException
|
||||
{
|
||||
}
|
||||
32
sources/Forms/Block/FormBlockHelper.php
Normal file
32
sources/Forms/Block/FormBlockHelper.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block;
|
||||
|
||||
/**
|
||||
* Form block helper.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormBlockHelper
|
||||
{
|
||||
/**
|
||||
* Returns a unique form ID for the given form block, based on its hierarchy.
|
||||
*
|
||||
* @param AbstractFormBlock $oForm
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function GetFormId(AbstractFormBlock $oForm): string
|
||||
{
|
||||
if (is_null($oForm->getParent())) {
|
||||
return $oForm->getName();
|
||||
}
|
||||
return self::GetFormId($oForm->getParent()).'_'.$oForm->getName();
|
||||
}
|
||||
}
|
||||
67
sources/Forms/Block/FormBlockService.php
Normal file
67
sources/Forms/Block/FormBlockService.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Block;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use Combodo\iTop\Forms\Compiler\FormsCompiler;
|
||||
use Combodo\iTop\Service\Cache\DataModelDependantCache;
|
||||
use Combodo\iTop\Service\DependencyInjection\DIService;
|
||||
use ModelReflection;
|
||||
use ModelReflectionRuntime;
|
||||
use utils;
|
||||
|
||||
class FormBlockService
|
||||
{
|
||||
public const CACHE_POOL = 'Forms';
|
||||
private static FormBlockService $oInstance;
|
||||
private DataModelDependantCache $oCacheService;
|
||||
|
||||
protected function __construct(ModelReflection $oModelReflection = null)
|
||||
{
|
||||
DIService::GetInstance()->RegisterService('ModelReflection', $oModelReflection ?? new ModelReflectionRuntime());
|
||||
$this->oCacheService = DataModelDependantCache::GetInstance();
|
||||
}
|
||||
|
||||
final public static function GetInstance(ModelReflection $oModelReflection = null): FormBlockService
|
||||
{
|
||||
if (!isset(static::$oInstance)) {
|
||||
static::$oInstance = new FormBlockService($oModelReflection);
|
||||
}
|
||||
|
||||
return static::$oInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sId name of the form to retrieve
|
||||
* @param string $sType
|
||||
*
|
||||
* @return \Combodo\iTop\Forms\Block\Base\FormBlock
|
||||
* @throws \Combodo\iTop\Forms\Block\FormBlockException
|
||||
* @throws \Combodo\iTop\Forms\Compiler\FormsCompilerException
|
||||
* @throws \Combodo\iTop\PropertyTree\PropertyTreeException
|
||||
* @throws \DOMFormatException
|
||||
*/
|
||||
public function GetFormBlockById(string $sId, string $sType): FormBlock
|
||||
{
|
||||
$sFilteredId = preg_replace('/[^0-9a-zA-Z_]/', '', $sId);
|
||||
if (strlen($sFilteredId) === 0 || $sFilteredId !== $sId) {
|
||||
throw new FormBlockException('Malformed name for block: '.json_encode($sId));
|
||||
}
|
||||
$sCacheKey = $sType.'/'.$sFilteredId;
|
||||
if (!$this->oCacheService->HasEntry(self::CACHE_POOL, $sCacheKey) || utils::IsDevelopmentEnvironment()) {
|
||||
// Cache not found, compile the form
|
||||
$sPHPContent = FormsCompiler::GetInstance()->CompileForm($sFilteredId, $sType);
|
||||
$this->oCacheService->StorePhpContent(FormBlockService::CACHE_POOL, $sCacheKey, "<?php\n\n$sPHPContent");
|
||||
}
|
||||
$this->oCacheService->FetchPHP(self::CACHE_POOL, $sCacheKey);
|
||||
$sFormBlockClass = 'FormFor__'.$sFilteredId;
|
||||
|
||||
return new $sFormBlockClass($sFilteredId);
|
||||
}
|
||||
|
||||
}
|
||||
13
sources/Forms/Block/IFormBlock.php
Normal file
13
sources/Forms/Block/IFormBlock.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Forms\Block;
|
||||
|
||||
/**
|
||||
* Form block interface.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Block
|
||||
* @since 3.3.0
|
||||
*/
|
||||
interface IFormBlock
|
||||
{
|
||||
}
|
||||
90
sources/Forms/Compiler/FormsCompiler.php
Normal file
90
sources/Forms/Compiler/FormsCompiler.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Compiler;
|
||||
|
||||
use Combodo\iTop\DesignDocument;
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use Combodo\iTop\Forms\Block\FormBlockService;
|
||||
use Combodo\iTop\PropertyTree\PropertyTreeFactory;
|
||||
use Combodo\iTop\Service\Cache\DataModelDependantCache;
|
||||
use DOMFormatException;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* XML to PHP Forms compiler.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Compiler
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormsCompiler
|
||||
{
|
||||
private static FormsCompiler $oInstance;
|
||||
private DataModelDependantCache $oCacheService;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
$this->oCacheService = DataModelDependantCache::GetInstance();
|
||||
}
|
||||
|
||||
final public static function GetInstance(): FormsCompiler
|
||||
{
|
||||
if (!isset(static::$oInstance)) {
|
||||
static::$oInstance = new FormsCompiler();
|
||||
}
|
||||
|
||||
return static::$oInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile XML property tree into PHP to create the configuration form
|
||||
*
|
||||
* @param string $sXMLContent property tree structure in xml
|
||||
*
|
||||
* @return string Generated PHP
|
||||
* @throws \Combodo\iTop\Forms\Compiler\FormsCompilerException
|
||||
* @throws \Combodo\iTop\PropertyTree\PropertyTreeException
|
||||
* @throws \DOMFormatException
|
||||
*/
|
||||
public function CompileFormFromXML(string $sXMLContent): string
|
||||
{
|
||||
$oDoc = new DesignDocument();
|
||||
libxml_clear_errors();
|
||||
$oDoc->loadXML($sXMLContent);
|
||||
$aErrors = libxml_get_errors();
|
||||
if (count($aErrors) > 0) {
|
||||
throw new FormsCompilerException('Dashlet properties definition not correctly formatted!');
|
||||
}
|
||||
|
||||
/** @var \Combodo\iTop\DesignElement $oRoot */
|
||||
$oRoot = $oDoc->firstChild;
|
||||
$oPropertyTree = PropertyTreeFactory::GetInstance()->CreateTreeFromDom($oRoot);
|
||||
|
||||
return $oPropertyTree->ToPHPFormBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sId
|
||||
* @param string $sType
|
||||
*
|
||||
* @return string Generated PHP
|
||||
* @throws \Combodo\iTop\Forms\Compiler\FormsCompilerException
|
||||
* @throws \Combodo\iTop\PropertyTree\PropertyTreeException
|
||||
* @throws \DOMFormatException
|
||||
*/
|
||||
public function CompileForm(string $sId, string $sType): string
|
||||
{
|
||||
$sPath = utils::GetAbsoluteModulePath('core')."property_trees/$sType/$sId.xml";
|
||||
if (!file_exists($sPath)) {
|
||||
throw new FormsCompilerException("Properties definition $sType/$sId not present");
|
||||
}
|
||||
|
||||
$sXMLContent = file_get_contents($sPath);
|
||||
|
||||
return $this->CompileFormFromXML($sXMLContent);
|
||||
}
|
||||
}
|
||||
14
sources/Forms/Compiler/FormsCompilerException.php
Normal file
14
sources/Forms/Compiler/FormsCompilerException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Compiler;
|
||||
|
||||
use Combodo\iTop\Forms\FormsException;
|
||||
|
||||
class FormsCompilerException extends FormsException
|
||||
{
|
||||
}
|
||||
64
sources/Forms/Controller/FormsController.php
Normal file
64
sources/Forms/Controller/FormsController.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Controller;
|
||||
|
||||
use Combodo\iTop\Application\TwigBase\Controller\Controller;
|
||||
use Combodo\iTop\Forms\Block\FormBlockService;
|
||||
use Combodo\iTop\ItopSdkFormDemonstrator\Helper\ItopSdkFormDemonstratorLog;
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
|
||||
class FormsController extends Controller
|
||||
{
|
||||
public const ROUTE_NAMESPACE = 'forms';
|
||||
|
||||
public function __construct($sViewPath = '', $sModuleName = 'core', $aAdditionalPaths = [])
|
||||
{
|
||||
$sModuleName = 'core';
|
||||
$sViewPath = APPROOT.'templates/application/forms';
|
||||
parent::__construct($sViewPath, $sModuleName, $aAdditionalPaths);
|
||||
}
|
||||
|
||||
public function OperationDashletConfiguration()
|
||||
{
|
||||
try {
|
||||
$oRequest = $this->getRequest();
|
||||
$sDashletId = $oRequest->query->get('dashlet_code');
|
||||
|
||||
// Get the form block from the service (and the compiler)
|
||||
$oFormBlock = FormBlockService::GetInstance()->GetFormBlockById($sDashletId, 'Dashlet');
|
||||
$oBuilder = $this->GetFormBuilder($oFormBlock, []);
|
||||
$oForm = $oBuilder->getForm();
|
||||
$oForm->handleRequest($oRequest);
|
||||
|
||||
if ($oForm->isSubmitted()) {
|
||||
if ($oForm->isValid()) {
|
||||
IssueLog::Info('form is valid');
|
||||
}
|
||||
|
||||
// Compute blocks to redraw
|
||||
$this->HandleFormSubmitted($oFormBlock, $oForm);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// $this->DisplayPage([
|
||||
// 'form' => $oForm->createView(),
|
||||
// 'sAction' => utils::GetAbsoluteUrlAppRoot().'pages/UI.php?route=forms.dashlet_configuration&dashlet_code='.urlencode($sDashletId),
|
||||
// ], 'itop_form');
|
||||
|
||||
} catch (Exception $e) {
|
||||
ItopSdkFormDemonstratorLog::Exception($e->getMessage(), $e);
|
||||
$this->DisplayPage([
|
||||
'sControllerError' => $e->getMessage(),
|
||||
], 'itop_error_update', Controller::ENUM_PAGE_TYPE_TURBO_FORM_AJAX);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
240
sources/Forms/FormBuilder/DependencyHandler.php
Normal file
240
sources/Forms/FormBuilder/DependencyHandler.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormBuilder;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractFormBlock;
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use Combodo\iTop\Forms\Block\FormBlockException;
|
||||
use Exception;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
/**
|
||||
* Handler responsible for form blocks dependencies.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormBuilder
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class DependencyHandler
|
||||
{
|
||||
public static array $aDependencyHandlers = [];
|
||||
|
||||
/** @var DependencyMap dependencies map */
|
||||
private DependencyMap $oDependenciesMap;
|
||||
|
||||
/** @var array events */
|
||||
private array $aEvents = [];
|
||||
private readonly FormBuilder $oFormBuilder;
|
||||
private readonly FormBlock $oFormBlock;
|
||||
private readonly array $aDependentBlocks;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param FormBuilder $oFormBuilder The form builder
|
||||
* @param FormBlock $oFormBlock The block attached to the builder
|
||||
* @param array $aDependentBlocks Dependants blocks
|
||||
*/
|
||||
public function __construct(FormBuilder $oFormBuilder, FormBlock $oFormBlock, array $aDependentBlocks)
|
||||
{
|
||||
$this->aDependentBlocks = $aDependentBlocks;
|
||||
$this->oFormBuilder = $oFormBuilder;
|
||||
$this->oFormBlock = $oFormBlock;
|
||||
|
||||
// dependencies map
|
||||
$this->oDependenciesMap = new DependencyMap($aDependentBlocks);
|
||||
|
||||
// Add form ready listener
|
||||
$this->AddFormReadyListener();
|
||||
|
||||
// Check the dependencies (handle internal binding)
|
||||
$this->CheckDependencies($this->oFormBuilder);
|
||||
|
||||
// Store the dependency handler (debug purpose)
|
||||
self::$aDependencyHandlers[] = $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the form name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function GetName(): string
|
||||
{
|
||||
return $this->oFormBuilder->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the debug data.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetDebugData(): array
|
||||
{
|
||||
return $this->aEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dependencies map.
|
||||
*
|
||||
* @return DependencyMap
|
||||
*/
|
||||
public function GetMap(): DependencyMap
|
||||
{
|
||||
return $this->oDependenciesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add form ready listener.
|
||||
*
|
||||
* Listen the form PRE_SET_DATA
|
||||
* First event from Symfony framework, we know that the form is built at this step.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function AddFormReadyListener(): void
|
||||
{
|
||||
// Initialize the dependencies listeners once the form is built
|
||||
$this->oFormBuilder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
|
||||
|
||||
/** Iterate throw blocks impacting other but without dependencies */
|
||||
foreach ($this->oDependenciesMap->GetImpactingBlocksWithoutDependencies() as $sOutputBlockName) {
|
||||
|
||||
// Add event
|
||||
$this->AddEvent('form.listen', $sOutputBlockName);
|
||||
|
||||
// Listen the output block POST_SET_DATA & POST_SUBMIT
|
||||
$this->oFormBuilder->get($sOutputBlockName)->addEventListener(FormEvents::POST_SET_DATA, $this->GetEventListeningCallback());
|
||||
$this->oFormBuilder->get($sOutputBlockName)->addEventListener(FormEvents::POST_SUBMIT, $this->GetEventListeningCallback());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the listening callback.
|
||||
*
|
||||
* @return callable
|
||||
*/
|
||||
private function GetEventListeningCallback(): callable
|
||||
{
|
||||
return function (FormEvent $oEvent) {
|
||||
|
||||
// Get the event type
|
||||
$sEventType = FormHelper::GetEventType($oEvent);
|
||||
|
||||
// Add event
|
||||
$this->AddEvent($sEventType, $oEvent->getForm()->getName(), $oEvent->getData());
|
||||
|
||||
// Get the form
|
||||
$oForm = $oEvent->getForm();
|
||||
|
||||
// Get the form block
|
||||
$oFormBlock = $this->oFormBlock->Get($oForm->getName());
|
||||
|
||||
// Compute the block outputs with the data
|
||||
try {
|
||||
$oFormBlock->ComputeOutputs($sEventType, $oForm->getData());
|
||||
} catch (Exception $e) {
|
||||
$oForm->addError(new FormError($e->getMessage()));
|
||||
}
|
||||
|
||||
// Check dependencies
|
||||
$this->CheckDependencies($oForm->getParent(), $oForm->getName(), $sEventType);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormInterface|FormBuilderInterface $oForm
|
||||
* @param string|null $sOutputBlock
|
||||
* @param string|null $sEventType
|
||||
*
|
||||
* @return void
|
||||
* @throws FormBlockException
|
||||
*/
|
||||
private function CheckDependencies(FormInterface|FormBuilderInterface $oForm, string $sOutputBlock = null, string $sEventType = null): void
|
||||
{
|
||||
$aImpactedBlocks = $this->aDependentBlocks;
|
||||
if ($sOutputBlock !== null) {
|
||||
$aImpactedBlocks = $this->oDependenciesMap->GetBlocksImpactedBy($sOutputBlock, function (AbstractFormBlock $oBlock) use ($sEventType) {
|
||||
return $oBlock instanceof AbstractTypeFormBlock;
|
||||
});
|
||||
}
|
||||
|
||||
/** Iterate throw dependencies... @var AbstractFormBlock $oDependentBlock */
|
||||
foreach ($aImpactedBlocks as $oDependentBlock) {
|
||||
if (!$oDependentBlock instanceof AbstractTypeFormBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// When dependencies met, add the dependent field if not already done or options changed
|
||||
if ($oDependentBlock->IsVisible($sEventType) && $oDependentBlock->IsInputsDataReady($sEventType)) {
|
||||
|
||||
// Get the Symfony options
|
||||
$aOptions = $oDependentBlock->GetOptions();
|
||||
|
||||
// Add the listener callback to the dependent field if it is also a dependency for another field
|
||||
if ($this->oDependenciesMap->HasBlocksImpactedBy($oDependentBlock->getName())) {
|
||||
|
||||
// Pass the listener call back to be registered by the dependency form builder
|
||||
$aOptions = array_merge($aOptions, [
|
||||
'builder_listener' => $this->GetEventListeningCallback(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($oDependentBlock->AllowAdd($sEventType)) {
|
||||
|
||||
// Add events
|
||||
$this->AddEvent('form.add', $oDependentBlock->getName());
|
||||
if (array_key_exists('builder_listener', $aOptions)) {
|
||||
$this->AddEvent('form.listen.after', $oDependentBlock->getName());
|
||||
}
|
||||
|
||||
// Mark the dependency as added
|
||||
$oDependentBlock->SetAdded(true);
|
||||
|
||||
// Add the dependent field to the form
|
||||
$oForm->add($oDependentBlock->GetName(), $oDependentBlock->GetFormType(), $aOptions);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($oDependentBlock->IsAdded() && (!$oDependentBlock->IsVisible($sEventType) || !$oDependentBlock->IsInputsDataReady($sEventType))) {
|
||||
$oForm->remove($oDependentBlock->GetName());
|
||||
$oDependentBlock->SetAdded(false);
|
||||
|
||||
// Add event
|
||||
$this->AddEvent('form.remove', $oDependentBlock->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a debug event.
|
||||
*
|
||||
* @param string $sEvent
|
||||
* @param string $sForm
|
||||
* @param mixed $oValue
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function AddEvent(string $sEvent, string $sForm, mixed $oValue = 'NA'): void
|
||||
{
|
||||
$this->aEvents[] = [
|
||||
'builder' => $this->oFormBuilder->getName(),
|
||||
'event' => $sEvent,
|
||||
'form' => $sForm,
|
||||
'value' => $oValue,
|
||||
];
|
||||
}
|
||||
}
|
||||
240
sources/Forms/FormBuilder/DependencyMap.php
Normal file
240
sources/Forms/FormBuilder/DependencyMap.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormBuilder;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Expression\AbstractExpressionFormBlock;
|
||||
use Combodo\iTop\Forms\Block\FormBlock;
|
||||
use Combodo\iTop\Forms\IO\FormBinding;
|
||||
use Combodo\iTop\Forms\IO\FormInput;
|
||||
use Combodo\iTop\Forms\IO\FormOutput;
|
||||
|
||||
/**
|
||||
* Map containing information of form block dependencies.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormBuilder
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class DependencyMap
|
||||
{
|
||||
/** @var array array of blocks impacted by dependence */
|
||||
private array $aBlocksImpactedBy = [];
|
||||
|
||||
/** @var array array of binding */
|
||||
private array $aBindings = [];
|
||||
|
||||
/** @var array array of binding (OUT > OUT) grouped by block and output name */
|
||||
private array $aBindingsOutputToInput = [];
|
||||
|
||||
/** @var array array of binding (IN > IN) grouped by block and output name */
|
||||
private array $aBindingsInputToInput = [];
|
||||
|
||||
/** @var array array of binding (OUT > OUT) grouped by block and output name */
|
||||
private array $aBindingsOutputToOutputs = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $aBlocksWithDependencies
|
||||
*/
|
||||
public function __construct(private readonly array $aBlocksWithDependencies)
|
||||
{
|
||||
// Initialization
|
||||
$this->Init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function Init(): void
|
||||
{
|
||||
/** Iterate throw blocks with dependencies... @var AbstractFormBlock $oDependentBlock */
|
||||
foreach ($this->aBlocksWithDependencies as $oDependentBlock) {
|
||||
|
||||
/** Iterate throw the block inputs bindings... @var FormBinding $oBinding**/
|
||||
foreach ($oDependentBlock->GetBoundInputsBindings() as $oBinding) {
|
||||
|
||||
// OUT > IN
|
||||
if ($oBinding->oSourceIO instanceof FormOutput
|
||||
&& $oBinding->oDestinationIO instanceof FormInput) {
|
||||
$this->AddBindingToMap($this->aBindingsOutputToInput, $oBinding);
|
||||
$this->AddToBlockImpactedBy($oBinding->oSourceIO->GetOwnerBlock()->GetName(), $oDependentBlock);
|
||||
}
|
||||
|
||||
// IN > IN
|
||||
if ($oBinding->oSourceIO instanceof FormInput
|
||||
&& $oBinding->oDestinationIO instanceof FormInput) {
|
||||
$this->AddBindingToMap($this->aBindingsInputToInput, $oBinding);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Iterate throw the block inputs connections... @var FormBinding $oBinding**/
|
||||
foreach ($oDependentBlock->GetBoundOutputBindings() as $oBinding) {
|
||||
|
||||
// OUT > OUT
|
||||
if ($oBinding->oSourceIO instanceof FormOutput
|
||||
&& $oBinding->oDestinationIO instanceof FormOutput) {
|
||||
$this->AddBindingToMap($this->aBindingsOutputToOutputs, $oBinding);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a binding to a map.
|
||||
*
|
||||
* @param array $map
|
||||
* @param FormBinding $oBinding
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function AddBindingToMap(array &$map, FormBinding $oBinding): void
|
||||
{
|
||||
// Binding information
|
||||
$sBlockName = $oBinding->oSourceIO->GetOwnerBlock()->GetName();
|
||||
$sIOName = $oBinding->oSourceIO->GetName();
|
||||
|
||||
// initialize map
|
||||
if (!isset($map[$sBlockName])) {
|
||||
$map[$sBlockName] = [];
|
||||
}
|
||||
if (!isset($map[$sBlockName][$sIOName])) {
|
||||
$map[$sBlockName][$sIOName] = [];
|
||||
}
|
||||
|
||||
// add to map
|
||||
$map[$sBlockName][$sIOName][] = $oBinding;
|
||||
$this->aBindings[] = $oBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sDependsOnName
|
||||
* @param AbstractFormBlock $oImpactedBlock
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function AddToBlockImpactedBy(string $sDependsOnName, AbstractFormBlock $oImpactedBlock): void
|
||||
{
|
||||
// Initialize array for this dependence
|
||||
if (!array_key_exists($sDependsOnName, $this->aBlocksImpactedBy)) {
|
||||
$this->aBlocksImpactedBy[$sDependsOnName] = [];
|
||||
}
|
||||
|
||||
// Add the block
|
||||
$this->aBlocksImpactedBy[$sDependsOnName][$oImpactedBlock->GetName()] = $oImpactedBlock;
|
||||
|
||||
// TODO
|
||||
if ($oImpactedBlock instanceof AbstractExpressionFormBlock) {
|
||||
foreach ($oImpactedBlock->GetOutputs() as $oOutput) {
|
||||
foreach ($oOutput->GetBindings() as $oBinding) {
|
||||
$this->AddToBlockImpactedBy($sDependsOnName, $oBinding->oDestinationIO->GetOwnerBlock());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetImpactingBlocksWithoutDependencies(): array
|
||||
{
|
||||
$aResult = [];
|
||||
|
||||
// Iterate throw binding OUT > IN
|
||||
foreach (array_keys($this->aBindingsOutputToInput) as $sOutputBlockName) {
|
||||
|
||||
// Exclude block containing dependencies
|
||||
if (!array_key_exists($sOutputBlockName, $this->aBlocksWithDependencies)) {
|
||||
$aResult[] = $sOutputBlockName;
|
||||
}
|
||||
}
|
||||
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block impacted by a given block.
|
||||
* The blocks can be filtered using a callable.
|
||||
*
|
||||
* @param string $sBlockName
|
||||
* @param callable|null $oFilter
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function GetBlocksImpactedBy(string $sBlockName, callable $oFilter = null): ?array
|
||||
{
|
||||
if (!array_key_exists($sBlockName, $this->aBlocksImpactedBy)) {
|
||||
return null;
|
||||
}
|
||||
$aBlocks = $this->aBlocksImpactedBy[$sBlockName];
|
||||
|
||||
// Filtering
|
||||
if ($oFilter !== null) {
|
||||
$aBlocks = array_filter($aBlocks, $oFilter);
|
||||
}
|
||||
|
||||
return $aBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block impacts other blocks.
|
||||
*
|
||||
* @param string $sBlockName
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function HasBlocksImpactedBy(string $sBlockName): bool
|
||||
{
|
||||
return $this->GetBlocksImpactedBy($sBlockName) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bindings OUT > IN.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetOutputToInputs(): array
|
||||
{
|
||||
return $this->aBindingsOutputToInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bindings IN > IN.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetInputToInputs(): array
|
||||
{
|
||||
return $this->aBindingsInputToInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bindings OUT > OUT.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetOutputToOutputs(): array
|
||||
{
|
||||
return $this->aBindingsOutputToOutputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all bindings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetAllBindings()
|
||||
{
|
||||
return $this->aBindings;
|
||||
}
|
||||
}
|
||||
539
sources/Forms/FormBuilder/FormBuilder.php
Normal file
539
sources/Forms/FormBuilder/FormBuilder.php
Normal file
@@ -0,0 +1,539 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormBuilder;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractFormBlock;
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use IteratorAggregate;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Form\DataMapperInterface;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormConfigInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\RequestHandlerInterface;
|
||||
use Symfony\Component\Form\ResolvedFormTypeInterface;
|
||||
use Symfony\Component\PropertyAccess\PropertyPathInterface;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Specialization of the Symfony form builder to handle iTop form blocks and dependencies.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormBuilder
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormBuilder implements FormBuilderInterface, IteratorAggregate
|
||||
{
|
||||
/** @var DependencyHandler|null */
|
||||
private ?DependencyHandler $oDependencyHandler = null;
|
||||
|
||||
/** @var AbstractFormBlock */
|
||||
private AbstractFormBlock $oFormBlock;
|
||||
|
||||
/** @var FormBuilderInterface */
|
||||
private readonly FormBuilderInterface $builder;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param 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) {
|
||||
$this->BuildForm($oFormBlock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the form.
|
||||
*
|
||||
* @param FormBlock $oFormBlock
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function BuildForm(FormBlock $oFormBlock): void
|
||||
{
|
||||
// Prevent form build option
|
||||
$aOptions = $this->builder->getOptions();
|
||||
if (array_key_exists('prevent_form_build', $aOptions) && $aOptions['prevent_form_build']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aBlocksWithDependencies = [];
|
||||
/** Iterate throw the form sub blocks... @var FormBlock $oSubFormBlock */
|
||||
foreach ($oFormBlock->GetChildren() as $sBlockName => $oChildBlock) {
|
||||
|
||||
// Handle child block
|
||||
$bHasDependency = $this->HandleChildBlock($oChildBlock);
|
||||
|
||||
// Add to the array of blocks with dependencies
|
||||
if ($bHasDependency) {
|
||||
$aBlocksWithDependencies[$sBlockName] = $oChildBlock;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create a dependency handler if needed
|
||||
if (count($aBlocksWithDependencies) > 0) {
|
||||
$this->oDependencyHandler = new DependencyHandler($this, $oFormBlock, $aBlocksWithDependencies);
|
||||
$oFormBlock->oDependencyMap = $this->oDependencyHandler->GetMap();
|
||||
}
|
||||
|
||||
if ($oFormBlock->IsRootBlock()) {
|
||||
// Insert a hidden type to save the place
|
||||
$this->builder->add('_turbo_trigger', HiddenType::class, [
|
||||
'prevent_form_build' => true,
|
||||
'mapped' => false,
|
||||
'priority' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sub block.
|
||||
*
|
||||
* @param AbstractFormBlock $oSubFormBlock
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function HandleChildBlock(AbstractFormBlock $oSubFormBlock): bool
|
||||
{
|
||||
|
||||
// Has dependencies blocks
|
||||
if (!$oSubFormBlock->HasDependenciesBlocks()) {
|
||||
if ($oSubFormBlock instanceof AbstractTypeFormBlock) {
|
||||
// Directly insert the block corresponding form type
|
||||
$this->add($oSubFormBlock->GetName(), $oSubFormBlock->GetFormType(), $oSubFormBlock->GetOptions());
|
||||
$oSubFormBlock->SetAdded(true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dependency handler attached to this builder.
|
||||
*
|
||||
* @return DependencyHandler|null
|
||||
*/
|
||||
protected function GetDependencyHandler(): ?DependencyHandler
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return $this->builder->getIterator();
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return $this->builder->count();
|
||||
}
|
||||
|
||||
public function create(string $name, ?string $type = null, array $options = []): FormBuilderInterface
|
||||
{
|
||||
return $this->builder->create($name, $type, $options);
|
||||
}
|
||||
|
||||
public function get(string $name): FormBuilderInterface
|
||||
{
|
||||
return $this->builder->get($name);
|
||||
}
|
||||
|
||||
public function remove(string $name): static
|
||||
{
|
||||
$this->builder->remove($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return $this->builder->has($name);
|
||||
}
|
||||
|
||||
public function all(): array
|
||||
{
|
||||
return $this->builder->all();
|
||||
}
|
||||
|
||||
public function getForm(): FormInterface
|
||||
{
|
||||
return $this->builder->getForm();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function setFormFactory(FormFactoryInterface $formFactory)
|
||||
{
|
||||
$this->builder->setFormFactory($formFactory);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function getFormConfig(): FormConfigInterface
|
||||
{
|
||||
return $this->builder->getFormConfig();
|
||||
}
|
||||
|
||||
public function setIsEmptyCallback(?callable $isEmptyCallback): static
|
||||
{
|
||||
$this->builder->setIsEmptyCallback($isEmptyCallback);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEventDispatcher(): EventDispatcherInterface
|
||||
{
|
||||
return $this->builder->getEventDispatcher();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->builder->getName();
|
||||
}
|
||||
|
||||
public function getPropertyPath(): ?PropertyPathInterface
|
||||
{
|
||||
return $this->builder->getPropertyPath();
|
||||
}
|
||||
|
||||
public function getMapped(): bool
|
||||
{
|
||||
return $this->builder->getMapped();
|
||||
}
|
||||
|
||||
public function getByReference(): bool
|
||||
{
|
||||
return $this->builder->getByReference();
|
||||
}
|
||||
|
||||
public function getInheritData(): bool
|
||||
{
|
||||
return $this->builder->getInheritData();
|
||||
}
|
||||
|
||||
public function getCompound(): bool
|
||||
{
|
||||
return $this->builder->getCompound();
|
||||
}
|
||||
|
||||
public function getType(): ResolvedFormTypeInterface
|
||||
{
|
||||
return $this->builder->getType();
|
||||
}
|
||||
|
||||
public function getViewTransformers(): array
|
||||
{
|
||||
return $this->builder->getViewTransformers();
|
||||
}
|
||||
|
||||
public function getModelTransformers(): array
|
||||
{
|
||||
return $this->builder->getModelTransformers();
|
||||
}
|
||||
|
||||
public function getDataMapper(): ?DataMapperInterface
|
||||
{
|
||||
return $this->builder->getDataMapper();
|
||||
}
|
||||
|
||||
public function getRequired(): bool
|
||||
{
|
||||
return $this->builder->getRequired();
|
||||
}
|
||||
|
||||
public function getDisabled(): bool
|
||||
{
|
||||
return $this->builder->getDisabled();
|
||||
}
|
||||
|
||||
public function getErrorBubbling(): bool
|
||||
{
|
||||
return $this->builder->getErrorBubbling();
|
||||
}
|
||||
|
||||
public function getEmptyData(): mixed
|
||||
{
|
||||
return $this->builder->getEmptyData();
|
||||
}
|
||||
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->builder->getAttributes();
|
||||
}
|
||||
|
||||
public function hasAttribute(string $name): bool
|
||||
{
|
||||
return $this->builder->hasAttribute($name);
|
||||
}
|
||||
|
||||
public function getAttribute(string $name, mixed $default = null): mixed
|
||||
{
|
||||
return $this->builder->getAttribute($name, $default);
|
||||
}
|
||||
|
||||
public function getData(): mixed
|
||||
{
|
||||
return $this->builder->getData();
|
||||
}
|
||||
|
||||
public function getDataClass(): ?string
|
||||
{
|
||||
return $this->builder->getDataClass();
|
||||
}
|
||||
|
||||
public function getDataLocked(): bool
|
||||
{
|
||||
return $this->builder->getDataLocked();
|
||||
}
|
||||
|
||||
public function getFormFactory(): FormFactoryInterface
|
||||
{
|
||||
return $this->builder->getFormFactory();
|
||||
}
|
||||
|
||||
public function getAction(): string
|
||||
{
|
||||
return $this->builder->getAction();
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->builder->getMethod();
|
||||
}
|
||||
|
||||
public function getRequestHandler(): RequestHandlerInterface
|
||||
{
|
||||
return $this->builder->getRequestHandler();
|
||||
}
|
||||
|
||||
public function getAutoInitialize(): bool
|
||||
{
|
||||
return $this->builder->getAutoInitialize();
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->builder->getOptions();
|
||||
}
|
||||
|
||||
public function hasOption(string $name): bool
|
||||
{
|
||||
return $this->builder->hasOption($name);
|
||||
}
|
||||
|
||||
public function getOption(string $name, mixed $default = null): mixed
|
||||
{
|
||||
return $this->builder->getOption($name, $default);
|
||||
}
|
||||
|
||||
public function getIsEmptyCallback(): ?callable
|
||||
{
|
||||
return $this->builder->getIsEmptyCallback();
|
||||
}
|
||||
}
|
||||
20
sources/Forms/FormBuilder/FormBuilderException.php
Normal file
20
sources/Forms/FormBuilder/FormBuilderException.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormBuilder;
|
||||
|
||||
use Combodo\iTop\Forms\FormsException;
|
||||
|
||||
/**
|
||||
* Form builder exception.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormBuilder
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormBuilderException extends FormsException
|
||||
{
|
||||
}
|
||||
57
sources/Forms/FormBuilder/FormHelper.php
Normal file
57
sources/Forms/FormBuilder/FormHelper.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Forms\FormBuilder;
|
||||
|
||||
use Symfony\Component\Form\Event\PostSetDataEvent;
|
||||
use Symfony\Component\Form\Event\PostSubmitEvent;
|
||||
use Symfony\Component\Form\Event\PreSubmitEvent;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
/**
|
||||
* Form helper.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormBuilder
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormHelper
|
||||
{
|
||||
/**
|
||||
* Get the event type.
|
||||
*
|
||||
* @param FormEvent $event
|
||||
*
|
||||
* @return string
|
||||
* @throws FormBuilderException
|
||||
*/
|
||||
public static function GetEventType(FormEvent $event): string
|
||||
{
|
||||
if ($event instanceof PostSetDataEvent) {
|
||||
return FormEvents::POST_SET_DATA;
|
||||
} elseif ($event instanceof PostSubmitEvent) {
|
||||
return FormEvents::POST_SUBMIT;
|
||||
} elseif ($event instanceof PreSubmitEvent) {
|
||||
return FormEvents::PRE_SUBMIT;
|
||||
}
|
||||
|
||||
throw new FormBuilderException(sprintf('Unknown event type %s', get_class($event)));
|
||||
}
|
||||
|
||||
public static function CompareArrayValues($mValue1, $mValue2): int
|
||||
{
|
||||
if (is_array($mValue1) && is_array($mValue2)) {
|
||||
if (count($mValue1) !== count($mValue2)) {
|
||||
return 1;
|
||||
}
|
||||
$aDiff = array_udiff_assoc($mValue1, $mValue2, [FormHelper::class, 'CompareArrayValues']);
|
||||
|
||||
return count($aDiff);
|
||||
}
|
||||
|
||||
if ($mValue1 === $mValue2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
66
sources/Forms/FormBuilder/FormTypeExtension.php
Normal file
66
sources/Forms/FormBuilder/FormTypeExtension.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormBuilder;
|
||||
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Form type extension for common initialization.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormBuilder
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormTypeExtension extends AbstractTypeExtension
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public static function getExtendedTypes(): iterable
|
||||
{
|
||||
return [
|
||||
FormType::class,
|
||||
];
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefined([
|
||||
'form_block',
|
||||
'form_block_class',
|
||||
'builder_listener',
|
||||
'prevent_form_build',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
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 */
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
if (array_key_exists('form_block', $options)) {
|
||||
$view->vars['form_block'] = $options['form_block'];
|
||||
$view->vars['form_block_class'] = $options['form_block_class'];
|
||||
|
||||
$oFormBlock = $options['form_block'];
|
||||
$view->vars['trigger_form_submit_on_modify'] = $oFormBlock->IsImpactingBlocks();
|
||||
$view->vars['impacted_by'] = array_keys($oFormBlock->GetImpactedBlocks());
|
||||
}
|
||||
}
|
||||
}
|
||||
29
sources/Forms/FormBuilder/ResolvedFormType.php
Normal file
29
sources/Forms/FormBuilder/ResolvedFormType.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormBuilder;
|
||||
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\ResolvedFormType as SymfonyResolvedFormType;
|
||||
use Symfony\Component\Form\ResolvedFormTypeInterface;
|
||||
|
||||
/**
|
||||
* Symfony ResolvedFormType override to use our FormBuilder.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormBuilder
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ResolvedFormType extends SymfonyResolvedFormType implements ResolvedFormTypeInterface
|
||||
{
|
||||
protected function newBuilder(string $name, ?string $dataClass, FormFactoryInterface $factory, array $options): FormBuilderInterface
|
||||
{
|
||||
$builder = parent::newBuilder($name, $dataClass, $factory, $options);
|
||||
|
||||
return new FormBuilder($builder);
|
||||
}
|
||||
}
|
||||
26
sources/Forms/FormBuilder/ResolvedFormTypeFactory.php
Normal file
26
sources/Forms/FormBuilder/ResolvedFormTypeFactory.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormBuilder;
|
||||
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
use Symfony\Component\Form\ResolvedFormTypeFactoryInterface;
|
||||
use Symfony\Component\Form\ResolvedFormTypeInterface;
|
||||
|
||||
/**
|
||||
* Symfony ResolvedFormTypeFactory override to use our ResolvedFormType.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormBuilder
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ResolvedFormTypeFactory implements ResolvedFormTypeFactoryInterface
|
||||
{
|
||||
public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ?ResolvedFormTypeInterface $parent = null): ResolvedFormTypeInterface
|
||||
{
|
||||
return new ResolvedFormType($type, $typeExtensions, $parent);
|
||||
}
|
||||
}
|
||||
141
sources/Forms/FormType/Base/ChoiceFormType.php
Normal file
141
sources/Forms/FormType/Base/ChoiceFormType.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormType\Base;
|
||||
|
||||
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\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* ChoiceType form type.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormType\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ChoiceFormType extends AbstractType
|
||||
{
|
||||
private bool $bErrorAdded = false;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function getParent(): string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
// options to control the inline display of choices
|
||||
$resolver->setDefault('inline_display', true);
|
||||
$resolver->setDefault('max_items_selected', '');
|
||||
$resolver->setDefault('disable_auto_complete', true);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
parent::buildView($view, $form, $options);
|
||||
|
||||
// pass options to the view
|
||||
$view->vars['inline_display'] = $options['inline_display'];
|
||||
$view->vars['max_items_selected'] = $options['max_items_selected'];
|
||||
$view->vars['disable_auto_complete'] = $options['disable_auto_complete'];
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
// on preset data
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $oEvent) use ($options) {
|
||||
$this->InitializeValue($oEvent, $options);
|
||||
|
||||
// reset value if not in available choices
|
||||
if (!empty($oEvent->getData()) && !$this->CheckValue($oEvent->getData(), $options)) {
|
||||
if (!$this->bErrorAdded) {
|
||||
$oEvent->getForm()->addError(new FormError("The value has been reset because it is not part of the available choices anymore."));
|
||||
}
|
||||
$oEvent->setData(null);
|
||||
$this->bErrorAdded = true;
|
||||
}
|
||||
});
|
||||
|
||||
// on pre submit (prior)
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $oEvent) use ($options) {
|
||||
|
||||
// reset value if not in available choices
|
||||
if (!empty($oEvent->getData()) && !$this->CheckValue($oEvent->getData(), $options)) {
|
||||
$oEvent->getForm()->addError(new FormError("The value has been reset because it is not part of the available choices anymore."));
|
||||
$oEvent->setData(null);
|
||||
}
|
||||
|
||||
}, 1); // priority 1 to be executed before the default validation (priority 0)
|
||||
|
||||
// on pre submit
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (PreSubmitEvent $oEvent) use ($options) {
|
||||
$this->InitializeValue($oEvent, $options);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the value of the choice field.
|
||||
*
|
||||
* @param PreSetDataEvent|PreSubmitEvent $oEvent
|
||||
* @param array $options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function InitializeValue(PreSetDataEvent|PreSubmitEvent $oEvent, array $options): void
|
||||
{
|
||||
if ($options['multiple'] === false && $options['required'] === true) {
|
||||
if ($oEvent->getData() === null) {
|
||||
$oFirstElement = array_shift($options['choices']);
|
||||
if ($oFirstElement !== null) {
|
||||
$oEvent->setData(strval($oFirstElement));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the value(s) are part of the available choices.
|
||||
*
|
||||
* @param $oValue
|
||||
* @param $options
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function CheckValue($oValue, $options): bool
|
||||
{
|
||||
// Check multi selection values
|
||||
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'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
54
sources/Forms/FormType/Base/CollectionFormType.php
Normal file
54
sources/Forms/FormType/Base/CollectionFormType.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormType\Base;
|
||||
|
||||
use Dict;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Collection form type.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormType\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class CollectionFormType extends AbstractType
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public function getParent(): string
|
||||
{
|
||||
return CollectionType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'button_label' => Dict::S('UI:Links:Add:Button'),
|
||||
'allow_ordering' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
parent::buildView($view, $form, $options);
|
||||
if (\utils::IsNotNullOrEmptyString($options['button_label'])) {
|
||||
$view->vars['button_label'] = $options['button_label'];
|
||||
} else {
|
||||
$view->vars['button_label'] = Dict::S('UI:Links:Add:Button');
|
||||
}
|
||||
$view->vars['allow_ordering'] = $options['allow_ordering'];
|
||||
}
|
||||
|
||||
}
|
||||
63
sources/Forms/FormType/Base/FormType.php
Normal file
63
sources/Forms/FormType/Base/FormType.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormType\Base;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractTypeFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use Combodo\iTop\Forms\FormType\FormTypeHelper;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
|
||||
/**
|
||||
* Form type.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormType\Base
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormType extends AbstractType
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public function getParent(): string
|
||||
{
|
||||
return \Symfony\Component\Form\Extension\Core\Type\FormType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
parent::buildView($view, $form, $options);
|
||||
|
||||
/** @var FormBlock $oBlock */
|
||||
$oBlock = $options['form_block'];
|
||||
|
||||
$aData = [];
|
||||
foreach ($oBlock->GetChildren() as $oChild) {
|
||||
if (!$oChild instanceof AbstractTypeFormBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($oChild->IsAdded()) {
|
||||
$aData[] = [
|
||||
'name' => $oChild->GetName(),
|
||||
'added' => $oChild->IsAdded(),
|
||||
'id' => FormTypeHelper::GetFormId($form).'_'.$oChild->GetName(),
|
||||
];
|
||||
} else {
|
||||
$aData[] = [
|
||||
'name' => $oChild->GetName(),
|
||||
'added' => $oChild->IsAdded(),
|
||||
'id' => FormTypeHelper::GetFormId($form).'_'.$oChild->GetName(),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
$view->vars['blocks'] = $aData;
|
||||
}
|
||||
|
||||
}
|
||||
43
sources/Forms/FormType/DataModel/OqlFormType.php
Normal file
43
sources/Forms/FormType/DataModel/OqlFormType.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormType\DataModel;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* OQL expression form type.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormType\DataModel
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class OqlFormType extends AbstractType
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public function getParent(): string
|
||||
{
|
||||
return TextareaType::class;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefined('with_ai_button');
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
parent::buildView($view, $form, $options);
|
||||
|
||||
$view->vars['with_ai_button'] = $options['with_ai_button'];
|
||||
}
|
||||
}
|
||||
111
sources/Forms/FormType/FormTypeHelper.php
Normal file
111
sources/Forms/FormType/FormTypeHelper.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\FormType;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\FormBlock;
|
||||
use Combodo\iTop\Forms\FormBuilder\DependencyMap;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
/**
|
||||
* Form type helper.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\FormType
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormTypeHelper
|
||||
{
|
||||
/**
|
||||
* @param FormInterface $oForm
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function GetFormId(FormInterface $oForm): string
|
||||
{
|
||||
if (is_null($oForm->getParent())) {
|
||||
return $oForm->getName();
|
||||
}
|
||||
return self::GetFormId($oForm->getParent()).'_'.$oForm->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the blocks to redraw based on a turbo trigger.
|
||||
*
|
||||
* @param FormBlock $oFormBlock
|
||||
* @param FormInterface $oForm
|
||||
* @param string $sBlockTurboTriggerName
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function ComputeBlocksToRedraw(FormBlock $oFormBlock, FormInterface $oForm, string $sBlockTurboTriggerName): array
|
||||
{
|
||||
// Result
|
||||
$aBlocksToRedraw = [];
|
||||
|
||||
// Get the form corresponding to the turbo trigger
|
||||
$oFormTurboTrigger = self::GetFormAt($oForm, $sBlockTurboTriggerName);
|
||||
|
||||
// Get the parent form
|
||||
$oParent = $oFormTurboTrigger->getParent();
|
||||
$sParentName = self::GetFormId($oParent);
|
||||
|
||||
// Get the block corresponding to the turbo trigger form
|
||||
$oBlockTurboTrigger = $oFormTurboTrigger->getConfig()->getOption('form_block');
|
||||
$oMap = $oBlockTurboTrigger->GetParent()->GetDependenciesMap();
|
||||
|
||||
// Add impacted blocks
|
||||
$aImpacted = static::GetImpactedByRecursive($oMap, $oBlockTurboTrigger);
|
||||
foreach ($aImpacted as $oImpactedBlock) {
|
||||
$sName = $sParentName.'_'.$oImpactedBlock->GetName();
|
||||
if ($oParent->has($oImpactedBlock->GetName())) {
|
||||
$aBlocksToRedraw[$sName] = $oParent->get($oImpactedBlock->GetName())->createView();
|
||||
} else {
|
||||
$aBlocksToRedraw[$sName] = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [
|
||||
'blocks_to_redraw' => $aBlocksToRedraw,
|
||||
'current_block' => $oFormTurboTrigger->createView(),
|
||||
];
|
||||
}
|
||||
|
||||
private static function GetImpactedByRecursive(DependencyMap $oMap, AbstractFormBlock $oBLock): ?array
|
||||
{
|
||||
$aImpacted = $oMap->GetBlocksImpactedBy($oBLock->GetName());
|
||||
if ($aImpacted !== null) {
|
||||
foreach ($aImpacted as $oImpactedBlock) {
|
||||
$aRecursiveImpacted = static::GetImpactedByRecursive($oMap, $oImpactedBlock);
|
||||
if ($aRecursiveImpacted !== null) {
|
||||
$aImpacted = array_merge($aImpacted, $aRecursiveImpacted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $aImpacted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormInterface $oForm
|
||||
* @param string $sBlockTurboTriggerName
|
||||
*
|
||||
* @return FormInterface|null
|
||||
*/
|
||||
public static function GetFormAt(FormInterface $oForm, string $sBlockTurboTriggerName): ?FormInterface
|
||||
{
|
||||
if (preg_match_all('/\[(?<level>[^\[]+)\]/', $sBlockTurboTriggerName, $aMatches)) {
|
||||
foreach ($aMatches['level'] as $level) {
|
||||
$oForm = $oForm->Get($level);
|
||||
}
|
||||
}
|
||||
|
||||
return $oForm;
|
||||
}
|
||||
|
||||
}
|
||||
57
sources/Forms/Forms.php
Normal file
57
sources/Forms/Forms.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms;
|
||||
|
||||
use Combodo\iTop\Forms\FormBuilder\FormTypeExtension;
|
||||
use Combodo\iTop\Forms\FormBuilder\ResolvedFormTypeFactory;
|
||||
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
|
||||
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
|
||||
use Symfony\Component\Form\FormFactoryBuilder;
|
||||
use Symfony\Component\Form\FormFactoryBuilderInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Validator\Validation;
|
||||
|
||||
/**
|
||||
* Form.
|
||||
*
|
||||
* @package Combodo\iTop\Forms
|
||||
* @since 3.3.0
|
||||
*/
|
||||
final class Forms
|
||||
{
|
||||
/**
|
||||
* Creates a form factory with the iTop configuration.
|
||||
*/
|
||||
public static function createFormFactory(): FormFactoryInterface
|
||||
{
|
||||
return self::createFormFactoryBuilder()->getFormFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form factory builder with the iTop configuration.
|
||||
*/
|
||||
public static function createFormFactoryBuilder(): FormFactoryBuilderInterface
|
||||
{
|
||||
// Set up the Validator component
|
||||
$validator = Validation::createValidatorBuilder()
|
||||
->enableAttributeMapping()->getValidator();
|
||||
|
||||
return (new FormFactoryBuilder())
|
||||
->addExtension(new HttpFoundationExtension())
|
||||
->addExtension(new ValidatorExtension($validator))
|
||||
->addTypeExtension(new FormTypeExtension())
|
||||
->setResolvedTypeFactory(new ResolvedFormTypeFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* This class cannot be instantiated.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
27
sources/Forms/FormsException.php
Normal file
27
sources/Forms/FormsException.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms;
|
||||
|
||||
use Exception;
|
||||
use IssueLog;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Form exception.
|
||||
*
|
||||
* @package Combodo\iTop\Forms
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormsException extends Exception
|
||||
{
|
||||
public function __construct(string $sMessage = '', int $iCode = 0, ?Throwable $oPrevious = null, array $aContext = [])
|
||||
{
|
||||
parent::__construct($sMessage, $iCode, $oPrevious);
|
||||
IssueLog::Exception(get_class($this).' occurs: '.$sMessage, $this, null, $aContext);
|
||||
}
|
||||
}
|
||||
315
sources/Forms/IO/AbstractFormIO.php
Normal file
315
sources/Forms/IO/AbstractFormIO.php
Normal file
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractFormBlock;
|
||||
use Combodo\iTop\Forms\IO\Format\AbstractIOFormat;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
|
||||
/**
|
||||
* Abstract form IO.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class AbstractFormIO
|
||||
{
|
||||
public const EVENT_POST_SET_DATA = FormEvents::POST_SET_DATA;
|
||||
public const EVENT_POST_SUBMIT = FormEvents::POST_SUBMIT;
|
||||
public const EVENT_FORM_STATIC = 'form.static';
|
||||
|
||||
/** @var AbstractFormBlock The owner block */
|
||||
private AbstractFormBlock $oOwnerBlock;
|
||||
|
||||
/** @var string Name of the IO */
|
||||
private string $sName;
|
||||
|
||||
/** @var string Type of the IO data */
|
||||
private string $sType;
|
||||
|
||||
/** @var bool array */
|
||||
private bool $bIsArray;
|
||||
|
||||
/** @var array Stored values */
|
||||
private array $aValues = [];
|
||||
|
||||
/** @var FormBinding|null */
|
||||
private FormBinding|null $oBinding = null;
|
||||
|
||||
/** @var array bindings pointing to other inputs */
|
||||
protected array $aBindingsToInputs = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $sName name of the IO
|
||||
* @param string $sType type of the IO
|
||||
* @param bool $bIsArray indicates if the IO is an array
|
||||
*
|
||||
* @throws FormBlockIOException
|
||||
*/
|
||||
public function __construct(string $sName, string $sType, bool $bIsArray = false)
|
||||
{
|
||||
if (!is_a($sType, AbstractIOFormat::class, true)) {
|
||||
throw new FormBlockIOException('invalid form format type '.json_encode($sType).' given');
|
||||
}
|
||||
$this->sType = $sType;
|
||||
$this->bIsArray = $bIsArray;
|
||||
$this->SetName($sName);
|
||||
}
|
||||
|
||||
public function SetOwnerBlock(AbstractFormBlock $oOwnerBlock): void
|
||||
{
|
||||
$this->oOwnerBlock = $oOwnerBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the owner block.
|
||||
*
|
||||
* @return AbstractFormBlock
|
||||
*/
|
||||
public function GetOwnerBlock(): AbstractFormBlock
|
||||
{
|
||||
return $this->oOwnerBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IO name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function GetName(): string
|
||||
{
|
||||
return $this->sName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the IO name.
|
||||
*
|
||||
* @param string $sName
|
||||
*
|
||||
* @return self
|
||||
* @throws FormBlockIOException
|
||||
*/
|
||||
public function SetName(string $sName): self
|
||||
{
|
||||
// Check name validity
|
||||
if (preg_match('/^(?<name>((\w+\.\w+)|\w+))$/', $sName, $aMatches)) {
|
||||
$sParsedName = $aMatches['name'];
|
||||
if ($sParsedName !== $sName) {
|
||||
$sName = json_encode($sName);
|
||||
$sParsedName = json_encode($sParsedName);
|
||||
// $sBlockName = json_encode($this->GetOwnerBlock()->GetName());
|
||||
throw new FormBlockIOException("Input $sName does not match $sParsedName for block.");
|
||||
}
|
||||
} else {
|
||||
$sName = json_encode($sName);
|
||||
// $sBlockName = json_encode($this->GetOwnerBlock()->GetName());
|
||||
throw new FormBlockIOException("Input $sName is not valid for block.");
|
||||
}
|
||||
|
||||
// Name is valid
|
||||
$this->sName = $sName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IO data type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function GetDataType(): string
|
||||
{
|
||||
return $this->sType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if is array.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsArray(): bool
|
||||
{
|
||||
return $this->bIsArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the IO value.
|
||||
*
|
||||
* @param string $sEventType
|
||||
* @param mixed $oValue
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function SetValue(string $sEventType, mixed $oValue): self
|
||||
{
|
||||
$this->aValues[$sEventType] = $oValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IO value.
|
||||
*
|
||||
* @param string|null $sEventType
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function GetValue(string $sEventType = null): mixed
|
||||
{
|
||||
if ($sEventType === null) {
|
||||
return $this->Value();
|
||||
}
|
||||
|
||||
return $this->aValues[$sEventType] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if value exist.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function HasValue(): bool
|
||||
{
|
||||
return $this->HasEventValue(FormEvents::POST_SET_DATA) || $this->HasEventValue(FormEvents::POST_SUBMIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if value exist.
|
||||
*
|
||||
* @param string|null $sEventType
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function HasEventValue(string $sEventType = null): bool
|
||||
{
|
||||
if ($sEventType === null) {
|
||||
return $this->HasValue();
|
||||
}
|
||||
|
||||
return array_key_exists($sEventType, $this->aValues) && $this->aValues[$sEventType] !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetValues(): array
|
||||
{
|
||||
return $this->aValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the IO values.
|
||||
*
|
||||
* @param array $aValues
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function SetValues(array $aValues): self
|
||||
{
|
||||
$this->aValues = $aValues;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most relevant value.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function Value(): mixed
|
||||
{
|
||||
if (array_key_exists('form.static', $this->aValues)) {
|
||||
return $this->aValues['form.static'];
|
||||
}
|
||||
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)) {
|
||||
return $this->aValues[FormEvents::POST_SET_DATA];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to input.
|
||||
*
|
||||
* @param FormInput $oDestinationIO
|
||||
*
|
||||
* @return FormBinding
|
||||
* @throws FormBlockIOException
|
||||
*/
|
||||
public function BindToInput(FormInput $oDestinationIO): FormBinding
|
||||
{
|
||||
$oBinding = new FormBinding($this, $oDestinationIO);
|
||||
$this->aBindingsToInputs[] = $oBinding;
|
||||
|
||||
return $oBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a binding.
|
||||
*
|
||||
* @param FormBinding $oFormBinding
|
||||
*
|
||||
* @return void
|
||||
* @throws FormBlockIOException when already bound
|
||||
*/
|
||||
public function Attach(FormBinding $oFormBinding): void
|
||||
{
|
||||
if ($this->IsBound()) {
|
||||
throw new FormBlockIOException("Can't attach ".json_encode($oFormBinding->oSourceIO->GetName())." to ".json_encode($this->GetName()).", already bound to ".json_encode($this->oBinding->oSourceIO->GetName()));
|
||||
}
|
||||
$this->oBinding = $oFormBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate IO is bound.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsBound(): bool
|
||||
{
|
||||
return $this->oBinding !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the binding.
|
||||
*
|
||||
* @return FormBinding|null
|
||||
*/
|
||||
public function GetBinding(): ?FormBinding
|
||||
{
|
||||
return $this->oBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicated inputs data is ready.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsDataReady(): bool
|
||||
{
|
||||
return $this->HasValue();
|
||||
}
|
||||
|
||||
public function HasBindingOut(): bool
|
||||
{
|
||||
return count($this->aBindingsToInputs) > 0;
|
||||
}
|
||||
|
||||
public function GetBindingsToInputs(): array
|
||||
{
|
||||
return $this->aBindingsToInputs;
|
||||
}
|
||||
|
||||
}
|
||||
26
sources/Forms/IO/Converter/AbstractConverter.php
Normal file
26
sources/Forms/IO/Converter/AbstractConverter.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Converter;
|
||||
|
||||
/**
|
||||
* Abstract converter.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Converter
|
||||
* @since 3.3.0
|
||||
*/
|
||||
abstract class AbstractConverter
|
||||
{
|
||||
/**
|
||||
* Convert the date to output format.
|
||||
*
|
||||
* @param mixed $oData
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function Convert(mixed $oData): mixed;
|
||||
}
|
||||
43
sources/Forms/IO/Converter/ChoiceValueToLabelConverter.php
Normal file
43
sources/Forms/IO/Converter/ChoiceValueToLabelConverter.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Converter;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
|
||||
use Combodo\iTop\Forms\IO\Format\AttributeIOFormat;
|
||||
use Combodo\iTop\Forms\IO\Format\StringIOFormat;
|
||||
|
||||
/**
|
||||
* Convert a choice value to its label.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Converter
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ChoiceValueToLabelConverter extends AbstractConverter
|
||||
{
|
||||
private ChoiceFormBlock $oChoiceBlock;
|
||||
|
||||
public function __construct(ChoiceFormBlock $oChoiceBlock)
|
||||
{
|
||||
$this->oChoiceBlock = $oChoiceBlock;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function Convert(mixed $oData): ?StringIOFormat
|
||||
{
|
||||
if (is_null($oData) || is_array($oData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$aOptions = array_flip($this->oChoiceBlock->GetOption('choices'));
|
||||
if (!array_key_exists($oData, $aOptions) || is_null($aOptions[$oData])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StringIOFormat($aOptions[$oData]);
|
||||
}
|
||||
}
|
||||
29
sources/Forms/IO/Converter/CollectionToCountConverter.php
Normal file
29
sources/Forms/IO/Converter/CollectionToCountConverter.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Converter;
|
||||
|
||||
use Combodo\iTop\Forms\IO\Format\IntegerIOFormat;
|
||||
|
||||
/**
|
||||
* Count elements in a collection.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Converter
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class CollectionToCountConverter extends AbstractConverter
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public function Convert(mixed $oData): ?IntegerIOFormat
|
||||
{
|
||||
if ($oData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new IntegerIOFormat(count($oData));
|
||||
}
|
||||
}
|
||||
44
sources/Forms/IO/Converter/OqlToClassConverter.php
Normal file
44
sources/Forms/IO/Converter/OqlToClassConverter.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Converter;
|
||||
|
||||
use Combodo\iTop\Service\DependencyInjection\DIException;
|
||||
use Combodo\iTop\Service\DependencyInjection\DIService;
|
||||
use Combodo\iTop\Forms\IO\Format\ClassIOFormat;
|
||||
use Combodo\iTop\Forms\IO\FormBlockIOException;
|
||||
use Exception;
|
||||
use ModelReflection;
|
||||
|
||||
/**
|
||||
* Extract the selected class from an OQL query.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Converter
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class OqlToClassConverter extends AbstractConverter
|
||||
{
|
||||
/** @inheritdoc
|
||||
* @throws DIException
|
||||
* @throws FormBlockIOException
|
||||
*/
|
||||
public function Convert(mixed $oData): ?ClassIOFormat
|
||||
{
|
||||
if ($oData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var ModelReflection $oModelReflection */
|
||||
$oModelReflection = DIService::GetInstance()->GetService('ModelReflection');
|
||||
try {
|
||||
$oQuery = $oModelReflection->GetQuery($oData);
|
||||
} catch (Exception $e) {
|
||||
throw new FormBlockIOException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
return new ClassIOFormat($oQuery->GetClass());
|
||||
}
|
||||
}
|
||||
50
sources/Forms/IO/FormBinding.php
Normal file
50
sources/Forms/IO/FormBinding.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO;
|
||||
|
||||
/**
|
||||
* Abstract form binding.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormBinding
|
||||
{
|
||||
public readonly AbstractFormIO $oSourceIO;
|
||||
public readonly AbstractFormIO $oDestinationIO;
|
||||
|
||||
/**
|
||||
* @param AbstractFormIO $oSourceIO
|
||||
* @param AbstractFormIO $oDestinationIO
|
||||
*
|
||||
* @throws \Combodo\iTop\Forms\IO\FormBlockIOException
|
||||
*/
|
||||
public function __construct(AbstractFormIO $oSourceIO, AbstractFormIO $oDestinationIO)
|
||||
{
|
||||
// Check IOFormat validity
|
||||
$sSourceDataType = $oSourceIO->GetDataType();
|
||||
$sDestinationDataType = $oDestinationIO->GetDataType();
|
||||
if ($sSourceDataType !== $sDestinationDataType) {
|
||||
throw new FormBlockIOException('binding '.json_encode($sSourceDataType).' to '.json_encode($sDestinationDataType).' is not supported');
|
||||
}
|
||||
$this->oDestinationIO = $oDestinationIO;
|
||||
$this->oSourceIO = $oSourceIO;
|
||||
$oDestinationIO->Attach($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate binding values.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function PropagateValues(): void
|
||||
{
|
||||
$this->oDestinationIO->SetValues($this->oSourceIO->GetValues());
|
||||
$this->oDestinationIO->GetOwnerBlock()->BindingReceivedEvent($this->oDestinationIO);
|
||||
}
|
||||
}
|
||||
20
sources/Forms/IO/FormBlockIOException.php
Normal file
20
sources/Forms/IO/FormBlockIOException.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO;
|
||||
|
||||
use Combodo\iTop\Forms\FormsException;
|
||||
|
||||
/**
|
||||
* Form block IO exception.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormBlockIOException extends FormsException
|
||||
{
|
||||
}
|
||||
64
sources/Forms/IO/FormInput.php
Normal file
64
sources/Forms/IO/FormInput.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO;
|
||||
|
||||
/**
|
||||
* Form input IO.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormInput extends AbstractFormIO
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function IsDataReady(): bool
|
||||
{
|
||||
return $this->HasValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $sEventType
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsEventDataReady(string $sEventType = null): bool
|
||||
{
|
||||
return $this->HasEventValue($sEventType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values of the input.
|
||||
*
|
||||
* @param array $aValues
|
||||
*
|
||||
* @return AbstractFormIO
|
||||
*/
|
||||
public function SetValues(array $aValues): AbstractFormIO
|
||||
{
|
||||
parent::SetValues($aValues);
|
||||
|
||||
$this->PropagateBindingsValues();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate the bindings values.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function PropagateBindingsValues(): void
|
||||
{
|
||||
// propagate the value
|
||||
foreach ($this->aBindingsToInputs as $oBinding) {
|
||||
$oBinding->PropagateValues();
|
||||
}
|
||||
}
|
||||
}
|
||||
164
sources/Forms/IO/FormOutput.php
Normal file
164
sources/Forms/IO/FormOutput.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO;
|
||||
|
||||
use Combodo\iTop\Forms\IO\Converter\AbstractConverter;
|
||||
use IssueLog;
|
||||
|
||||
/**
|
||||
* Form output IO.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormOutput extends AbstractFormIO
|
||||
{
|
||||
/** @var AbstractConverter|null */
|
||||
private null|AbstractConverter $oConverter;
|
||||
|
||||
/** @var array */
|
||||
private array $aBindingsToOutputs = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $sName
|
||||
* @param string $sType
|
||||
* @param bool $bIsArray
|
||||
* @param AbstractConverter|null $oConverter
|
||||
*
|
||||
* @throws FormBlockIOException
|
||||
*/
|
||||
public function __construct(string $sName, string $sType, bool $bIsArray = false, AbstractConverter $oConverter = null)
|
||||
{
|
||||
parent::__construct($sName, $sType, $bIsArray);
|
||||
$this->oConverter = $oConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the value.
|
||||
*
|
||||
* @param mixed $oData
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function ConvertValue(mixed $oData): mixed
|
||||
{
|
||||
if ($this->IsArray()) {
|
||||
return $this->ConvertArrayValue($oData);
|
||||
} else {
|
||||
return $this->ConvertSingleValue($oData);
|
||||
}
|
||||
}
|
||||
|
||||
private function ConvertArrayValue(array $aData): array
|
||||
{
|
||||
return array_map(function ($v) {
|
||||
return $this->ConvertSingleValue($v);
|
||||
}, $aData);
|
||||
}
|
||||
|
||||
private function ConvertSingleValue(mixed $oData): mixed
|
||||
{
|
||||
if (is_null($this->oConverter)) {
|
||||
$sType = $this->GetDataType();
|
||||
return $oData !== null ? new $sType($oData) : null;
|
||||
}
|
||||
|
||||
return $this->oConverter->Convert($oData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the value.
|
||||
*
|
||||
* @param string $sEventType
|
||||
* @param mixed $oData
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ComputeValue(string $sEventType, mixed $oData): void
|
||||
{
|
||||
$this->SetValue($sEventType, $this->ConvertValue($oData));
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate the bindings values.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function PropagateBindingsValues(): void
|
||||
{
|
||||
// propagate the value
|
||||
foreach ($this->aBindingsToInputs as $oBinding) {
|
||||
$oBinding->PropagateValues();
|
||||
}
|
||||
|
||||
// propagate the value
|
||||
foreach ($this->aBindingsToOutputs as $oBinding) {
|
||||
$oBinding->PropagateValues();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to output.
|
||||
*
|
||||
* @param FormOutput $oDestinationIO
|
||||
*
|
||||
* @return FormBinding
|
||||
* @throws FormBlockIOException
|
||||
*/
|
||||
public function BindToOutput(FormOutput $oDestinationIO): FormBinding
|
||||
{
|
||||
$oBinding = new FormBinding($this, $oDestinationIO);
|
||||
$this->aBindingsToOutputs[] = $oBinding;
|
||||
|
||||
return $oBinding;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetBindingsToOutputs(): array
|
||||
{
|
||||
return $this->aBindingsToOutputs;
|
||||
}
|
||||
|
||||
public function HasBindingOut(): bool
|
||||
{
|
||||
if (parent::HasBindingOut()) {
|
||||
return true; // has bindings to inputs
|
||||
}
|
||||
|
||||
return count($this->aBindingsToOutputs) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bindings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetBindings(): array
|
||||
{
|
||||
return $this->aBindingsToInputs;
|
||||
}
|
||||
|
||||
public function HasBindings(): bool
|
||||
{
|
||||
return count($this->aBindingsToInputs) > 0;
|
||||
}
|
||||
|
||||
public function SetValue(string $sEventType, mixed $oValue): AbstractFormIO
|
||||
{
|
||||
parent::SetValue($sEventType, $oValue);
|
||||
|
||||
// propagate the bindings values
|
||||
$this->PropagateBindingsValues();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
21
sources/Forms/IO/Format/AbstractIOFormat.php
Normal file
21
sources/Forms/IO/Format/AbstractIOFormat.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Format;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* Abstract IO format.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Format
|
||||
* @since 3.3.0
|
||||
*/
|
||||
abstract class AbstractIOFormat implements JsonSerializable
|
||||
{
|
||||
abstract public function jsonSerialize(): mixed;
|
||||
}
|
||||
30
sources/Forms/IO/Format/AttributeIOFormat.php
Normal file
30
sources/Forms/IO/Format/AttributeIOFormat.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Format;
|
||||
|
||||
/**
|
||||
* Attribute IO format.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Format
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class AttributeIOFormat extends AbstractIOFormat
|
||||
{
|
||||
public string $sAttributeName;
|
||||
|
||||
public function __construct(string $sAttributeName)
|
||||
{
|
||||
$this->sAttributeName = $sAttributeName;
|
||||
// validation du format sinon exception
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->sAttributeName;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->sAttributeName;
|
||||
}
|
||||
}
|
||||
34
sources/Forms/IO/Format/AttributeTypeArrayIOFormat.php
Normal file
34
sources/Forms/IO/Format/AttributeTypeArrayIOFormat.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Format;
|
||||
|
||||
use Combodo\iTop\Service\DependencyInjection\DIService;
|
||||
use Combodo\iTop\Forms\IO\FormBlockIOException;
|
||||
|
||||
/**
|
||||
* Attribute type array IO format.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Format
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class AttributeTypeArrayIOFormat extends AbstractIOFormat
|
||||
{
|
||||
public array $aClasses;
|
||||
|
||||
/**
|
||||
*/
|
||||
public function __construct(array $aClasses)
|
||||
{
|
||||
$this->aClasses = $aClasses;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return json_encode($this->aClasses);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return json_encode($this->aClasses);
|
||||
}
|
||||
}
|
||||
29
sources/Forms/IO/Format/AttributeTypeIOFormat.php
Normal file
29
sources/Forms/IO/Format/AttributeTypeIOFormat.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Format;
|
||||
|
||||
/**
|
||||
* Attribute type IO format.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Format
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class AttributeTypeIOFormat extends AbstractIOFormat
|
||||
{
|
||||
public string $sAttributeType;
|
||||
|
||||
public function __construct(string $sAttributeType)
|
||||
{
|
||||
$this->sAttributeType = $sAttributeType;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->sAttributeType;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->sAttributeType;
|
||||
}
|
||||
}
|
||||
39
sources/Forms/IO/Format/BooleanIOFormat.php
Normal file
39
sources/Forms/IO/Format/BooleanIOFormat.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Format;
|
||||
|
||||
/**
|
||||
* Boolean IO format.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Format
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class BooleanIOFormat extends AbstractIOFormat
|
||||
{
|
||||
public bool $bValue;
|
||||
|
||||
public function __construct(bool $bValue)
|
||||
{
|
||||
$this->bValue = $bValue;
|
||||
}
|
||||
|
||||
public function IsTrue(): bool
|
||||
{
|
||||
return $this->bValue;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->bValue ? 'true' : 'false';
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->bValue;
|
||||
}
|
||||
}
|
||||
42
sources/Forms/IO/Format/ClassIOFormat.php
Normal file
42
sources/Forms/IO/Format/ClassIOFormat.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Format;
|
||||
|
||||
use Combodo\iTop\Service\DependencyInjection\DIService;
|
||||
use Combodo\iTop\Forms\IO\FormBlockIOException;
|
||||
|
||||
/**
|
||||
* Class IO format.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Format
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ClassIOFormat extends AbstractIOFormat
|
||||
{
|
||||
public string $sClassName;
|
||||
|
||||
/**
|
||||
* @throws \Combodo\iTop\Service\DependencyInjection\DIException
|
||||
* @throws \Combodo\iTop\Forms\IO\FormBlockIOException
|
||||
*/
|
||||
public function __construct(string $sClassName)
|
||||
{
|
||||
// Check class validity
|
||||
/** @var \ModelReflection $oModelReflection */
|
||||
$oModelReflection = DIService::GetInstance()->GetService('ModelReflection');
|
||||
if (!$oModelReflection->IsValidClass($sClassName)) {
|
||||
throw new FormBlockIOException("Class ".json_encode($sClassName)." is not valid");
|
||||
}
|
||||
$this->sClassName = $sClassName;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->sClassName;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->sClassName;
|
||||
}
|
||||
}
|
||||
30
sources/Forms/IO/Format/IntegerIOFormat.php
Normal file
30
sources/Forms/IO/Format/IntegerIOFormat.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Format;
|
||||
|
||||
use Combodo\iTop\Forms\IO\Format\AbstractIOFormat;
|
||||
|
||||
class IntegerIOFormat extends AbstractIOFormat
|
||||
{
|
||||
public int $oValue;
|
||||
|
||||
public function __construct(string $oValue)
|
||||
{
|
||||
$this->oValue = intval($oValue);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return strval($this->oValue);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return strval($this->oValue);
|
||||
}
|
||||
}
|
||||
29
sources/Forms/IO/Format/NumberIOFormat.php
Normal file
29
sources/Forms/IO/Format/NumberIOFormat.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Format;
|
||||
|
||||
/**
|
||||
* Number IO format.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Format
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class NumberIOFormat extends AbstractIOFormat
|
||||
{
|
||||
public mixed $oValue;
|
||||
|
||||
public function __construct(string $oValue)
|
||||
{
|
||||
$this->oValue = $oValue;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return strval($this->oValue);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return strval($this->oValue);
|
||||
}
|
||||
}
|
||||
37
sources/Forms/IO/Format/StringIOFormat.php
Normal file
37
sources/Forms/IO/Format/StringIOFormat.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\IO\Format;
|
||||
|
||||
/**
|
||||
* String IO format.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\IO\Format
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class StringIOFormat extends AbstractIOFormat
|
||||
{
|
||||
public string $sValue;
|
||||
|
||||
/**
|
||||
* @param string $sValue
|
||||
*/
|
||||
public function __construct(string $sValue)
|
||||
{
|
||||
$this->sValue = $sValue;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->sValue;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->sValue;
|
||||
}
|
||||
}
|
||||
404
sources/Forms/Register/IORegister.php
Normal file
404
sources/Forms/Register/IORegister.php
Normal file
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Register;
|
||||
|
||||
use Combodo\iTop\Forms\Block\AbstractFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Expression\AbstractExpressionFormBlock;
|
||||
use Combodo\iTop\Forms\Block\FormBlockException;
|
||||
use Combodo\iTop\Forms\Block\FormBlockHelper;
|
||||
use Combodo\iTop\Forms\IO\Converter\AbstractConverter;
|
||||
use Combodo\iTop\Forms\IO\FormBlockIOException;
|
||||
use Combodo\iTop\Forms\IO\FormInput;
|
||||
use Combodo\iTop\Forms\IO\FormOutput;
|
||||
|
||||
/**
|
||||
* IO register.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Register
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class IORegister
|
||||
{
|
||||
/** @var array */
|
||||
private array $aInputs = [];
|
||||
|
||||
/** @var array */
|
||||
private array $aOutputs = [];
|
||||
|
||||
/**
|
||||
* @param AbstractFormBlock $oFormBlock
|
||||
*/
|
||||
public function __construct(private readonly AbstractFormBlock $oFormBlock)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sName
|
||||
* @param string $sType
|
||||
* @param bool $bIsArray
|
||||
*
|
||||
* @return $this
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function AddInput(string $sName, string $sType, bool $bIsArray = false): self
|
||||
{
|
||||
$oFormInput = new FormInput($sName, $sType, $bIsArray);
|
||||
$oFormInput->SetOwnerBlock($this->oFormBlock);
|
||||
if (array_key_exists($oFormInput->GetName(), $this->aInputs)) {
|
||||
throw new RegisterException('Input already exists '.json_encode($oFormInput->GetName()).' for '.json_encode($this->oFormBlock->GetName()));
|
||||
}
|
||||
$this->aInputs[$oFormInput->GetName()] = $oFormInput;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an input connected to another block.
|
||||
*
|
||||
* @param string $sName the input name
|
||||
* @param string $sOutputBlockName
|
||||
* @param string $sOutputName
|
||||
*
|
||||
* @return $this
|
||||
* @throws FormBlockException
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function AddInputDependsOn(string $sName, string $sOutputBlockName, string $sOutputName): self
|
||||
{
|
||||
$oOutputBlock = $this->oFormBlock->GetParent()->Get($sOutputBlockName);
|
||||
$oBlockOutput = $oOutputBlock->GetOutput($sOutputName);
|
||||
|
||||
$this->AddInput($sName, $oBlockOutput->GetDataType(), $oBlockOutput->IsArray());
|
||||
$this->InputDependsOn($sName, $sOutputBlockName, $sOutputName);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an input to a block output.
|
||||
*
|
||||
* @param string $sInputName the input name
|
||||
* @param string $sOutputBlockName the dependency block name
|
||||
* @param string $sOutputName the dependency output name
|
||||
*
|
||||
* @return $this
|
||||
* @throws FormBlockException
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function InputDependsOn(string $sInputName, string $sOutputBlockName, string $sOutputName): self
|
||||
{
|
||||
$oOutputBlock = $this->oFormBlock->GetParent()?->Get($sOutputBlockName);
|
||||
if (is_null($oOutputBlock)) {
|
||||
throw new RegisterException('Output block not found '.json_encode($sOutputBlockName));
|
||||
}
|
||||
$oFormInput = $this->GetInput($sInputName);
|
||||
$oFormOutput = $oOutputBlock->GetOutput($sOutputName);
|
||||
$oFormOutput->BindToInput($oFormInput);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an output to a parent block output.
|
||||
*
|
||||
* @param string $sOutputName output name
|
||||
* @param string $sParentOutputName parent output name
|
||||
*
|
||||
* @return $this
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function OutputImpactParent(string $sOutputName, string $sParentOutputName): self
|
||||
{
|
||||
$oFormOutput = $this->GetOutput($sOutputName);
|
||||
$oParentFormOutput = $this->oFormBlock->GetParent()->GetOutput($sParentOutputName);
|
||||
$oFormOutput->BindToOutput($oParentFormOutput);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sName
|
||||
* @param string $sType
|
||||
* @param bool $bIsArray
|
||||
* @param AbstractConverter|null $oConverter
|
||||
*
|
||||
* @return void
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function AddOutput(string $sName, string $sType, bool $bIsArray = false, AbstractConverter $oConverter = null): void
|
||||
{
|
||||
$oFormOutput = new FormOutput($sName, $sType, $bIsArray, $oConverter);
|
||||
$oFormOutput->SetOwnerBlock($this->oFormBlock);
|
||||
if (array_key_exists($oFormOutput->GetName(), $this->aOutputs)) {
|
||||
throw new RegisterException('Output already exists '.json_encode($oFormOutput->GetName()).' for '.json_encode($this->oFormBlock->GetName()).' in block '.FormBlockHelper::GetFormId($this->oFormBlock).' of class '.get_class($this->oFormBlock));
|
||||
}
|
||||
$this->aOutputs[$oFormOutput->GetName()] = $oFormOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an input.
|
||||
*
|
||||
* @param string $sName
|
||||
*
|
||||
* @return FormInput
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function GetInput(string $sName): FormInput
|
||||
{
|
||||
if (!$this->HasInput($sName)) {
|
||||
throw new RegisterException('Missing input '.json_encode($sName).' for '.json_encode($this->oFormBlock->GetName()));
|
||||
}
|
||||
|
||||
return $this->aInputs[$sName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test input existence.
|
||||
*
|
||||
* @param string $sName
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function HasInput(string $sName): bool
|
||||
{
|
||||
return array_key_exists($sName, $this->aInputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetInputs(): array
|
||||
{
|
||||
return $this->aInputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetBoundInputs(): array
|
||||
{
|
||||
$aInputs = [];
|
||||
|
||||
/** @var FormInput $oFormInput */
|
||||
foreach ($this->aInputs as $oFormInput) {
|
||||
if ($oFormInput->IsBound() || $oFormInput->HasBindingOut()) {
|
||||
$aInputs[] = $oFormInput;
|
||||
}
|
||||
}
|
||||
|
||||
return $aInputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetBoundOutputs(): array
|
||||
{
|
||||
$aOutputs = [];
|
||||
|
||||
/** @var FormOutput $oFormOutput */
|
||||
foreach ($this->aOutputs as $oFormOutput) {
|
||||
if ($oFormOutput->IsBound() || $oFormOutput->HasBindingOut()) {
|
||||
$aOutputs[] = $oFormOutput;
|
||||
}
|
||||
}
|
||||
|
||||
return $aOutputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an output.
|
||||
*
|
||||
* @param string $sName output name
|
||||
*
|
||||
* @return FormOutput
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function GetOutput(string $sName): FormOutput
|
||||
{
|
||||
if (!array_key_exists($sName, $this->aOutputs)) {
|
||||
throw new RegisterException('Missing output '.json_encode($sName).' for '.json_encode($this->oFormBlock->GetName()));
|
||||
}
|
||||
|
||||
return $this->aOutputs[$sName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetOutputs(): array
|
||||
{
|
||||
return $this->aOutputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check existence of one or more dependencies.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function HasDependenciesBlocks(): bool
|
||||
{
|
||||
foreach ($this->aInputs as $oFormInput) {
|
||||
if ($oFormInput->IsBound()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check existence of one or more dependents blocks.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsImpactingBlocks(): bool
|
||||
{
|
||||
/** @var FormOutput $oFormOutput */
|
||||
foreach ($this->aOutputs as $oFormOutput) {
|
||||
if (count($oFormOutput->GetBindings()) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dependencies blocks.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetImpactedBlocks(): array
|
||||
{
|
||||
$aBlocks = [];
|
||||
|
||||
/** @var FormInput $oFormInput */
|
||||
foreach ($this->aInputs as $oFormInput) {
|
||||
if ($oFormInput->IsBound()) {
|
||||
$oBlock = $oFormInput->GetBinding()->oSourceIO->GetOwnerBlock();
|
||||
|
||||
if ($oBlock instanceof AbstractExpressionFormBlock) {
|
||||
foreach ($oBlock->GetBoundInputs() as $oExpressionFormInput) {
|
||||
$oBlock = $oExpressionFormInput->GetBinding()->oSourceIO->GetOwnerBlock();
|
||||
$sId = FormBlockHelper::GetFormId($oBlock);
|
||||
$aBlocks[$sId] = $oBlock;
|
||||
}
|
||||
}
|
||||
|
||||
$sId = FormBlockHelper::GetFormId($oBlock);
|
||||
$aBlocks[$sId] = $oBlock;
|
||||
}
|
||||
}
|
||||
|
||||
return $aBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bound inputs bindings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetBoundInputsBindings(): array
|
||||
{
|
||||
$aBindings = [];
|
||||
|
||||
/** @var FormInput $oFormInput */
|
||||
foreach ($this->aInputs as $oFormInput) {
|
||||
if ($oFormInput->IsBound()) {
|
||||
$aBindings[$oFormInput->GetName()] = $oFormInput->GetBinding();
|
||||
}
|
||||
}
|
||||
|
||||
return $aBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bound outputs bindings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetBoundOutputBindings(): array
|
||||
{
|
||||
$aBindings = [];
|
||||
|
||||
/** @var FormInput $oFormInput */
|
||||
foreach ($this->aOutputs as $oFormOutput) {
|
||||
if ($oFormOutput->IsBound()) {
|
||||
$aBindings[$oFormOutput->GetName()] = $oFormOutput->GetBinding();
|
||||
}
|
||||
}
|
||||
|
||||
return $aBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inputs data ready.
|
||||
*
|
||||
* @param string|null $sType
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsInputsDataReady(string $sType = null): bool
|
||||
{
|
||||
foreach ($this->aInputs as $oFormInput) {
|
||||
if ($oFormInput->IsBound()) {
|
||||
if (!$oFormInput->IsEventDataReady($sType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute outputs values.
|
||||
*
|
||||
* @param string $sEventType
|
||||
* @param mixed $oData
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ComputeOutputs(string $sEventType, mixed $oData): void
|
||||
{
|
||||
/** Iterate throw output @var FormOutput $oFormOutput */
|
||||
foreach ($this->aOutputs as $oFormOutput) {
|
||||
|
||||
// Compute the output value
|
||||
$oFormOutput->ComputeValue($sEventType, $oData);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an input to a parent block input.
|
||||
*
|
||||
* @param string $sInputName input name
|
||||
* @param string $sParentInputName parent input name
|
||||
*
|
||||
* @return $this
|
||||
* @throws FormBlockException
|
||||
* @throws FormBlockIOException
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function InputDependsOnParent(string $sInputName, string $sParentInputName): self
|
||||
{
|
||||
$oFormInput = $this->GetInput($sInputName);
|
||||
$oParentFormInput = $this->oFormBlock->GetParent()->GetInput($sParentInputName);
|
||||
$oParentFormInput->BindToInput($oFormInput);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
16
sources/Forms/Register/Option.php
Normal file
16
sources/Forms/Register/Option.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Combodo\iTop\Forms\Register;
|
||||
|
||||
/**
|
||||
* Option.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Register
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class Option
|
||||
{
|
||||
public function __construct(public string $sName, public mixed $oValue, public bool $bIsTypeOption = true)
|
||||
{
|
||||
}
|
||||
}
|
||||
108
sources/Forms/Register/OptionsRegister.php
Normal file
108
sources/Forms/Register/OptionsRegister.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Register;
|
||||
|
||||
/**
|
||||
* Option register.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Register
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class OptionsRegister
|
||||
{
|
||||
/** @var Option[] options used for Symfony type creation */
|
||||
private array $aOptions = [];
|
||||
|
||||
/**
|
||||
* Set an option.
|
||||
*
|
||||
* @param string $sOptionName
|
||||
* @param mixed $mDefaultValue
|
||||
* @param bool $bTypeOption
|
||||
*
|
||||
* @return void
|
||||
* @throws RegisterException
|
||||
*/
|
||||
public function SetOption(string $sOptionName, mixed $mDefaultValue = null, bool $bTypeOption = true): void
|
||||
{
|
||||
$this->VerifyOptionName($sOptionName);
|
||||
|
||||
if (isset($this->aOptions[$sOptionName])) {
|
||||
$this->aOptions[$sOptionName]->oValue = $mDefaultValue;
|
||||
} else {
|
||||
$this->aOptions[$sOptionName] = new Option($sOptionName, $mDefaultValue, $bTypeOption);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sOptionName
|
||||
*
|
||||
* @return void
|
||||
* @throws RegisterException
|
||||
*/
|
||||
private function VerifyOptionName(string $sOptionName): void
|
||||
{
|
||||
if (!ctype_alnum(str_replace(['-', '_'], '', $sOptionName))) {
|
||||
throw new RegisterException("Option name '$sOptionName' is not valid. Only alphanumeric characters, hyphens and underscores are allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an option array value.
|
||||
*
|
||||
* @param string $sOptionName
|
||||
* @param string $sArrayKey
|
||||
* @param mixed $mDefaultValue
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function SetOptionArrayValue(string $sOptionName, string $sArrayKey, mixed $mDefaultValue = null): void
|
||||
{
|
||||
// Initialization of the option as an array if not set
|
||||
if (!isset($this->aOptions[$sOptionName])) {
|
||||
$this->SetOption($sOptionName, []);
|
||||
}
|
||||
|
||||
$this->aOptions[$sOptionName]->oValue[$sArrayKey] = $mDefaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all options.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetOptions(): array
|
||||
{
|
||||
$aOptions = array_filter($this->aOptions, fn ($oElement) => $oElement->bIsTypeOption);
|
||||
return array_map(fn ($oElement) => $oElement->oValue, $aOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a type option.
|
||||
*
|
||||
* @param string $sOption
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function GetOption(string $sOption): mixed
|
||||
{
|
||||
return $this->aOptions[$sOption]->oValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an option exists.
|
||||
*
|
||||
* @param string $sOption
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function HasOption(string $sOption): bool
|
||||
{
|
||||
return array_key_exists($sOption, $this->aOptions);
|
||||
}
|
||||
}
|
||||
20
sources/Forms/Register/RegisterException.php
Normal file
20
sources/Forms/Register/RegisterException.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Register;
|
||||
|
||||
use Combodo\iTop\Forms\FormsException;
|
||||
|
||||
/**
|
||||
* Register exception.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Register
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class RegisterException extends FormsException
|
||||
{
|
||||
}
|
||||
@@ -1,21 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (C) 2013-2024 Combodo SAS
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SARL
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Twig\Extension;
|
||||
@@ -28,6 +15,7 @@ use Twig\TwigFilter;
|
||||
* Extension to provide compatibility with Symfony/Twig standard functions
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Twig\Extension
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class FormCompatibilityExtension extends AbstractExtension
|
||||
{
|
||||
|
||||
68
sources/Forms/Validator/AttributeExist.php
Normal file
68
sources/Forms/Validator/AttributeExist.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Validator;
|
||||
|
||||
use Attribute;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Attribute exist constraint.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Validator
|
||||
* @since 3.3.0
|
||||
*/
|
||||
#[Attribute]
|
||||
class AttributeExist extends Constraint
|
||||
{
|
||||
/** @var string Violation message */
|
||||
public string $sMessage = 'The attribute "{{ attribute }}" doesn\'t exist in class "{{ class }}" from OQL "{{ oql }}".';
|
||||
|
||||
/** @var string|mixed OQL expression property path */
|
||||
public string $sOqlPropertyPath;
|
||||
|
||||
/** @var string|null Attribute list filter */
|
||||
public ?string $sFilter;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string|null $sOqlPropertyPath
|
||||
* @param string|null $sFilter
|
||||
* @param array $aOptions
|
||||
* @param array|null $aGroups
|
||||
* @param mixed|null $oPayload
|
||||
*/
|
||||
public function __construct(string $sOqlPropertyPath = null, string $sFilter = null, array $aOptions = [], ?array $aGroups = null, mixed $oPayload = null)
|
||||
{
|
||||
if ($sOqlPropertyPath === null) {
|
||||
throw new InvalidArgumentException('The argument "sOqlPropertyPath" must be set.');
|
||||
}
|
||||
|
||||
// Merge argument into options array
|
||||
$aOptions = array_merge([
|
||||
'sOqlPropertyPath' => $sOqlPropertyPath,
|
||||
], $aOptions);
|
||||
|
||||
parent::__construct($aOptions, $aGroups, $oPayload);
|
||||
|
||||
// Retrieve options
|
||||
$this->sFilter = $sFilter;
|
||||
$this->sOqlPropertyPath = $aOptions['sOqlPropertyPath'];
|
||||
}
|
||||
|
||||
public function getDefaultOption(): string
|
||||
{
|
||||
return 'sOqlPropertyPath';
|
||||
}
|
||||
|
||||
public function getRequiredOptions(): array
|
||||
{
|
||||
return ['sOqlPropertyPath'];
|
||||
}
|
||||
}
|
||||
57
sources/Forms/Validator/AttributeExistValidator.php
Normal file
57
sources/Forms/Validator/AttributeExistValidator.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\Forms\Validator;
|
||||
|
||||
use Combodo\iTop\Forms\IO\Converter\OqlToClassConverter;
|
||||
use Combodo\iTop\Service\DependencyInjection\DIService;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Attribute exist validator.
|
||||
*
|
||||
* @package Combodo\iTop\Forms\Validator
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class AttributeExistValidator extends ConstraintValidator
|
||||
{
|
||||
private ?PropertyAccessorInterface $propertyAccessor;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
$sOql = $this->propertyAccessor->getValue($this->context->getObject(), $constraint->sOqlPropertyPath);
|
||||
|
||||
$oOqlToClassConverter = new OqlToClassConverter();
|
||||
$sClass = strval($oOqlToClassConverter->Convert($sOql));
|
||||
|
||||
$sClass = "UserRequest";
|
||||
|
||||
/** List attributes @var ModelReflection $oModelReflection */
|
||||
$oModelReflection = DIService::GetInstance()->GetService('ModelReflection');
|
||||
$aAttributeCodes = array_keys($oModelReflection->ListAttributes($sClass));
|
||||
|
||||
if (!in_array($value, $aAttributeCodes, true)) {
|
||||
$this->context->buildViolation($constraint->sMessage)
|
||||
->setParameter('{{ attribute }}', $value)
|
||||
->setParameter('{{ class }}', $sClass)
|
||||
->setParameter('{{ oql }}', $sOql)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
86
sources/PropertyTree/AbstractProperty.php
Normal file
86
sources/PropertyTree/AbstractProperty.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree;
|
||||
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\PropertyTree\ValueType\AbstractValueType;
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
*/
|
||||
abstract class AbstractProperty
|
||||
{
|
||||
protected ?AbstractProperty $oParent;
|
||||
protected string $sId;
|
||||
protected ?string $sLabel;
|
||||
|
||||
/** @var array<AbstractProperty> */
|
||||
protected array $aChildren = [];
|
||||
protected ?AbstractValueType $oValueType;
|
||||
protected ?string $sIdWithPath;
|
||||
|
||||
/**
|
||||
* Init property tree node from xml dom node
|
||||
*
|
||||
* @param \Combodo\iTop\DesignElement $oDomNode
|
||||
* @param string $sParentId
|
||||
*
|
||||
* @return void
|
||||
* @throws \DOMFormatException
|
||||
* @throws \Combodo\iTop\PropertyTree\PropertyTreeException
|
||||
*/
|
||||
public function InitFromDomNode(DesignElement $oDomNode, ?AbstractProperty $oParent = null): void
|
||||
{
|
||||
$this->oParent = $oParent;
|
||||
$this->sId = $oDomNode->getAttribute('id');
|
||||
if (is_null($oParent)) {
|
||||
$this->sIdWithPath = $this->sId;
|
||||
} else {
|
||||
$this->sIdWithPath = $oParent->sIdWithPath.'__'.$this->sId;
|
||||
}
|
||||
$this->sLabel = $oDomNode->GetChildText('label');
|
||||
}
|
||||
|
||||
abstract public function ToPHPFormBlock(array &$aPHPFragments = []): string;
|
||||
|
||||
public function GetValueType(): ?AbstractValueType
|
||||
{
|
||||
return $this->oValueType;
|
||||
}
|
||||
|
||||
public function AddChild(AbstractProperty $oValueType): void
|
||||
{
|
||||
$this->aChildren[] = $oValueType;
|
||||
}
|
||||
|
||||
public function GetChildren(): array
|
||||
{
|
||||
return $this->aChildren;
|
||||
}
|
||||
|
||||
public function GetSibling(string $sId): ?AbstractProperty
|
||||
{
|
||||
if (is_null($this->oParent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->oParent->GetChildren() as $oSibling) {
|
||||
if ($oSibling->sId == $sId) {
|
||||
return $oSibling;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function GetIdWithPath(): ?string
|
||||
{
|
||||
return $this->sIdWithPath;
|
||||
}
|
||||
|
||||
}
|
||||
155
sources/PropertyTree/Property.php
Normal file
155
sources/PropertyTree/Property.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree;
|
||||
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\Forms\Block\Expression\BooleanExpressionFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Expression\NumberExpressionFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Expression\StringExpressionFormBlock;
|
||||
use Combodo\iTop\Forms\IO\Format\BooleanIOFormat;
|
||||
use Combodo\iTop\Forms\IO\Format\ClassIOFormat;
|
||||
use Combodo\iTop\Forms\IO\Format\NumberIOFormat;
|
||||
use Combodo\iTop\Forms\IO\Format\StringIOFormat;
|
||||
use Combodo\iTop\PropertyTree\ValueType\ValueTypeFactory;
|
||||
use Exception;
|
||||
use Expression;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class Property extends AbstractProperty
|
||||
{
|
||||
private ?string $sRelevanceCondition = null;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function InitFromDomNode(DesignElement $oDomNode, ?AbstractProperty $oParent = null): void
|
||||
{
|
||||
parent::InitFromDomNode($oDomNode, $oParent);
|
||||
|
||||
$oValueTypeNode = $oDomNode->GetOptionalElement('value-type');
|
||||
if ($oValueTypeNode) {
|
||||
$this->oValueType = ValueTypeFactory::GetInstance()->CreateValueTypeFromDomNode($oValueTypeNode, $this);
|
||||
} else {
|
||||
throw new PropertyTreeException("Node: {$this->sId}, missing value-type in node specification");
|
||||
}
|
||||
|
||||
$this->sRelevanceCondition = $oDomNode->GetChildText('relevance-condition');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $aPHPFragments
|
||||
*
|
||||
* @return string
|
||||
* @throws \Combodo\iTop\PropertyTree\PropertyTreeException
|
||||
*/
|
||||
public function ToPHPFormBlock(array &$aPHPFragments = []): string
|
||||
{
|
||||
$sFormBlockClass = $this->oValueType->GetFormBlockClass();
|
||||
|
||||
$sInputs = '';
|
||||
$sPrerequisiteExpressions = '';
|
||||
if (!is_null($this->sRelevanceCondition)) {
|
||||
$this->GenerateInputs('visible', $this->sRelevanceCondition, $sPrerequisiteExpressions, $sInputs);
|
||||
}
|
||||
|
||||
foreach ($this->oValueType->GetInputValues() as $sInputName => $sValue) {
|
||||
$this->GenerateInputs($sInputName, $sValue, $sPrerequisiteExpressions, $sInputs);
|
||||
}
|
||||
|
||||
foreach ($this->oValueType->GetDynamicInputValues() as $sInputName => $sValue) {
|
||||
$this->GenerateInputs($sInputName, $sValue, $sPrerequisiteExpressions, $sInputs, true);
|
||||
}
|
||||
|
||||
$sLabel = utils::QuoteForPHP($this->sLabel);
|
||||
$aOptions = [
|
||||
'label' => $sLabel,
|
||||
];
|
||||
$aOptions += $this->oValueType->GetFormBlockOptions();
|
||||
$sOptions = '';
|
||||
foreach ($aOptions as $sOption => $sValue) {
|
||||
$sOptions .= "\t\t\t".utils::QuoteForPHP($sOption)." => $sValue,\n";
|
||||
}
|
||||
$this->oValueType->UpdatePHPFragmentsList($aPHPFragments);
|
||||
return <<<PHP
|
||||
{$sPrerequisiteExpressions}\$this->Add('$this->sId', '$sFormBlockClass', [
|
||||
$sOptions\t\t]){$sInputs};
|
||||
|
||||
PHP;
|
||||
}
|
||||
|
||||
private function GenerateInputs(string $sInputName, string $sValue, string &$sPrerequisiteExpressions, string &$sInputs, bool $bIsDynamic = false): void
|
||||
{
|
||||
if (preg_match('/^{{(?<node>\w+)\.(?<output>\w+)}}$/', $sValue, $aMatches) === 1) {
|
||||
$sVerb = $bIsDynamic ? 'AddInputDependsOn' : 'InputDependsOn';
|
||||
$sInputs .= "\n ->$sVerb('$sInputName', '{$aMatches['node']}', '{$aMatches['output']}')";
|
||||
} elseif (preg_match('/^{{(?<expression>.*)}}$/', $sValue, $aMatches) === 1) {
|
||||
$sExpression = $aMatches['expression'];
|
||||
$sBindings = '';
|
||||
try {
|
||||
$oExpression = Expression::FromOQL($sExpression);
|
||||
} catch (Exception $e) {
|
||||
throw new PropertyTreeException("Node: {$this->sId}, invalid syntax in condition: ".$e->getMessage());
|
||||
}
|
||||
$aFieldsToResolve = array_unique($oExpression->ListRequiredFields());
|
||||
foreach ($aFieldsToResolve as $sFieldToResolve) {
|
||||
if (preg_match('/(?<node>\w+)\.(?<output>\w+)/', $sFieldToResolve, $aMatches) === 1) {
|
||||
$sNode = $aMatches['node'];
|
||||
$oSibling = $this->GetSibling($sNode);
|
||||
if (is_null($oSibling)) {
|
||||
// Search in collection
|
||||
if (is_a($this->oParent?->oValueType ?? null, 'Combodo-ValueType-Collection')) {
|
||||
$bSourceNodeFound = false;
|
||||
$aSiblings = $this->oParent->oValueType->GetChildren();
|
||||
foreach ($aSiblings as $oSibling) {
|
||||
if ($oSibling->sId == $sNode) {
|
||||
$bSourceNodeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$bSourceNodeFound) {
|
||||
throw new PropertyTreeException("node: {$this->sId}, source: $sNode not found in collection: {$this->oParent->sId}");
|
||||
}
|
||||
} else {
|
||||
throw new PropertyTreeException("Node: {$this->sId}, invalid source in condition: $sNode");
|
||||
}
|
||||
}
|
||||
$sOutput = $aMatches['output'];
|
||||
if (!in_array($sOutput, $oSibling->oValueType->GetOutputs())) {
|
||||
throw new PropertyTreeException("Node: {$this->sId}, invalid output in condition: $sFieldToResolve");
|
||||
}
|
||||
$sBindings .= "\n ->AddInputDependsOn('{$sNode}.$sOutput', '$sNode', '$sOutput')";
|
||||
} else {
|
||||
throw new PropertyTreeException("Node: {$this->sId}, missing output or source in condition: $sFieldToResolve");
|
||||
}
|
||||
}
|
||||
|
||||
$sExpressionClass = match ($this->oValueType->GetInputType($sInputName)) {
|
||||
BooleanIOFormat::class => BooleanExpressionFormBlock::class,
|
||||
StringIOFormat::class, ClassIOFormat::class => StringExpressionFormBlock::class,
|
||||
NumberIOFormat::class => NumberExpressionFormBlock::class,
|
||||
default => throw new PropertyTreeException("Node: {$this->sId}, unsupported expression for input type: $sInputName"),
|
||||
};
|
||||
|
||||
$sExpression = utils::QuoteForPHP($sExpression);
|
||||
$sPrerequisiteExpressions = <<<PHP
|
||||
\$this->Add('{$this->sId}_{$sInputName}_expression', '$sExpressionClass', [
|
||||
'expression' => $sExpression,
|
||||
]){$sBindings};
|
||||
|
||||
|
||||
PHP;
|
||||
$sVerb = $bIsDynamic ? 'AddInputDependsOn' : 'InputDependsOn';
|
||||
$sInputs .= "\n ->$sVerb('$sInputName', '{$this->sId}_{$sInputName}_expression', 'result')";
|
||||
} else {
|
||||
$sInputs .= "\n ->SetInputValue('$sInputName', ".utils::QuoteForPHP($sValue).")";
|
||||
}
|
||||
}
|
||||
}
|
||||
65
sources/PropertyTree/PropertyTree.php
Normal file
65
sources/PropertyTree/PropertyTree.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree;
|
||||
|
||||
use Combodo\iTop\DesignElement;
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class PropertyTree extends AbstractProperty
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function InitFromDomNode(DesignElement $oDomNode, ?AbstractProperty $oParent = null): void
|
||||
{
|
||||
parent::InitFromDomNode($oDomNode, $oParent);
|
||||
$oPropertyTreeFactory = PropertyTreeFactory::GetInstance();
|
||||
|
||||
// read child properties
|
||||
foreach ($oDomNode->GetUniqueElement('nodes')->childNodes as $oNode) {
|
||||
if ($oNode instanceof DesignElement) {
|
||||
$this->AddChild($oPropertyTreeFactory->CreateNodeFromDom($oNode, $this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function ToPHPFormBlock(array &$aPHPFragments = []): string
|
||||
{
|
||||
$bIsRoot = (count($aPHPFragments) === 0);
|
||||
$sLocalPHP = <<<PHP
|
||||
class FormFor__$this->sId extends Combodo\iTop\Forms\Block\Base\FormBlock
|
||||
{
|
||||
protected function BuildForm(): void
|
||||
{
|
||||
PHP;
|
||||
|
||||
foreach ($this->aChildren as $oProperty) {
|
||||
$sLocalPHP .= "\n".$oProperty->ToPHPFormBlock($aPHPFragments);
|
||||
}
|
||||
|
||||
$sLocalPHP .= <<<PHP
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
$aPHPFragments[] = $sLocalPHP;
|
||||
|
||||
if ($bIsRoot) {
|
||||
// $sOutputPHP = <<<PHP
|
||||
//namespace Combodo\iTop\Forms\Block\Generated;
|
||||
//
|
||||
//PHP;
|
||||
|
||||
return implode("\n", $aPHPFragments);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
81
sources/PropertyTree/PropertyTreeDesign.php
Normal file
81
sources/PropertyTree/PropertyTreeDesign.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree;
|
||||
|
||||
use Combodo\iTop\DesignDocument;
|
||||
use Exception;
|
||||
use ReturnTypeWillChange;
|
||||
use utils;
|
||||
|
||||
class PropertyTreeDesign extends DesignDocument
|
||||
{
|
||||
public function __construct(string $sDesignSourceId = null, string $sType = 'Default')
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
if (!is_null($sDesignSourceId)) {
|
||||
$this->LoadFromCompiledDesigns($sDesignSourceId, $sType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data where the compiler has left them...
|
||||
* @param $sDesignSourceId String Identifier of the section module_design (generally a module name)
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function LoadFromCompiledDesigns(string $sDesignSourceId, string $sType)
|
||||
{
|
||||
$sDesignDir = APPROOT.'env-'.utils::GetCurrentEnvironment()."/core/property_trees/$sType/";
|
||||
$sFile = $sDesignDir.$sDesignSourceId.'.xml';
|
||||
if (!file_exists($sFile)) {
|
||||
$aFiles = glob($sDesignDir.'/*/*.xml');
|
||||
if (count($aFiles) == 0) {
|
||||
$sAvailable = 'none!';
|
||||
} else {
|
||||
$aAvailable = [];
|
||||
foreach ($aFiles as $sFile) {
|
||||
$aAvailable[] = "'".basename($sFile, '.xml')."'";
|
||||
}
|
||||
$sAvailable = implode(', ', $aAvailable);
|
||||
}
|
||||
throw new Exception("Could not load property tree design '$sDesignSourceId'. Available designs: $sAvailable");
|
||||
}
|
||||
|
||||
// Silently keep track of errors
|
||||
libxml_use_internal_errors(true);
|
||||
libxml_clear_errors();
|
||||
$this->load($sFile);
|
||||
//$bValidated = $oDocument->schemaValidate(APPROOT.'setup/itop_design.xsd');
|
||||
$aErrors = libxml_get_errors();
|
||||
if (count($aErrors) > 0) {
|
||||
$aDisplayErrors = [];
|
||||
foreach ($aErrors as $oXmlError) {
|
||||
$aDisplayErrors[] = 'Line '.$oXmlError->line.': '.$oXmlError->message;
|
||||
}
|
||||
|
||||
throw new Exception("Invalid XML in '$sFile'. Errors: ".implode(', ', $aDisplayErrors));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overload of the standard API
|
||||
*
|
||||
* @param $filename
|
||||
* @param int $options
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
// Return type union is not supported by PHP 7.4, we can remove the following PHP attribute and add the return type once iTop min PHP version is PHP 8.0+
|
||||
#[ReturnTypeWillChange]
|
||||
public function save($filename, $options = null)
|
||||
{
|
||||
$this->documentElement->setAttribute('xsi:noNamespaceSchemaLocation', 'https://www.combodo.com/itop-schema/'.ITOP_DESIGN_LATEST_VERSION);
|
||||
|
||||
return parent::save($filename);
|
||||
}
|
||||
}
|
||||
14
sources/PropertyTree/PropertyTreeException.php
Normal file
14
sources/PropertyTree/PropertyTreeException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PropertyTreeException extends Exception
|
||||
{
|
||||
}
|
||||
66
sources/PropertyTree/PropertyTreeFactory.php
Normal file
66
sources/PropertyTree/PropertyTreeFactory.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree;
|
||||
|
||||
use Combodo\iTop\DesignDocument;
|
||||
use Combodo\iTop\DesignElement;
|
||||
|
||||
class PropertyTreeFactory
|
||||
{
|
||||
private static PropertyTreeFactory $oInstance;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
final public static function GetInstance(): PropertyTreeFactory
|
||||
{
|
||||
if (!isset(static::$oInstance)) {
|
||||
static::$oInstance = new PropertyTreeFactory();
|
||||
}
|
||||
|
||||
return static::$oInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a property node from a design element
|
||||
*
|
||||
* @param \Combodo\iTop\DesignElement $oDomNode
|
||||
* @param \Combodo\iTop\PropertyTree\AbstractProperty|null $oParent
|
||||
*
|
||||
* @return \Combodo\iTop\PropertyTree\AbstractProperty
|
||||
* @throws \Combodo\iTop\PropertyTree\PropertyTreeException
|
||||
* @throws \DOMFormatException
|
||||
*/
|
||||
public function CreateTreeFromDom(DesignElement $oDomNode, ?AbstractProperty $oParent = null): AbstractProperty
|
||||
{
|
||||
$oNode = new PropertyTree();
|
||||
$oNode->InitFromDomNode($oDomNode, $oParent);
|
||||
|
||||
return $oNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a property node from a design element
|
||||
*
|
||||
* @param \Combodo\iTop\DesignElement $oDomNode
|
||||
* @param \Combodo\iTop\PropertyTree\AbstractProperty|null $oParent
|
||||
*
|
||||
* @return \Combodo\iTop\PropertyTree\AbstractProperty
|
||||
* @throws \Combodo\iTop\PropertyTree\PropertyTreeException
|
||||
* @throws \DOMFormatException
|
||||
*/
|
||||
public function CreateNodeFromDom(DesignElement $oDomNode, ?AbstractProperty $oParent = null): AbstractProperty
|
||||
{
|
||||
// The class of the property tree node is given by the xsi:type attribute
|
||||
$oNode = new Property();
|
||||
$oNode->InitFromDomNode($oDomNode, $oParent);
|
||||
|
||||
return $oNode;
|
||||
}
|
||||
}
|
||||
81
sources/PropertyTree/ValueType/AbstractValueType.php
Normal file
81
sources/PropertyTree/ValueType/AbstractValueType.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree\ValueType;
|
||||
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\Forms\IO\FormInput;
|
||||
use Combodo\iTop\PropertyTree\AbstractProperty;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
*/
|
||||
abstract class AbstractValueType
|
||||
{
|
||||
abstract public function GetFormBlockClass(): string;
|
||||
|
||||
/** @var FormInput[] */
|
||||
protected array $aInputs = [];
|
||||
protected array $aOutputs = [];
|
||||
protected array $aInputValues = [];
|
||||
protected array $aDynamicInputValues = [];
|
||||
protected array $aFormBlockOptionsForPHP = [];
|
||||
|
||||
/**
|
||||
* @param \Combodo\iTop\DesignElement $oDomNode
|
||||
* @param \Combodo\iTop\PropertyTree\AbstractProperty $oParent Parent node (used for trees)
|
||||
*
|
||||
* @return void
|
||||
* @throws \DOMFormatException
|
||||
*/
|
||||
public function InitFromDomNode(DesignElement $oDomNode, AbstractProperty $oParent): void
|
||||
{
|
||||
$sBlockNodeClass = $this->GetFormBlockClass();
|
||||
$oBlockNode = new $sBlockNodeClass('foo');
|
||||
foreach ($oBlockNode->GetInputs() as $oInput) {
|
||||
$sInputName = $oInput->GetName();
|
||||
$this->aInputs[$sInputName] = $oInput;
|
||||
$sInputValue = $oDomNode->GetChildText($sInputName);
|
||||
if (utils::IsNotNullOrEmptyString($sInputValue)) {
|
||||
$this->aInputValues[$sInputName] = $sInputValue;
|
||||
}
|
||||
}
|
||||
foreach ($oBlockNode->GetOutputs() as $oOutput) {
|
||||
$this->aOutputs[] = $oOutput->GetName();
|
||||
}
|
||||
}
|
||||
|
||||
public function GetFormBlockOptions(): array
|
||||
{
|
||||
return $this->aFormBlockOptionsForPHP;
|
||||
}
|
||||
|
||||
public function GetInputValues(): array
|
||||
{
|
||||
return $this->aInputValues;
|
||||
}
|
||||
|
||||
public function GetInputType(string $sInputName): string
|
||||
{
|
||||
return $this->aInputs[$sInputName]->GetDataType();
|
||||
}
|
||||
|
||||
public function GetDynamicInputValues(): array
|
||||
{
|
||||
return $this->aDynamicInputValues;
|
||||
}
|
||||
|
||||
public function GetOutputs(): array
|
||||
{
|
||||
return $this->aOutputs;
|
||||
}
|
||||
|
||||
public function UpdatePHPFragmentsList(array &$aPHPFragments): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree\ValueType;
|
||||
|
||||
use Combodo\iTop\Forms\Block\DataModel\Dashlet\AggregateFunctionFormBlock;
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ValueTypeAggregateFunction extends AbstractValueType
|
||||
{
|
||||
public function GetFormBlockClass(): string
|
||||
{
|
||||
return AggregateFunctionFormBlock::class;
|
||||
}
|
||||
}
|
||||
19
sources/PropertyTree/ValueType/ValueTypeBoolean.php
Normal file
19
sources/PropertyTree/ValueType/ValueTypeBoolean.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree\ValueType;
|
||||
|
||||
use Combodo\iTop\Forms\Block\Base\CheckboxFormBlock;
|
||||
use Combodo\iTop\PropertyTree\ValueType\AbstractValueType;
|
||||
|
||||
class ValueTypeBoolean extends AbstractValueType
|
||||
{
|
||||
public function GetFormBlockClass(): string
|
||||
{
|
||||
return CheckboxFormBlock::class;
|
||||
}
|
||||
}
|
||||
43
sources/PropertyTree/ValueType/ValueTypeChoice.php
Normal file
43
sources/PropertyTree/ValueType/ValueTypeChoice.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree\ValueType;
|
||||
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
|
||||
use Combodo\iTop\PropertyTree\AbstractProperty;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ValueTypeChoice extends AbstractValueType
|
||||
{
|
||||
public function GetFormBlockClass(): string
|
||||
{
|
||||
return ChoiceFormBlock::class;
|
||||
}
|
||||
|
||||
public function InitFromDomNode(DesignElement $oDomNode, AbstractProperty $oParent): void
|
||||
{
|
||||
parent::InitFromDomNode($oDomNode, $oParent);
|
||||
|
||||
$sChoices = "[\n";
|
||||
foreach ($oDomNode->GetNodes('values/value') as $oValueNode) {
|
||||
/** @var DesignElement $oValueNode */
|
||||
$sValue = utils::QuoteForPHP($oValueNode->GetAttribute('id'));
|
||||
$sLabel = utils::QuoteForPHP($oValueNode->GetChildText('label'));
|
||||
$sChoices .= <<<PHP
|
||||
\Dict::S($sLabel) => $sValue,
|
||||
|
||||
PHP;
|
||||
}
|
||||
$sChoices .= "\t\t\t]";
|
||||
|
||||
$this->aFormBlockOptionsForPHP['choices'] = $sChoices;
|
||||
}
|
||||
}
|
||||
34
sources/PropertyTree/ValueType/ValueTypeChoiceFromInput.php
Normal file
34
sources/PropertyTree/ValueType/ValueTypeChoiceFromInput.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree\ValueType;
|
||||
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\Forms\Block\Base\ChoiceFromInputsBlock;
|
||||
use Combodo\iTop\PropertyTree\AbstractProperty;
|
||||
use Combodo\iTop\PropertyTree\ValueType\AbstractValueType;
|
||||
use utils;
|
||||
|
||||
class ValueTypeChoiceFromInput extends AbstractValueType
|
||||
{
|
||||
public function GetFormBlockClass(): string
|
||||
{
|
||||
return ChoiceFromInputsBlock::class;
|
||||
}
|
||||
|
||||
public function InitFromDomNode(DesignElement $oDomNode, AbstractProperty $oParent): void
|
||||
{
|
||||
parent::InitFromDomNode($oDomNode, $oParent);
|
||||
|
||||
foreach ($oDomNode->GetNodes('values/value') as $oValueNode) {
|
||||
/** @var DesignElement $oValueNode */
|
||||
$sValue = $oValueNode->GetAttribute('id');
|
||||
$sLabel = $oValueNode->GetChildText('label');
|
||||
$this->aDynamicInputValues[$sValue] = $sLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
sources/PropertyTree/ValueType/ValueTypeClass.php
Normal file
53
sources/PropertyTree/ValueType/ValueTypeClass.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree\ValueType;
|
||||
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\Forms\Block\Base\ChoiceFormBlock;
|
||||
use Combodo\iTop\Forms\Block\Base\TextFormBlock;
|
||||
use Combodo\iTop\PropertyTree\AbstractProperty;
|
||||
use Combodo\iTop\PropertyTree\ValueType\AbstractValueType;
|
||||
use Combodo\iTop\Service\DependencyInjection\DIService;
|
||||
use utils;
|
||||
|
||||
class ValueTypeClass extends AbstractValueType
|
||||
{
|
||||
protected array $aCategories = [];
|
||||
|
||||
public function GetFormBlockClass(): string
|
||||
{
|
||||
return ChoiceFormBlock::class;
|
||||
}
|
||||
|
||||
public function InitFromDomNode(DesignElement $oDomNode, AbstractProperty $oParent): void
|
||||
{
|
||||
parent::InitFromDomNode($oDomNode, $oParent);
|
||||
|
||||
$sCategories = $oDomNode->GetChildText('categories-csv');
|
||||
/** @var \ModelReflection $oModelReflection */
|
||||
$oModelReflection = DIService::GetInstance()->GetService('ModelReflection');
|
||||
|
||||
$sChoices = "[\n";
|
||||
$aClasses = $oModelReflection->GetClasses($sCategories, true);
|
||||
sort($aClasses);
|
||||
foreach ($aClasses as $sClass) {
|
||||
if ($oModelReflection->IsAbstract($sClass)) {
|
||||
continue;
|
||||
}
|
||||
$sValue = utils::QuoteForPHP($sClass);
|
||||
$sChoices .= <<<PHP
|
||||
\Dict::S('Class:$sClass') => $sValue,
|
||||
|
||||
PHP;
|
||||
}
|
||||
$sChoices .= "\t\t\t]";
|
||||
|
||||
$this->aFormBlockOptionsForPHP['choices'] = $sChoices;
|
||||
}
|
||||
|
||||
}
|
||||
23
sources/PropertyTree/ValueType/ValueTypeClassAttribute.php
Normal file
23
sources/PropertyTree/ValueType/ValueTypeClassAttribute.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree\ValueType;
|
||||
|
||||
use Combodo\iTop\DesignElement;
|
||||
use Combodo\iTop\Forms\Block\DataModel\AttributeChoiceFormBlock;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ValueTypeClassAttribute extends AbstractValueType
|
||||
{
|
||||
public function GetFormBlockClass(): string
|
||||
{
|
||||
return AttributeChoiceFormBlock::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree\ValueType;
|
||||
|
||||
use Combodo\iTop\Forms\Block\DataModel\Dashlet\ClassAttributeGroupByFormBlock;
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ValueTypeClassAttributeGroupBy extends AbstractValueType
|
||||
{
|
||||
public function GetFormBlockClass(): string
|
||||
{
|
||||
return ClassAttributeGroupByFormBlock::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* @copyright Copyright (C) 2010-2025 Combodo SAS
|
||||
* @license http://opensource.org/licenses/AGPL-3.0
|
||||
*/
|
||||
|
||||
namespace Combodo\iTop\PropertyTree\ValueType;
|
||||
|
||||
use Combodo\iTop\Forms\Block\DataModel\AttributeValueChoiceFormBlock;
|
||||
|
||||
/**
|
||||
* @since 3.3.0
|
||||
*/
|
||||
class ValueTypeClassAttributeValue extends AbstractValueType
|
||||
{
|
||||
public function GetFormBlockClass(): string
|
||||
{
|
||||
return AttributeValueChoiceFormBlock::class;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user