From f077ccd0091eea50c07c0b9bae85cb14d6f1578d Mon Sep 17 00:00:00 2001 From: David ALLIX <517753+webda2l@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:42:40 +0100 Subject: [PATCH] Progress --- .devcontainer/Dockerfile | 18 +++--- src/A2lixAutoFormBundle.php | 17 +++++- src/Form/Attribute/AutoTypeCustom.php | 2 + src/Form/Builder/AutoTypeBuilder.php | 59 ++++++++++++++++---- src/Form/Type/AutoType.php | 33 +++++++++-- src/Form/TypeGuesser/TypeInfoTypeGuesser.php | 1 + 6 files changed, 104 insertions(+), 26 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index cb72d12..b2884bf 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,20 +1,20 @@ FROM php:8.4-cli-trixie RUN apt-get update && apt-get install -y --no-install-recommends \ - file \ - git \ + file \ + git \ openssh-client \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ RUN set -eux; \ - install-php-extensions \ - @composer \ - apcu \ - opcache \ - xdebug \ - ; + install-php-extensions \ + @composer \ + apcu \ + opcache \ + xdebug \ + ; RUN useradd -m vscode diff --git a/src/A2lixAutoFormBundle.php b/src/A2lixAutoFormBundle.php index a6a11f9..c325715 100644 --- a/src/A2lixAutoFormBundle.php +++ b/src/A2lixAutoFormBundle.php @@ -12,11 +12,12 @@ namespace A2lix\AutoFormBundle; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; -final class A2lixAutoFormBundle extends AbstractBundle +final class A2lixAutoFormBundle extends AbstractBundle implements CompilerPassInterface { #[\Override] public function configure(DefinitionConfigurator $definition): void @@ -42,4 +43,18 @@ public function loadExtension(array $config, ContainerConfigurator $container, C ->arg('$globalExcludedChildren', $config['children_excluded']) ; } + + public function build(ContainerBuilder $container): void + { + $container->addCompilerPass($this); + } + + public function process(ContainerBuilder $container): void + { + if ($container->hasExtension('a2lix_translation_form')) { + $container->getDefinition('a2lix_auto_form.form.type.auto_type') + ->setArgument('$globalTranslatedChildren', true) + ; + } + } } diff --git a/src/Form/Attribute/AutoTypeCustom.php b/src/Form/Attribute/AutoTypeCustom.php index 9104895..c2dcb1d 100644 --- a/src/Form/Attribute/AutoTypeCustom.php +++ b/src/Form/Attribute/AutoTypeCustom.php @@ -30,6 +30,7 @@ public function __construct( private ?string $name = null, private ?bool $excluded = null, private ?bool $embedded = null, + private ?bool $translated = null, private ?array $groups = null, ) {} @@ -45,6 +46,7 @@ public function getOptions(): array ...(null !== $this->name ? ['child_name' => $this->name] : []), ...(null !== $this->excluded ? ['child_excluded' => $this->excluded] : []), ...(null !== $this->embedded ? ['child_embedded' => $this->embedded] : []), + ...(null !== $this->embedded ? ['child_translated' => $this->translated] : []), ...(null !== $this->groups ? ['child_groups' => $this->groups] : []), ]; } diff --git a/src/Form/Builder/AutoTypeBuilder.php b/src/Form/Builder/AutoTypeBuilder.php index f362374..4208ec9 100644 --- a/src/Form/Builder/AutoTypeBuilder.php +++ b/src/Form/Builder/AutoTypeBuilder.php @@ -13,6 +13,7 @@ use A2lix\AutoFormBundle\Form\Attribute\AutoTypeCustom; use A2lix\AutoFormBundle\Form\Type\AutoType; +use Doctrine\Common\Collections\ArrayCollection; use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; @@ -26,6 +27,7 @@ * child_name?: string, * child_excluded?: bool, * child_embedded?: bool, + * child_translated?: bool, * child_groups?: list, * ... * } @@ -35,6 +37,7 @@ * children: array, * children_excluded: list|"*", * children_embedded: list|"*", + * children_translated: bool, * children_groups: list|null, * builder: FormBuilderCallable|null, * } @@ -52,6 +55,7 @@ public function __construct( public function buildChildren(FormBuilderInterface $builder, array $formOptions): void { $dataClass = $this->getDataClass($form = $builder->getForm()); + dump($dataClass); if (null === $classProperties = $this->propertyInfoExtractor->getProperties($dataClass)) { throw new \RuntimeException(\sprintf('Unable to extract properties of "%s".', $dataClass)); @@ -61,7 +65,7 @@ public function buildChildren(FormBuilderInterface $builder, array $formOptions) $allChildrenExcluded = '*' === $formOptions['children_excluded']; $allChildrenEmbedded = '*' === $formOptions['children_embedded']; $childrenGroups = $formOptions['children_groups'] ?? ['Default']; - $formLevel = $this->getFormLevel($form); + $formDepth = $this->getFormDepth($form); /** @var list $classProperties */ foreach ($classProperties as $classProperty) { @@ -116,12 +120,17 @@ public function buildChildren(FormBuilderInterface $builder, array $formOptions) // PropertyInfo? Enrich childOptions if (null !== $propTypeInfo = $this->propertyInfoExtractor->getType($dataClass, $classProperty)) { // @phpstan-ignore argument.type + $formChildTranslated = ($formOptions['children_translated'] || ($childOptions['child_translated'] ?? false)) + && ('translations' === $classProperty); + // @phpstan-ignore argument.type $formChildEmbedded = $allChildrenEmbedded || \in_array($classProperty, $formOptions['children_embedded'], true) || ($childOptions['child_embedded'] ?? false); - if ($formChildEmbedded) { - $childOptions = $this->updateChildOptions($childOptions, $propTypeInfo, $formLevel); - } + $childOptions = match (true) { + $formChildTranslated => $this->updateTranslatedChildOptions($childOptions, $propTypeInfo, $refProperty), + $formChildEmbedded => $this->updateEmbeddedChildOptions($childOptions, $propTypeInfo, $refProperty, $formDepth), + default => $childOptions, + }; } $this->addChild($builder, $classProperty, $childOptions); @@ -171,6 +180,7 @@ private function addChild(FormBuilderInterface $builder, string|FormBuilderInter $options['child_type'], $options['child_excluded'], $options['child_embedded'], + $options['child_translated'], $options['child_groups'], ); @@ -193,14 +203,41 @@ private function getDataClass(FormInterface $form): string throw new \RuntimeException('Unable to get dataClass'); } + /** + * @param ChildOptions $baseChildOptions + * + * @return ChildOptions + */ + private function updateTranslatedChildOptions( + array $baseChildOptions, + TypeInfo $propTypeInfo, + \ReflectionProperty $refProperty, + ): array { + if (!$propTypeInfo instanceof TypeInfo\CollectionType) { + return []; + } + + dump($refProperty); + + return [ + 'child_type' => 'A2lix\TranslationFormBundle\Form\Type\TranslationsType', + 'translation_class' => $propTypeInfo->getCollectionValueType()->getClassName(), + 'required' => $propTypeInfo->isNullable(), + ...$baseChildOptions, + ]; + } /** * @param ChildOptions $baseChildOptions * * @return ChildOptions */ - private function updateChildOptions(array $baseChildOptions, TypeInfo $propTypeInfo, int $formLevel): array - { + private function updateEmbeddedChildOptions( + array $baseChildOptions, + TypeInfo $propTypeInfo, + \ReflectionProperty $refProperty, + int $formDepth + ): array { // TypeInfo matching native FormType? Abort, guessers are enough if (self::isTypeInfoWithMatchingNativeFormType($propTypeInfo)) { return $baseChildOptions; @@ -214,7 +251,7 @@ private function updateChildOptions(array $baseChildOptions, TypeInfo $propTypeI 'allow_delete' => true, 'delete_empty' => true, 'by_reference' => false, - 'prototype_name' => '__name'.$formLevel.'__', + 'prototype_name' => '__name'.$formDepth.'__', ...$baseChildOptions, ]; @@ -279,18 +316,18 @@ private static function isTypeInfoWithMatchingNativeFormType(TypeInfo $propTypeI /** * @param FormInterface $form */ - private function getFormLevel(FormInterface $form): int + private function getFormDepth(FormInterface $form): int { if ($form->isRoot()) { return 0; } - $level = 0; + $depth = 0; while (null !== $formParent = $form->getParent()) { $form = $formParent; - ++$level; + ++$depth; } - return $level; + return $depth; } } diff --git a/src/Form/Type/AutoType.php b/src/Form/Type/AutoType.php index b84c7c9..b6fd453 100644 --- a/src/Form/Type/AutoType.php +++ b/src/Form/Type/AutoType.php @@ -26,10 +26,13 @@ final class AutoType extends AbstractType { /** * @param list $globalExcludedChildren + * @param list $globalEmbeddedChildren */ public function __construct( private readonly AutoTypeBuilder $autoTypeBuilder, private readonly array $globalExcludedChildren = [], + private readonly array $globalEmbeddedChildren = [], + private readonly bool $globalTranslatedChildren = false, ) {} #[\Override] @@ -45,17 +48,38 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setDefaults([ 'children' => [], 'children_excluded' => $this->globalExcludedChildren, - 'children_embedded' => [], + 'children_embedded' => $this->globalEmbeddedChildren, + 'children_translated' => $this->globalTranslatedChildren, 'children_groups' => null, 'builder' => null, ]); - $resolver->setAllowedTypes('children_excluded', 'string[]|string'); - $resolver->setAllowedTypes('children_embedded', 'string[]|string'); + $resolver->setAllowedTypes('children_excluded', 'string[]|string|callable'); + $resolver->setInfo('children_excluded', 'An array of properties, the * wildcard, or a callable (mixed $previousValue): mixed'); + $resolver->addNormalizer('children_excluded', static function (Options $options, mixed $value): mixed { + if (is_callable($value)) { + return ($value)($options['children_excluded']); + } + + return $value; + }); + + $resolver->setAllowedTypes('children_embedded', 'string[]|string|callable'); + $resolver->setInfo('children_embedded', 'An array of properties, the * wildcard, or a callable (mixed $previousValue): mixed'); + $resolver->addNormalizer('children_embedded', static function (Options $options, mixed $value): mixed { + if (is_callable($value)) { + return ($value)($options['children_embedded']); + } + + return $value; + }); + + $resolver->setAllowedTypes('children_translated', 'bool'); $resolver->setAllowedTypes('children_groups', 'string[]|null'); $resolver->setAllowedTypes('builder', 'callable|null'); - $resolver->setInfo('builder', 'A callable that accepts two arguments (FormBuilderInterface $builder, string[] $classProperties). It should not return anything.'); + $resolver->setInfo('builder', 'A callable (FormBuilderInterface $builder, string[] $classProperties): void'); + // Others defaults FormType:class options $resolver->setNormalizer('data_class', static function (Options $options, ?string $value): string { if (null === $value) { throw new \RuntimeException('Missing "data_class" option of "AutoType".'); @@ -63,7 +87,6 @@ public function configureOptions(OptionsResolver $resolver): void return $value; }); - $resolver->setDefault('validation_groups', static function (Options $options): ?array { /** @var list|null */ return $options['children_groups']; diff --git a/src/Form/TypeGuesser/TypeInfoTypeGuesser.php b/src/Form/TypeGuesser/TypeInfoTypeGuesser.php index 0961581..38f05e6 100644 --- a/src/Form/TypeGuesser/TypeInfoTypeGuesser.php +++ b/src/Form/TypeGuesser/TypeInfoTypeGuesser.php @@ -37,6 +37,7 @@ public function guessType(string $class, string $property): ?TypeGuess // FormTypes handling 'multiple' option if ($typeInfo->isIdentifiedBy(TypeIdentifier::ARRAY)) { + dump($typeInfo); /** @var TypeInfo\CollectionType $typeInfo */ // @phpstan-ignore missingType.generics $collValueType = $typeInfo->getCollectionValueType();