N°7063 - Forms SDK - Add Symfony forms component

error forms issue
This commit is contained in:
Benjamin Dalsass
2024-01-02 11:31:52 +01:00
parent 2bcc4d9989
commit 75fde4c9a3
15 changed files with 316 additions and 73 deletions

View File

@@ -42,4 +42,16 @@
color: grey;
}
.cke_focus .cke_contents{
border: 1px #86b7fe solid;
outline: 0;
border-radius: 6px;
box-shadow: 0 0 0 0.25rem rgba(13,110,253,.25);
}
.form_interval_horizontal{
display: flex;
}
.form_interval_horizontal > div{
margin-right: 8px;
}

View File

@@ -3,7 +3,7 @@
'name' => 'combodo/itop',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '20949605c30bc7a83d0b554f4af23b6cf802c292',
'reference' => '2bcc4d99896aab3e70ed0d7aa5f43d661b5bbeb5',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -22,7 +22,7 @@
'combodo/itop' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '20949605c30bc7a83d0b554f4af23b6cf802c292',
'reference' => '2bcc4d99896aab3e70ed0d7aa5f43d661b5bbeb5',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),

View File

@@ -37,11 +37,11 @@ class TestController extends AbstractAppController
$oFactory = FormHelper::CreateSampleFormFactory($oFormManager, $oRouter);
}
catch (Exception $e) {
throw $this->createNotFoundException('unable to load object Person 1');
throw $this->createNotFoundException('unable to create sample form factory', $e);
}
// get the form
$oForm = $oFactory->GetForm();
$oForm = $oFactory->CreateForm();
// handle request
$oForm->handleRequest($oRequest);
@@ -52,9 +52,17 @@ class TestController extends AbstractAppController
// retrieve form data
$data = $oForm->getData();
return $this->redirectToRoute('app_success');
// let's adaptaters save theirs data
foreach($oFactory->GetAllAdapters() as $oAdapter){
$oAdapter->UpdateFieldsData($data);
}
dump($data);
// return $this->redirectToRoute('app_success');
}
// render view
return $this->render('formSDK/form.html.twig', [
'form' => $oForm->createView(),
'theme' => 'formSDK/themes/portal.html.twig'
@@ -70,17 +78,18 @@ class TestController extends AbstractAppController
$oFactory = FormHelper::CreateSampleFormFactory($oFormManager, $oRouter);
}
catch (Exception $e) {
throw $this->createNotFoundException('unable to load object Person 1');
throw $this->createNotFoundException('unable to create sample form factory');
}
// get the forms (named instances)
$oForm1 = $oFactory->GetForm('form1');
$oForm2 = $oFactory->GetForm('form2');
$oForm1 = $oFactory->CreateForm('form1');
$oForm2 = $oFactory->CreateForm('form2');
// handle request
$oForm1->handleRequest($oRequest);
$oForm2->handleRequest($oRequest);
// render view
return $this->render('formSDK/theme.html.twig', [
'name1' => 'Portail',
'name2' => 'Console',
@@ -95,7 +104,7 @@ class TestController extends AbstractAppController
#[Route('/formSDK/ajax_select', name: 'formSDK_ajax_select')]
public function ajax(Request $oRequest): Response
{
$oJson = file_get_contents('sources/FormSDK/Resources/dogs.json');
$oJson = file_get_contents('sources/FormImplementation/Resources/dogs.json');
$aDogs = json_decode($oJson, true);
$sQuery = $oRequest->request->get('query');

View File

@@ -4,6 +4,7 @@ namespace Combodo\iTop\FormImplementation\Helper;
use Combodo\iTop\Controller\AbstractAppController;
use Combodo\iTop\FormSDK\Service\FormManager;
use DateInterval;
use DateTime;
use MetaModel;
use Symfony\Component\Routing\RouterInterface;
@@ -28,7 +29,7 @@ class FormHelper
$oFormFactory = $oFormManager->CreateFactory();
// add X person forms...
for($i = 0 ; $i < 2 ; $i++){
for($i = 0 ; $i < 5 ; $i++){
// retrieve person
$oPerson = MetaModel::GetObject('Person', $i+1);
@@ -49,8 +50,7 @@ class FormHelper
// tel - text with pattern
$oFormFactory->AddTextField('tel', [
'label' => 'Tel',
'constraints' => new Regex(['pattern' => '+{33}(0) 00 00 00 00']),
// 'constraints' => new Regex(['pattern' => '/^[1-6]\d{0,5}$/']),
'constraints' => new Regex(['pattern' => '/\+33\(\d\) \d\d \d\d \d\d \d\d/'], null, '+{33}(0) 00 00 00 00'),
'required' => false
], null);
@@ -61,6 +61,25 @@ class FormHelper
'required' => false
], new DateTime('1979/06/27'));
// count - number
$oFormFactory->AddNumberField('count', [
'label' => 'Compteur',
'required' => false
], 10);
// interval - duration
$oFormFactory->AddDurationField('interval', [
'label' => 'Fréquence',
'input' => 'array',
'with_minutes' => true,
'with_seconds' => true,
'with_weeks' => true,
'with_days' => false,
'attr' => [
'class' => 'form_interval_horizontal'
]
], ['days' => '12', 'hours' => '13', 'years' => '10', 'months' => '6', 'minutes' => '0', 'seconds' => '0', 'weeks' => '3']);
// ready
$oFormFactory->AddSwitchField('notify', [
'label' => 'Veuillez m\'avertir en cas de changement',

View File

@@ -19,6 +19,8 @@
namespace Combodo\iTop\FormSDK\Field;
use Exception;
/**
* Description of a form field.
*
@@ -41,7 +43,12 @@ class FormFieldDescription
private readonly array $aOptions
)
{
$oCheckStatus = $this->oType->CheckOptions($this->aOptions);
if(!$oCheckStatus['valid']){
$sInvalidOptions = implode(', ', $oCheckStatus['invalid_options']);
throw new Exception("Invalid option(s) $sInvalidOptions provided for field $sName");
}
}
/**

View File

@@ -28,11 +28,13 @@ namespace Combodo\iTop\FormSDK\Field;
enum FormFieldTypeEnumeration : string
{
case TEXT = 'TEXT';
case NUMBER = 'NUMBER';
case AREA = 'AREA';
case DATE = 'DATE';
case SELECT = 'SELECT';
case SWITCH = 'SWITCH';
case DB_OBJECT = 'DB_OBJECT';
case DURATION = 'DURATION';
/**
* Return available options.
@@ -41,11 +43,58 @@ enum FormFieldTypeEnumeration : string
*/
public function GetAvailableOptions() : array
{
$aOptions = ['required', 'disabled', 'attr', 'label', 'label_attr'];
// global options
$aOptions = ['required', 'disabled', 'attr', 'label', 'label_attr', 'help'];
return match ($this->value) {
FormFieldTypeEnumeration::SELECT => array_merge($aOptions, ['placeholder']),
// specific options
$test = match ($this) {
FormFieldTypeEnumeration::TEXT => array_merge($aOptions,
['constraints']
),
FormFieldTypeEnumeration::SELECT => array_merge($aOptions,
['placeholder', 'choices', 'expanded', 'multiple']
),
FormFieldTypeEnumeration::DATE => array_merge($aOptions,
['widget']
),
FormFieldTypeEnumeration::DURATION => array_merge($aOptions,
['input', 'with_minutes', 'with_seconds', 'with_weeks', 'with_days']
),
FormFieldTypeEnumeration::DB_OBJECT => array_merge($aOptions,
['fields']
),
default => $aOptions,
};
return $test;
}
/**
* Check array of options.
*
* @param array $aOptions
*
* @return array{valid: bool, invalid_options: array}
*/
public function CheckOptions(array $aOptions) : array
{
// invalid options array
$aInvalidOptions = [];
// retrieve available options
$aAvailableOptions = $this->GetAvailableOptions();
// check each option...
foreach($aOptions as $sKey => $oOption){
if(!in_array($sKey, $aAvailableOptions)){
$aInvalidOptions[] = $sKey;
}
}
return [
'valid' => empty($aInvalidOptions),
'invalid_options' => $aInvalidOptions,
];
}
}

View File

@@ -28,23 +28,34 @@ namespace Combodo\iTop\FormSDK\Service\FactoryAdapter;
interface FormFactoryAdapterInterface
{
/**
* Return data attached to the form.
*
* @return mixed
*/
public function GetFormData() : mixed;
/**
* Return description the form.
*
* @return \Combodo\iTop\FormSDK\Field\FormFieldDescription[]
*/
public function GetFormDescriptions() : array;
/**
* Return form identifier.
* Return adapter identifier.
*
* @return string
*/
public function GetIdentifier() : string;
/**
* Return fields descriptions.
*
* @return \Combodo\iTop\FormSDK\Field\FormFieldDescription[]
*/
public function GetFieldsDescriptions() : array;
/**
* Return fields data.
*
* @return mixed
*/
public function GetFieldsData() : mixed;
/**
* Update fields data.
*
* @param array $aFormData
*
* @return bool
*/
public function UpdateFieldsData(array $aFormData) : bool;
}

View File

@@ -140,7 +140,7 @@ final class FormFactoryObjectAdapter implements FormFactoryAdapterInterface
}
/** @inheritdoc */
public function GetFormData() : array
public function GetFieldsData() : array
{
$aData = [];
foreach ($this->aAttributes as $sAttributeCode => $oValue){
@@ -164,13 +164,13 @@ final class FormFactoryObjectAdapter implements FormFactoryAdapterInterface
}
/** @inheritdoc */
public function GetFormDescriptions() : array
public function GetFieldsDescriptions() : array
{
$aDescriptions = [];
$aFieldsDescriptions = [];
foreach ($this->aAttributes as $sAttCode => $oValue){
try {
$aDescriptions[$this->GetAttributeName($sAttCode)] = $this->GetAttributeDescription($sAttCode);
$aFieldsDescriptions[$this->GetAttributeName($sAttCode)] = $this->GetAttributeDescription($sAttCode);
}
catch (Exception $e) {
ExceptionLog::LogException($e);
@@ -179,13 +179,13 @@ final class FormFactoryObjectAdapter implements FormFactoryAdapterInterface
if($this->bGroup){
$oGroupDescriptions = new FormFieldDescription($this->GetIdentifier(), FormFieldTypeEnumeration::DB_OBJECT, [
'descriptions' => $aDescriptions,
'fields' => $aFieldsDescriptions,
'label' => $this->GetLabel()
]);
return [$this->GetIdentifier() => $oGroupDescriptions];
}
else{
return $aDescriptions;
return $aFieldsDescriptions;
}
}
@@ -199,4 +199,20 @@ final class FormFactoryObjectAdapter implements FormFactoryAdapterInterface
{
return get_class($this->oDBObject) . '_' . $this->oDBObject->GetKey();
}
/** @inheritdoc */
public function UpdateFieldsData(array $aFormData) : bool
{
if($this->bGroup){
$aFormData = $aFormData[$this->GetIdentifier()];
}
foreach ($this->aAttributes as $sAttCode => $aValue){
$this->oDBObject->Set($sAttCode, $aFormData[$this->GetAttributeName($sAttCode)]);
}
$this->oDBObject->DBUpdate();
return true;
}
}

View File

@@ -19,7 +19,6 @@
namespace Combodo\iTop\FormSDK\Service;
use Combodo\iTop\FormSDK\Helper\SelectDataProvider;
use Combodo\iTop\FormSDK\Service\FormFactoryBuilderTrait;
use Combodo\iTop\FormSDK\Service\FactoryAdapter\FormFactoryObjectAdapter;
use Combodo\iTop\FormSDK\Service\FactoryAdapter\FormFactoryAdapterInterface;
@@ -47,11 +46,11 @@ class FormFactory
/** @var \Combodo\iTop\FormSDK\Service\FactoryAdapter\FormFactoryAdapterInterface[] $aAdapters */
private array $aAdapters = [];
/** @var array $aDescriptions form types descriptions */
private array $aDescriptions = [];
/** @var array $aFieldsDescriptions form types descriptions */
private array $aFieldsDescriptions = [];
/** @var array $aData form data */
private array $aData = [];
/** @var array $aFieldsData form data */
private array $aFieldsData = [];
/** builder */
use FormFactoryBuilderTrait;
@@ -71,22 +70,22 @@ class FormFactory
}
/**
* Return descriptions and data arrays.
* Return fields descriptions and data arrays.
*
* @return array{descriptions:array, data:array}
*/
public function GetFormDescriptionsAndData() : array
public function GetFieldsDescriptionsAndData() : array
{
// prepare data
$aResult = [
'descriptions' => $this->aDescriptions,
'data' => $this->aData,
'descriptions' => $this->aFieldsDescriptions,
'data' => $this->aFieldsData,
];
// merge each adapter data...
foreach ($this->GetAllAdapters() as $oAdapter){
$aResult['descriptions'] = array_merge($aResult['descriptions'], $oAdapter->GetFormDescriptions());
$aResult['data'] = array_merge($aResult['data'], $oAdapter->GetFormData());
$aResult['descriptions'] = array_merge($aResult['descriptions'], $oAdapter->GetFieldsDescriptions());
$aResult['data'] = array_merge($aResult['data'], $oAdapter->GetFieldsData());
}
return $aResult;
@@ -132,15 +131,15 @@ class FormFactory
}
/**
* Get form.
* Create form.
*
* @param string|null $sName
* @return mixed
*/
public function GetForm(?string $sName = null) : mixed
public function CreateForm(?string $sName = null) : mixed
{
['descriptions' => $aDescriptions, 'data' => $aData] = $this->GetFormDescriptionsAndData();
return $this->oSymfonyBridge->GetForm($aDescriptions, $aData, $sName);
['descriptions' => $aDescriptions, 'data' => $aData] = $this->GetFieldsDescriptionsAndData();
return $this->oSymfonyBridge->CreateForm($aDescriptions, $aData, $sName);
}
}

View File

@@ -27,7 +27,7 @@ trait FormFactoryBuilderTrait
$oConstraint = $aOptions['constraints'];
if($oConstraint instanceof Regex){
$aWidgetOptions = [
'pattern' => $oConstraint->pattern,
'pattern' => $oConstraint->getHtmlPattern(),
];
$aOptions = array_merge([
'attr' => [
@@ -39,8 +39,26 @@ trait FormFactoryBuilderTrait
}
}
$this->aDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::TEXT, $aOptions);
$this->aData[$sKey] = $oData;
$this->aFieldsDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::TEXT, $aOptions);
$this->aFieldsData[$sKey] = $oData;
return $this;
}
/**
* Add number field.
*
* @param string $sKey
* @param array $aOptions
* @param mixed $oData
*
* @return $this
*/
public function AddNumberField(string $sKey, array $aOptions, mixed $oData = null) : FormFactory
{
$this->aFieldsDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::NUMBER, $aOptions);
$this->aFieldsData[$sKey] = $oData;
return $this;
}
@@ -63,8 +81,8 @@ trait FormFactoryBuilderTrait
]
], $aOptions);
$this->aDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::AREA, $aOptions);
$this->aData[$sKey] = $oData;
$this->aFieldsDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::AREA, $aOptions);
$this->aFieldsData[$sKey] = $oData;
return $this;
}
@@ -80,13 +98,30 @@ trait FormFactoryBuilderTrait
*/
public function AddDateField(string $sKey, array $aOptions, mixed $oData = null) : FormFactory
{
$this->aDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::DATE, $aOptions);
$this->aData[$sKey] = $oData;
$this->aFieldsDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::DATE, $aOptions);
$this->aFieldsData[$sKey] = $oData;
return $this;
}
/**
* Add duration field.
*
* @param string $sKey
* @param array $aOptions
* @param mixed $oData
*
* @return $this
*/
public function AddDurationField(string $sKey, array $aOptions, mixed $oData = null) : FormFactory
{
$this->aFieldsDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::DURATION, $aOptions);
$this->aFieldsData[$sKey] = $oData;
return $this;
}
/**
* Add select field.
*
@@ -98,8 +133,8 @@ trait FormFactoryBuilderTrait
*/
public function AddSelectField(string $sKey, array $aOptions, mixed $oData = null) : FormFactory
{
$this->aDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::SELECT, $aOptions);
$this->aData[$sKey] = $oData;
$this->aFieldsDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::SELECT, $aOptions);
$this->aFieldsData[$sKey] = $oData;
return $this;
}
@@ -202,8 +237,8 @@ trait FormFactoryBuilderTrait
'label_attr' => ['class' => 'checkbox-switch'],
], $aOptions);
$this->aDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::SWITCH, $aOptions);
$this->aData[$sKey] = $oData;
$this->aFieldsDescriptions[$sKey] = new FormFieldDescription($sKey, FormFieldTypeEnumeration::SWITCH, $aOptions);
$this->aFieldsData[$sKey] = $oData;
return $this;
}

View File

@@ -25,8 +25,10 @@ use Combodo\iTop\FormSDK\Field\FormFieldTypeEnumeration;
use LogAPI;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateIntervalType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormFactoryInterface;
@@ -100,25 +102,39 @@ class SymfonyBridge
case FormFieldTypeEnumeration::DB_OBJECT:
$aOptions = $oFormDescription->GetOptions();
$aItems = [];
foreach ($aOptions['descriptions'] as $oChildFormDescription){
$aFields = [];
foreach ($aOptions['fields'] as $oChildFormDescription){
$aSymfony = $this->ToSymfonyFormType($oChildFormDescription);
$aItems[] = $aSymfony;
$aFields[] = $aSymfony;
}
$aOptions['descriptions'] = $aItems;
$aOptions['fields'] = $aFields;
return [
'name' => $oFormDescription->GetName(),
'type' => FormObjectType::class,
'options' => $aOptions
];
case FormFieldTypeEnumeration::NUMBER:
return [
'name' => $oFormDescription->GetName(),
'type' => NumberType::class,
'options' => $oFormDescription->GetOptions()
];
case FormFieldTypeEnumeration::DURATION:
return [
'name' => $oFormDescription->GetName(),
'type' => DateIntervalType::class,
'options' => $oFormDescription->GetOptions()
];
default:
return null;
}
}
/**
* Return Symfony form.
* Create Symfony form.
*
* @param array $aDescriptions
* @param array $aData
@@ -126,7 +142,7 @@ class SymfonyBridge
*
* @return \Symfony\Component\Form\FormInterface
*/
public function GetForm(array $aDescriptions, array $aData, ?string $sName = null): FormInterface
public function CreateForm(array $aDescriptions, array $aData, ?string $sName = null): FormInterface
{
// create Symfony form builder
if($sName !== null){

View File

@@ -34,20 +34,61 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class FormObjectType extends AbstractType
{
/*
* View Definition.
*
* [
* 'row' => [
* 'col' => [
* 'description' => [
* 'label' => '',
* 'css_classes' => ''
* ],
* 'items' => ['name', 'birthday']
* 'fieldset' => ['address', 'city', 'country']
* ]
* ]
* ]
*
*
*/
/** @inheritdoc */
public function buildForm(FormBuilderInterface $builder, array $options) : void
{
/** @var FormFieldDescription $oDescription */
foreach ($options['descriptions'] as $oDescription){
$builder->add($oDescription['name'], $oDescription['type'], $oDescription['options']);
foreach ($options['view'] as $oItem) {
if($oItem === 'row'){
$this->handleRow();
}
else if($oItem === 'col'){
$this->handleColumn();
}
else{
}
}
foreach ($options['fields'] as $oField){
$builder->add($oField['name'], $oField['type'], $oField['options']);
}
}
private function handleRow(FormBuilderInterface $builder, array $aData){
}
private function handleColumn(FormBuilderInterface $builder, array $aData){
}
/** @inheritdoc */
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'descriptions' => [],
'fields' => [],
'view' => [],
'attr' => [
'class' => ''
]

View File

@@ -6,4 +6,6 @@
<h2>Success</h2>
{% endblock body %}

View File

@@ -1,3 +1,30 @@
{% use "form_table_layout.html.twig" %}
{% block checkbox_widget -%}
{% set parent_label_class = parent_label_class|default('') %}
{% if errors|length > 0 -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' error')|trim}) %}
{% endif %}
{% if 'checkbox-inline' in parent_label_class %}
{{ form_label(form, null, { widget: parent() }) }}
{% else %}
<div class="checkbox">
{{ form_label(form, null, { widget: parent() }) }}
</div>
{% endif %}
{%- endblock checkbox_widget %}
{% block radio_widget -%}
{% set parent_label_class = parent_label_class|default('') %}
{% if 'radio-inline' in parent_label_class %}
{{ form_label(form, null, { widget: parent() }) }}
{% else %}
{% if errors|length > 0 -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' error')|trim}) %}
{% endif %}
<div class="radio">
{{ form_label(form, null, { widget: parent() }) }}
</div>
{% endif %}
{%- endblock radio_widget %}

View File

@@ -15,7 +15,7 @@
{% endif %}
{% if form.vars.attr['data-pattern'] is defined %}
<i class="fa-solid fa-i-cursor pattern" title="{{ form.vars.attr['data-pattern'] }}"></i>
<i class="fa-solid fa-i-cursor pattern" title="pattern: {{ form.vars.attr['data-pattern'] }}"></i>
{% endif %}
{% if form.vars.attr['data-ajax-query-type'] is defined and form.vars.attr['data-ajax-query-type'] == 'AJAX' %}