From d6e53f5d4dee0aaeeaf6b1fb9fa0476a247a36ac Mon Sep 17 00:00:00 2001 From: aurelien Date: Mon, 20 Apr 2026 14:25:20 +0200 Subject: [PATCH 1/6] feat: update --- .github/workflows/ci.yml | 39 ++-- composer.json | 43 ++-- rector.php | 23 +- .../A2lixTranslationFormExtension.php | 4 +- ...octrineTranslationFieldsConfigProvider.php | 216 ++++++++++++++++++ .../EventListener/TranslationsListener.php | 38 ++- ...anslationFieldsConfigProviderInterface.php | 24 ++ src/Form/Type/TranslationsFormsType.php | 8 +- src/Locale/SimpleProvider.php | 4 +- src/Resources/config/a2lix_form.xml | 11 +- tests/Fixtures/Entity/ProductTranslation.php | 2 +- .../Type/TranslationsFormsTypeSimpleTest.php | 5 +- .../Type/TranslationsTypeAdvancedTest.php | 13 +- .../Form/Type/TranslationsTypeSimpleTest.php | 18 +- tests/Form/TypeTestCase.php | 118 ++++++++-- 15 files changed, 443 insertions(+), 123 deletions(-) create mode 100644 src/Form/Doctrine/DoctrineTranslationFieldsConfigProvider.php create mode 100644 src/Form/TranslationFieldsConfigProviderInterface.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a2bfff..c17588f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: name: Analyze runs-on: ubuntu-latest container: - image: php:8.3-alpine + image: php:8.5-alpine options: >- --tmpfs /tmp:exec --tmpfs /var/tmp:exec @@ -27,13 +27,13 @@ jobs: - uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-8.3-highest-${{ hashFiles('**/composer.json') }} + key: ${{ runner.os }}-composer-8.5-highest-${{ hashFiles('**/composer.json') }} restore-keys: | - ${{ runner.os }}-composer-8.3-highest + ${{ runner.os }}-composer-8.5-highest - name: Validate Composer run: composer validate - name: Install highest dependencies with Composer - run: composer update --no-progress --no-suggest --ansi + run: composer update --no-progress --ansi - name: Disable PHP memory limit run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini - name: Run CS-Fixer @@ -50,19 +50,10 @@ jobs: strategy: matrix: php: - - '8.1' - - '8.2' - - '8.3' + - '8.5' dependencies: - 'lowest' - 'highest' - include: - - php: '8.1' - phpunit-version: 10 - - php: '8.2' - phpunit-version: 10 - - php: '8.3' - phpunit-version: 10 fail-fast: false steps: - name: Checkout @@ -81,20 +72,18 @@ jobs: ${{ runner.os }}-composer-${{ matrix.php }}-${{ matrix.dependencies }} - name: Install lowest dependencies with Composer if: matrix.dependencies == 'lowest' - run: composer update --no-progress --no-suggest --prefer-stable --prefer-lowest --ansi + run: composer update --no-progress --prefer-stable --prefer-lowest --ansi - name: Install highest dependencies with Composer if: matrix.dependencies == 'highest' - run: composer update --no-progress --no-suggest --ansi + run: composer update --no-progress --ansi - name: Run tests with PHPUnit - env: - SYMFONY_MAX_PHPUNIT_VERSION: ${{ matrix.phpunit-version }} - run: vendor/bin/simple-phpunit --colors=always + run: vendor/bin/phpunit --colors=always coverage: - name: Coverage (PHP 8.3) + name: Coverage (PHP 8.5) runs-on: ubuntu-latest container: - image: php:8.3-alpine + image: php:8.5-alpine options: >- --tmpfs /tmp:exec --tmpfs /var/tmp:exec @@ -115,13 +104,13 @@ jobs: - uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-8.3-highest-${{ hashFiles('**/composer.json') }} + key: ${{ runner.os }}-composer-8.5-highest-${{ hashFiles('**/composer.json') }} restore-keys: | - ${{ runner.os }}-composer-8.3-highest + ${{ runner.os }}-composer-8.5-highest - name: Install highest dependencies with Composer - run: composer update --no-progress --no-suggest --ansi + run: composer update --no-progress --ansi - name: Run coverage with PHPUnit - run: vendor/bin/simple-phpunit --coverage-clover ./coverage.xml --colors=always + run: vendor/bin/phpunit --coverage-clover ./coverage.xml --colors=always - name: Send code coverage report to Codecov.io uses: codecov/codecov-action@v3 with: diff --git a/composer.json b/composer.json index 3a4c556..8e685f0 100644 --- a/composer.json +++ b/composer.json @@ -30,31 +30,34 @@ } ], "require": { - "php": "^8.1", - "a2lix/auto-form-bundle": "^0.4", + "php": "^8.5", + "a2lix/auto-form-bundle": "^1.0", "doctrine/dbal": "^4.0", + "doctrine/orm": "^3.0", "doctrine/persistence": "^4.0", - "symfony/config": "^5.4.30|^6.3|^7.0", - "symfony/dependency-injection": "^5.4.30|^6.3|^7.0", - "symfony/doctrine-bridge": "^7.3", - "symfony/event-dispatcher": "^5.4.30|^6.3|^7.0", - "symfony/form": "^5.4.30|^6.3|^7.0", - "symfony/http-foundation": "^5.4.30|^6.3|^7.0", - "symfony/http-kernel": "^5.4.30|^6.3|^7.0", - "symfony/options-resolver": "^5.4.30|^6.3|^7.0" + "phpdocumentor/reflection-docblock": "^5.6|^6.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/doctrine-bridge": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/options-resolver": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/type-info": "^7.4|^8.0" }, "require-dev": { - "doctrine/orm": "^3.0", "friendsofphp/php-cs-fixer": "^3.45", "kubawerlos/php-cs-fixer-custom-fixers": "^3.18", - "matthiasnoback/symfony-dependency-injection-test": "^5.0", - "phpstan/phpstan": "^1.10", - "rector/rector": "^0.18", - "symfony/cache": "^5.4.30|^6.3|^7.0", - "symfony/phpunit-bridge": "^5.4.30|^6.3|^7.0", - "symfony/validator": "^5.4.30|^6.3|^7.0", + "matthiasnoback/symfony-dependency-injection-test": "^6.0", + "phpstan/phpstan": "^2.0", + "rector/rector": "^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/phpunit-bridge": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", "toovalu-github/doctrine-behaviors": "^3.0", - "vimeo/psalm": "^5.18" + "vimeo/psalm": "^6.0" }, "suggest": { "knplabs/doctrine-behaviors": "For Knp strategy", @@ -68,7 +71,7 @@ "psalm" ], "phpunit": [ - "SYMFONY_DEPRECATIONS_HELPER=max[self]=0 simple-phpunit" + "SYMFONY_DEPRECATIONS_HELPER=max[self]=0 phpunit" ] }, "config": { @@ -82,7 +85,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "4.x-dev" } } } diff --git a/rector.php b/rector.php index e6244a4..1ac3d7e 100644 --- a/rector.php +++ b/rector.php @@ -3,14 +3,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; -use Rector\Core\ValueObject\PhpVersion; -use Rector\Doctrine\Set\DoctrineSetList; -use Rector\PHPUnit\Set\PHPUnitLevelSetList; -use Rector\PHPUnit\Set\PHPUnitSetList; -use Rector\Set\ValueObject\LevelSetList; -use Rector\Symfony\Set\SymfonyLevelSetList; -use Rector\Symfony\Set\SymfonySetList; -use Rector\Symfony\Set\TwigSetList; +use Rector\ValueObject\PhpVersion; return static function (RectorConfig $rectorConfig): void { $rectorConfig->parallel(); @@ -21,17 +14,5 @@ $rectorConfig->importNames(); $rectorConfig->importShortClasses(false); - $rectorConfig->phpVersion(PhpVersion::PHP_82); - $rectorConfig->sets([ - LevelSetList::UP_TO_PHP_82, - - DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, - // DoctrineSetList::DOCTRINE_CODE_QUALITY, - DoctrineSetList::DOCTRINE_ORM_214, - DoctrineSetList::DOCTRINE_DBAL_30, - - PHPUnitLevelSetList::UP_TO_PHPUNIT_91, - // PHPUnitSetList::PHPUNIT_CODE_QUALITY, - // PHPUnitSetList::PHPUNIT_YIELD_DATA_PROVIDER, - ]); + $rectorConfig->phpVersion(PhpVersion::PHP_85); }; diff --git a/src/DependencyInjection/A2lixTranslationFormExtension.php b/src/DependencyInjection/A2lixTranslationFormExtension.php index affd5ac..a63fb76 100644 --- a/src/DependencyInjection/A2lixTranslationFormExtension.php +++ b/src/DependencyInjection/A2lixTranslationFormExtension.php @@ -32,8 +32,8 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('a2lix_translation_form.locale_provider', $config['locale_provider']); $container->setParameter('a2lix_translation_form.locales', $config['locales']); $container->setParameter('a2lix_translation_form.required_locales', $config['required_locales']); - $container->setParameter('a2lix_translation_form.default_locale', $config['default_locale'] ?: - $container->getParameter('kernel.default_locale')); + $container->setParameter('a2lix_translation_form.default_locale', $config['default_locale'] + ?: $container->getParameter('kernel.default_locale')); $container->setParameter('a2lix_translation_form.templating', $config['templating']); } diff --git a/src/Form/Doctrine/DoctrineTranslationFieldsConfigProvider.php b/src/Form/Doctrine/DoctrineTranslationFieldsConfigProvider.php new file mode 100644 index 0000000..648c5d7 --- /dev/null +++ b/src/Form/Doctrine/DoctrineTranslationFieldsConfigProvider.php @@ -0,0 +1,216 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace A2lix\TranslationFormBundle\Form\Doctrine; + +use A2lix\AutoFormBundle\Form\Type\AutoType; +use A2lix\TranslationFormBundle\Form\TranslationFieldsConfigProviderInterface; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\FormInterface; + +/** + * Builds per-field options for {@see AutoType} from Doctrine ORM metadata (same role as + * AutoFormBundle 0.x DoctrineORMManipulator and DoctrineORMInfo). + */ +final class DoctrineTranslationFieldsConfigProvider implements TranslationFieldsConfigProviderInterface +{ + /** + * @param list $globalExcludedFields + */ + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly array $globalExcludedFields = ['id', 'locale', 'translatable'], + ) {} + + #[\Override] + public function getFieldsConfig(FormInterface $form): array + { + $class = $this->getDataClass($form); + $formOptions = $form->getConfig()->getOptions(); + + $objectFieldsConfig = $this->getObjectFieldsConfig($class); + $validObjectFieldsConfig = $this->filteringValidObjectFields($objectFieldsConfig, $formOptions['excluded_fields']); + + if (empty($formOptions['fields'])) { + return $validObjectFieldsConfig; + } + + $fields = []; + + foreach ($formOptions['fields'] as $formFieldName => $formFieldConfig) { + $this->checkFieldIsValid($formFieldName, $formFieldConfig, $validObjectFieldsConfig, $class); + + if (null === $formFieldConfig) { + continue; + } + + if (false === ($formFieldConfig['display'] ?? true)) { + continue; + } + + $fields[$formFieldName] = $this->normalizeLegacyFieldOptions($formFieldConfig); + + if (isset($validObjectFieldsConfig[$formFieldName])) { + $fields[$formFieldName] += $validObjectFieldsConfig[$formFieldName]; + } + } + + return $fields + $validObjectFieldsConfig; + } + + /** + * @return array> + */ + private function getObjectFieldsConfig(string $class): array + { + $fieldsConfig = []; + + $metadata = $this->entityManager->getClassMetadata($class); + + if (!empty($fields = $metadata->getFieldNames())) { + $fieldsConfig = array_fill_keys($fields, []); + } + + if (!empty($assocNames = $metadata->getAssociationNames())) { + $fieldsConfig += $this->getAssocsConfig($metadata, $assocNames); + } + + return $fieldsConfig; + } + + /** + * @param array> $objectFieldsConfig + * @param list $formExcludedFields + * + * @return array> + */ + private function filteringValidObjectFields(array $objectFieldsConfig, array $formExcludedFields): array + { + $excludedFields = array_merge($this->globalExcludedFields, $formExcludedFields); + + $validFields = []; + foreach ($objectFieldsConfig as $fieldName => $fieldConfig) { + if (\in_array($fieldName, $excludedFields, true)) { + continue; + } + + $validFields[$fieldName] = $fieldConfig; + } + + return $validFields; + } + + /** + * @param array> $validObjectFieldsConfig + */ + private function checkFieldIsValid(string $formFieldName, mixed $formFieldConfig, array $validObjectFieldsConfig, string $class): void + { + if (isset($validObjectFieldsConfig[$formFieldName])) { + return; + } + + if (false === ($formFieldConfig['mapped'] ?? true)) { + return; + } + + throw new \RuntimeException(\sprintf("Field '%s' doesn't exist in %s", $formFieldName, $class)); + } + + /** + * @param array $formFieldConfig + * + * @return array + */ + private function normalizeLegacyFieldOptions(array $formFieldConfig): array + { + if (isset($formFieldConfig['field_type']) && !isset($formFieldConfig['child_type'])) { + $formFieldConfig['child_type'] = $formFieldConfig['field_type']; + unset($formFieldConfig['field_type']); + } + + if (isset($formFieldConfig['entry_options']) && \is_array($formFieldConfig['entry_options'])) { + $formFieldConfig['entry_options'] = $this->normalizeLegacyFieldOptions($formFieldConfig['entry_options']); + } + + return $formFieldConfig; + } + + /** + * @param list $assocNames + * + * @return array> + */ + private function getAssocsConfig(ClassMetadata $metadata, array $assocNames): array + { + $assocsConfigs = []; + + foreach ($assocNames as $assocName) { + $associationMapping = $metadata->getAssociationMapping($assocName); + + if (isset($associationMapping['inversedBy'])) { + $assocsConfigs[$assocName] = []; + + continue; + } + + $class = $metadata->getAssociationTargetClass($assocName); + + if ($metadata->isSingleValuedAssociation($assocName)) { + $assocsConfigs[$assocName] = [ + 'child_type' => AutoType::class, + 'data_class' => $class, + 'required' => false, + ]; + + continue; + } + + $assocsConfigs[$assocName] = [ + 'child_type' => CollectionType::class, + 'entry_type' => AutoType::class, + 'entry_options' => [ + 'data_class' => $class, + ], + 'allow_add' => true, + 'by_reference' => false, + ]; + } + + return $assocsConfigs; + } + + private function getDataClass(FormInterface $form): string + { + if (null !== $dataClass = $form->getConfig()->getDataClass()) { + if (false === $pos = strrpos((string) $dataClass, '\\__CG__\\')) { + return $dataClass; + } + + return substr((string) $dataClass, $pos + 8); + } + + while (null !== $formParent = $form->getParent()) { + if (null === $dataClass = $formParent->getConfig()->getDataClass()) { + $form = $formParent; + + continue; + } + + return $this->entityManager->getClassMetadata($dataClass)->getAssociationTargetClass((string) $form->getPropertyPath()); + } + + throw new \RuntimeException('Unable to get dataClass'); + } +} diff --git a/src/Form/EventListener/TranslationsListener.php b/src/Form/EventListener/TranslationsListener.php index d200f6e..bb249ae 100644 --- a/src/Form/EventListener/TranslationsListener.php +++ b/src/Form/EventListener/TranslationsListener.php @@ -13,8 +13,8 @@ namespace A2lix\TranslationFormBundle\Form\EventListener; -use A2lix\AutoFormBundle\Form\Manipulator\FormManipulatorInterface; -use A2lix\AutoFormBundle\Form\Type\AutoFormType; +use A2lix\AutoFormBundle\Form\Type\AutoType; +use A2lix\TranslationFormBundle\Form\TranslationFieldsConfigProviderInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; @@ -23,7 +23,7 @@ class TranslationsListener implements EventSubscriberInterface { public function __construct( - private readonly FormManipulatorInterface $formManipulator, + private readonly TranslationFieldsConfigProviderInterface $fieldsConfigProvider, ) {} public static function getSubscribedEvents(): array @@ -51,13 +51,16 @@ public function preSetData(FormEvent $event): void continue; } - $form->add($locale, AutoFormType::class, [ + $form->add($locale, AutoType::class, [ 'data_class' => $translationClass, 'label' => $formOptions['locale_labels'][$locale] ?? null, 'required' => \in_array($locale, $formOptions['required_locales'], true), 'block_name' => ('field' === $formOptions['theming_granularity']) ? 'locale' : null, - 'fields' => $fieldsOptions[$locale], - 'excluded_fields' => $formOptions['excluded_fields'], + 'children' => array_map($this->normalizeLegacyFieldOptions(...), $fieldsOptions[$locale]), + 'children_excluded' => array_values(array_unique(array_merge( + ['id', 'locale', 'translatable'], + $formOptions['excluded_fields'], + ))), ]); } } @@ -87,7 +90,7 @@ public function getFieldsOptions(FormInterface $form, array $formOptions): array { $fieldsOptions = []; - $fieldsConfig = $this->formManipulator->getFieldsConfig($form); + $fieldsConfig = $this->fieldsConfigProvider->getFieldsConfig($form); foreach ($fieldsConfig as $fieldName => $fieldConfig) { // Simplest case: General options for all locales if (!isset($fieldConfig['locale_options'])) { @@ -105,7 +108,7 @@ public function getFieldsOptions(FormInterface $form, array $formOptions): array foreach ($formOptions['locales'] as $locale) { $localeFieldOptions = $localesFieldOptions[$locale] ?? []; if (!isset($localeFieldOptions['display']) || (true === $localeFieldOptions['display'])) { - $fieldsOptions[$locale][$fieldName] = $localeFieldOptions + $fieldConfig; + $fieldsOptions[$locale][$fieldName] = $this->normalizeLegacyFieldOptions($localeFieldOptions + $fieldConfig); } } } @@ -113,6 +116,25 @@ public function getFieldsOptions(FormInterface $form, array $formOptions): array return $fieldsOptions; } + /** + * @param array $options + * + * @return array + */ + private function normalizeLegacyFieldOptions(array $options): array + { + if (isset($options['field_type']) && !isset($options['child_type'])) { + $options['child_type'] = $options['field_type']; + unset($options['field_type']); + } + + if (isset($options['entry_options']) && \is_array($options['entry_options'])) { + $options['entry_options'] = $this->normalizeLegacyFieldOptions($options['entry_options']); + } + + return $options; + } + private function getTranslationClass(FormInterface $form): string { do { diff --git a/src/Form/TranslationFieldsConfigProviderInterface.php b/src/Form/TranslationFieldsConfigProviderInterface.php new file mode 100644 index 0000000..b12853f --- /dev/null +++ b/src/Form/TranslationFieldsConfigProviderInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace A2lix\TranslationFormBundle\Form; + +use Symfony\Component\Form\FormInterface; + +interface TranslationFieldsConfigProviderInterface +{ + /** + * @return array> + */ + public function getFieldsConfig(FormInterface $form): array; +} diff --git a/src/Form/Type/TranslationsFormsType.php b/src/Form/Type/TranslationsFormsType.php index 6e94959..ea7851e 100644 --- a/src/Form/Type/TranslationsFormsType.php +++ b/src/Form/Type/TranslationsFormsType.php @@ -13,7 +13,7 @@ namespace A2lix\TranslationFormBundle\Form\Type; -use A2lix\AutoFormBundle\Form\Type\AutoFormType; +use A2lix\AutoFormBundle\Form\Type\AutoType; use A2lix\TranslationFormBundle\Form\EventListener\TranslationsFormsListener; use A2lix\TranslationFormBundle\Locale\LocaleProviderInterface; use Doctrine\Common\Collections\ArrayCollection; @@ -56,9 +56,9 @@ public function configureOptions(OptionsResolver $resolver): void $resolver->setRequired('form_type'); $resolver->setNormalizer('form_options', static function (Options $options, $value): array { - // Check mandatory data_class option when AutoFormType use - if (($options['form_type'] instanceof AutoFormType) && !isset($value['data_class'])) { - throw new \RuntimeException('Missing "data_class" option under "form_options" of TranslationsFormsType. Required when "form_type" use "AutoFormType".'); + // Check mandatory data_class option when AutoType use + if (($options['form_type'] instanceof AutoType) && !isset($value['data_class'])) { + throw new \RuntimeException('Missing "data_class" option under "form_options" of TranslationsFormsType. Required when "form_type" use "AutoType".'); } return $value; diff --git a/src/Locale/SimpleProvider.php b/src/Locale/SimpleProvider.php index d25f881..5af5d90 100644 --- a/src/Locale/SimpleProvider.php +++ b/src/Locale/SimpleProvider.php @@ -22,10 +22,10 @@ public function __construct( ) { if (!\in_array($defaultLocale, $locales, true)) { if (\count($locales)) { - throw new \InvalidArgumentException(sprintf('Default locale `%s` not found within the configured locales `[%s]`. Perhaps you need to add it to your `a2lix_translation_form.locales` bundle configuration?', $defaultLocale, implode(',', $locales))); + throw new \InvalidArgumentException(\sprintf('Default locale `%s` not found within the configured locales `[%s]`. Perhaps you need to add it to your `a2lix_translation_form.locales` bundle configuration?', $defaultLocale, implode(',', $locales))); } - throw new \InvalidArgumentException(sprintf('No locales were configured, but expected at least the default locale `%s`. Perhaps you need to add it to your `a2lix_translation_form.locales` bundle configuration?', $defaultLocale)); + throw new \InvalidArgumentException(\sprintf('No locales were configured, but expected at least the default locale `%s`. Perhaps you need to add it to your `a2lix_translation_form.locales` bundle configuration?', $defaultLocale)); } if (array_diff($requiredLocales, $locales)) { diff --git a/src/Resources/config/a2lix_form.xml b/src/Resources/config/a2lix_form.xml index 6bd1b77..0903101 100644 --- a/src/Resources/config/a2lix_form.xml +++ b/src/Resources/config/a2lix_form.xml @@ -11,8 +11,17 @@ + + + + id + locale + translatable + + + - + diff --git a/tests/Fixtures/Entity/ProductTranslation.php b/tests/Fixtures/Entity/ProductTranslation.php index aecdd58..3cf4aea 100644 --- a/tests/Fixtures/Entity/ProductTranslation.php +++ b/tests/Fixtures/Entity/ProductTranslation.php @@ -16,7 +16,7 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Table(name: 'product_translations')] -#[ORM\UniqueConstraint(name: 'lookup_unique_idx', columns: ['locale', 'object_id'])] +#[ORM\UniqueConstraint(name: 'lookup_unique_idx', columns: ['locale', 'translatable_id'])] #[ORM\Entity] class ProductTranslation { diff --git a/tests/Form/Type/TranslationsFormsTypeSimpleTest.php b/tests/Form/Type/TranslationsFormsTypeSimpleTest.php index 59ee510..ffda8a6 100644 --- a/tests/Form/Type/TranslationsFormsTypeSimpleTest.php +++ b/tests/Form/Type/TranslationsFormsTypeSimpleTest.php @@ -18,6 +18,7 @@ use A2lix\TranslationFormBundle\Tests\Fixtures\Entity\Product; use A2lix\TranslationFormBundle\Tests\Fixtures\Form\MediaLocalizeType; use A2lix\TranslationFormBundle\Tests\Form\TypeTestCase; +use PHPUnit\Framework\Attributes\Depends; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\PreloadedExtension; @@ -113,9 +114,7 @@ public function testCreationForm(): Product return $product; } - /** - * @depends testCreationForm - */ + #[Depends('testCreationForm')] public function testEditionForm(Product $product): void { $product->getMedias()['en']->setUrl('http://ennnnn'); diff --git a/tests/Form/Type/TranslationsTypeAdvancedTest.php b/tests/Form/Type/TranslationsTypeAdvancedTest.php index d21746c..fa524db 100644 --- a/tests/Form/Type/TranslationsTypeAdvancedTest.php +++ b/tests/Form/Type/TranslationsTypeAdvancedTest.php @@ -95,12 +95,11 @@ public function testLabels(): void protected function getExtensions(): array { - $translationsType = $this->getConfiguredTranslationsType($this->locales, $this->defaultLocale, $this->requiredLocales); - $autoFormType = $this->getConfiguredAutoFormType(); - - return [new PreloadedExtension([ - $translationsType, - $autoFormType, - ], [])]; + return [ + ...$this->getFormExtensionsWithAutoType(), + new PreloadedExtension([ + $this->getConfiguredTranslationsType($this->locales, $this->defaultLocale, $this->requiredLocales), + ], []), + ]; } } diff --git a/tests/Form/Type/TranslationsTypeSimpleTest.php b/tests/Form/Type/TranslationsTypeSimpleTest.php index 63b480a..4cdd1c0 100644 --- a/tests/Form/Type/TranslationsTypeSimpleTest.php +++ b/tests/Form/Type/TranslationsTypeSimpleTest.php @@ -17,6 +17,7 @@ use A2lix\TranslationFormBundle\Tests\Fixtures\Entity\Product; use A2lix\TranslationFormBundle\Tests\Fixtures\Entity\ProductTranslation; use A2lix\TranslationFormBundle\Tests\Form\TypeTestCase; +use PHPUnit\Framework\Attributes\Depends; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\PreloadedExtension; @@ -108,9 +109,7 @@ public function testCreationForm(): Product return $product; } - /** - * @depends testCreationForm - */ + #[Depends('testCreationForm')] public function testEditionForm(Product $product): void { $product->getTranslations()['en']->setDescription('desc ennnnnnn'); @@ -155,12 +154,11 @@ public function testEditionForm(Product $product): void protected function getExtensions(): array { - $translationsType = $this->getConfiguredTranslationsType($this->locales, $this->defaultLocale, $this->requiredLocales); - $autoFormType = $this->getConfiguredAutoFormType(); - - return [new PreloadedExtension([ - $translationsType, - $autoFormType, - ], [])]; + return [ + ...$this->getFormExtensionsWithAutoType(), + new PreloadedExtension([ + $this->getConfiguredTranslationsType($this->locales, $this->defaultLocale, $this->requiredLocales), + ], []), + ]; } } diff --git a/tests/Form/TypeTestCase.php b/tests/Form/TypeTestCase.php index 040c11f..4082631 100644 --- a/tests/Form/TypeTestCase.php +++ b/tests/Form/TypeTestCase.php @@ -13,10 +13,10 @@ namespace A2lix\TranslationFormBundle\Tests\Form; -use A2lix\AutoFormBundle\Form\EventListener\AutoFormListener; -use A2lix\AutoFormBundle\Form\Manipulator\DoctrineORMManipulator; -use A2lix\AutoFormBundle\Form\Type\AutoFormType; -use A2lix\AutoFormBundle\ObjectInfo\DoctrineORMInfo; +use A2lix\AutoFormBundle\Form\Builder\AutoTypeBuilder; +use A2lix\AutoFormBundle\Form\Type\AutoType; +use A2lix\AutoFormBundle\Form\TypeGuesser\TypeInfoTypeGuesser; +use A2lix\TranslationFormBundle\Form\Doctrine\DoctrineTranslationFieldsConfigProvider; use A2lix\TranslationFormBundle\Form\EventListener\TranslationsFormsListener; use A2lix\TranslationFormBundle\Form\EventListener\TranslationsListener; use A2lix\TranslationFormBundle\Form\Type\TranslationsFormsType; @@ -24,19 +24,33 @@ use A2lix\TranslationFormBundle\Locale\SimpleProvider; use Doctrine\DBAL\DriverManager; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\ORMSetup; +use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\Persistence\ManagerRegistry; +use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; +use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension; use Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\Forms; +use Symfony\Component\Form\FormTypeGuesserChain; +use Symfony\Component\Form\PreloadedExtension; use Symfony\Component\Form\Test\TypeTestCase as BaseTypeTestCase; +use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Validator\ValidatorInterface; abstract class TypeTestCase extends BaseTypeTestCase { - protected ?DoctrineORMManipulator $doctrineORMManipulator = null; + private ?EntityManagerInterface $entityManager = null; + + private ?DoctrineTranslationFieldsConfigProvider $fieldsConfigProvider = null; protected function setUp(): void { @@ -66,30 +80,29 @@ protected function setUp(): void $this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory); } - protected function getDoctrineORMFormManipulator(): DoctrineORMManipulator + protected function getDoctrineTranslationFieldsConfigProvider(): DoctrineTranslationFieldsConfigProvider { - if (null !== $this->doctrineORMManipulator) { - return $this->doctrineORMManipulator; + if (null !== $this->fieldsConfigProvider) { + return $this->fieldsConfigProvider; } - $config = ORMSetup::createAttributeMetadataConfiguration([__DIR__.'/../Fixtures/Entity'], true); - $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config); - $entityManager = new EntityManager($connection, $config); - $doctrineORMInfo = new DoctrineORMInfo($entityManager->getMetadataFactory()); - - return $this->doctrineORMManipulator = new DoctrineORMManipulator($doctrineORMInfo, ['id', 'locale', 'translatable']); + return $this->fieldsConfigProvider = new DoctrineTranslationFieldsConfigProvider( + $this->getEntityManager(), + ['id', 'locale', 'translatable'] + ); } - protected function getConfiguredAutoFormType(): AutoFormType + protected function getConfiguredAutoFormType(): AutoType { - $autoFormListener = new AutoFormListener($this->getDoctrineORMFormManipulator()); - - return new AutoFormType($autoFormListener); + return new AutoType( + new AutoTypeBuilder($this->getPropertyInfoExtractor()), + ['id', 'locale', 'translatable'] + ); } protected function getConfiguredTranslationsType(array $locales, string $defaultLocale, array $requiredLocales): TranslationsType { - $translationsListener = new TranslationsListener($this->getDoctrineORMFormManipulator()); + $translationsListener = new TranslationsListener($this->getDoctrineTranslationFieldsConfigProvider()); $localProvider = new SimpleProvider($locales, $defaultLocale, $requiredLocales); return new TranslationsType($translationsListener, $localProvider); @@ -102,4 +115,71 @@ protected function getConfiguredTranslationsFormsType(array $locales, string $de return new TranslationsFormsType($translationsFormsListener, $localProvider); } + + protected function getFormExtensionsWithAutoType(): array + { + $managerRegistryStub = self::createStub(ManagerRegistry::class); + $managerRegistryStub + ->method('getManager') + ->willReturn($this->getEntityManager()) + ; + $managerRegistryStub + ->method('getManagers') + ->willReturn(['default' => $this->getEntityManager()]) + ; + + return [ + new DoctrineOrmExtension($managerRegistryStub), + new PreloadedExtension( + [$this->getConfiguredAutoFormType()], + [], + new FormTypeGuesserChain([ + new TypeInfoTypeGuesser(TypeResolver::create()), + ]), + ), + ]; + } + + private function getEntityManager(): EntityManagerInterface + { + if (null !== $this->entityManager) { + return $this->entityManager; + } + + $config = ORMSetup::createAttributeMetadataConfiguration([__DIR__.'/../Fixtures/Entity'], true); + if (\PHP_VERSION_ID >= 80400) { + $config->enableNativeLazyObjects(true); + } + + $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config); + + $this->entityManager = new EntityManager($connection, $config); + $tool = new SchemaTool($this->entityManager); + $tool->createSchema($this->entityManager->getMetadataFactory()->getAllMetadata()); + + return $this->entityManager; + } + + private function getPropertyInfoExtractor(): PropertyInfoExtractor + { + $doctrineExtractor = new DoctrineExtractor($this->getEntityManager()); + $reflectionExtractor = new ReflectionExtractor(); + + return new PropertyInfoExtractor( + listExtractors: [ + $reflectionExtractor, + $doctrineExtractor, + ], + typeExtractors: [ + $doctrineExtractor, + new PhpStanExtractor(), + new PhpDocExtractor(), + $reflectionExtractor, + ], + accessExtractors: [ + $doctrineExtractor, + $reflectionExtractor, + ] + ); + } } From 3253a539d447bcca9b31b89329e20e3465dcfa5f Mon Sep 17 00:00:00 2001 From: aurelien Date: Mon, 20 Apr 2026 14:31:00 +0200 Subject: [PATCH 2/6] fix: phpcs --- src/Form/Doctrine/DoctrineTranslationFieldsConfigProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Form/Doctrine/DoctrineTranslationFieldsConfigProvider.php b/src/Form/Doctrine/DoctrineTranslationFieldsConfigProvider.php index 648c5d7..0af27c4 100644 --- a/src/Form/Doctrine/DoctrineTranslationFieldsConfigProvider.php +++ b/src/Form/Doctrine/DoctrineTranslationFieldsConfigProvider.php @@ -194,7 +194,7 @@ private function getAssocsConfig(ClassMetadata $metadata, array $assocNames): ar private function getDataClass(FormInterface $form): string { if (null !== $dataClass = $form->getConfig()->getDataClass()) { - if (false === $pos = strrpos((string) $dataClass, '\\__CG__\\')) { + if (false === $pos = strrpos((string) $dataClass, '\__CG__\\')) { return $dataClass; } From 3cf6ee794aa0769d63193d9c64a3e16f27506f9d Mon Sep 17 00:00:00 2001 From: aurelien Date: Mon, 20 Apr 2026 14:38:06 +0200 Subject: [PATCH 3/6] fix phpunit --- .gitignore | 1 + phpunit.xml.dist | 53 ++++++++++++----------------- tests/Form/TypeTestCase.php | 12 ++----- tests/Locale/SimpleProviderTest.php | 18 ++++------ 4 files changed, 32 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 83edab6..223dbc0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .php-cs-fixer.cache psalm-phpqa.xml .phpunit.result.cache +.phpunit.cache/ composer.lock vendor/* diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b1de987..25a3c0a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,34 +1,25 @@ - - - - - - - - - - - - - src/ - - src/Resources - - - - - - - tests/ - tests/Fixtures - tests/tmp - - + + + + + + + + + + tests/ + tests/Fixtures + tests/tmp + + + + + src/ + + + src/Resources + + diff --git a/tests/Form/TypeTestCase.php b/tests/Form/TypeTestCase.php index 4082631..0dcf06f 100644 --- a/tests/Form/TypeTestCase.php +++ b/tests/Form/TypeTestCase.php @@ -54,12 +54,10 @@ abstract class TypeTestCase extends BaseTypeTestCase protected function setUp(): void { + $this->dispatcher = self::createStub(EventDispatcherInterface::class); parent::setUp(); - $validator = $this->getMockBuilder(ValidatorInterface::class) - ->disableOriginalConstructor() - ->getMock() - ; + $validator = self::createStub(ValidatorInterface::class); $validator->method('validate')->willReturn(new ConstraintViolationList()); $this->factory = Forms::createFormFactoryBuilder() @@ -68,15 +66,11 @@ protected function setUp(): void new FormTypeValidatorExtension($validator) ) ->addTypeGuesser( - $this->createMock(ValidatorTypeGuesser::class) + self::createStub(ValidatorTypeGuesser::class) ) ->getFormFactory() ; - $this->dispatcher = $this->getMockBuilder(EventDispatcherInterface::class) - ->disableOriginalConstructor() - ->getMock() - ; $this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory); } diff --git a/tests/Locale/SimpleProviderTest.php b/tests/Locale/SimpleProviderTest.php index 2016b6b..3ccf3f4 100644 --- a/tests/Locale/SimpleProviderTest.php +++ b/tests/Locale/SimpleProviderTest.php @@ -37,39 +37,33 @@ protected function setUp(): void public function testDefaultLocaleIsInLocales(): void { - // Get mock, without the constructor being called - $mock = $this->getMockBuilder(SimpleProvider::class) + $stub = self::getStubBuilder(SimpleProvider::class) ->disableOriginalConstructor() - ->getMock() + ->getStub() ; - // Set expectations for constructor calls $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('Default locale `de` not found within the configured locales `[es,en]`.' .' Perhaps you need to add it to your `a2lix_translation_form.locales` bundle configuration?'); - // Now call the constructor $reflectedClass = new \ReflectionClass(SimpleProvider::class); $constructor = $reflectedClass->getConstructor(); - $constructor->invoke($mock, ['es', 'en'], 'de', []); + $constructor->invoke($stub, ['es', 'en'], 'de', []); } public function testRequiredLocaleAreInLocales(): void { - // Get mock, without the constructor being called - $mock = $this->getMockBuilder(SimpleProvider::class) + $stub = self::getStubBuilder(SimpleProvider::class) ->disableOriginalConstructor() - ->getMock() + ->getStub() ; - // Set expectations for constructor calls $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('Required locales should be contained in locales'); - // Now call the constructor $reflectedClass = new \ReflectionClass(SimpleProvider::class); $constructor = $reflectedClass->getConstructor(); - $constructor->invoke($mock, ['es', 'en'], 'en', ['en', 'pt']); + $constructor->invoke($stub, ['es', 'en'], 'en', ['en', 'pt']); } public function testGetLocales(): void From 0714cc1ea25377325855b66ec6f9e3a4b6d154fd Mon Sep 17 00:00:00 2001 From: aurelien Date: Mon, 20 Apr 2026 14:47:02 +0200 Subject: [PATCH 4/6] fix --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 8e685f0..c955f35 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ "symfony/type-info": "^7.4|^8.0" }, "require-dev": { + "amphp/dns": "^2.1.2", "friendsofphp/php-cs-fixer": "^3.45", "kubawerlos/php-cs-fixer-custom-fixers": "^3.18", "matthiasnoback/symfony-dependency-injection-test": "^6.0", From cc9e74c44c69c90ffb9159b1b7794b87f53d984c Mon Sep 17 00:00:00 2001 From: aurelien Date: Mon, 20 Apr 2026 14:57:39 +0200 Subject: [PATCH 5/6] refactor: improve exception handling in SimpleProviderTest --- tests/Locale/SimpleProviderTest.php | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/tests/Locale/SimpleProviderTest.php b/tests/Locale/SimpleProviderTest.php index 3ccf3f4..347b0ab 100644 --- a/tests/Locale/SimpleProviderTest.php +++ b/tests/Locale/SimpleProviderTest.php @@ -14,6 +14,7 @@ namespace A2lix\TranslationFormBundle\Tests\Locale; use A2lix\TranslationFormBundle\Locale\SimpleProvider; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; /** @@ -37,33 +38,19 @@ protected function setUp(): void public function testDefaultLocaleIsInLocales(): void { - $stub = self::getStubBuilder(SimpleProvider::class) - ->disableOriginalConstructor() - ->getStub() - ; - - $this->expectException('InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Default locale `de` not found within the configured locales `[es,en]`.' .' Perhaps you need to add it to your `a2lix_translation_form.locales` bundle configuration?'); - $reflectedClass = new \ReflectionClass(SimpleProvider::class); - $constructor = $reflectedClass->getConstructor(); - $constructor->invoke($stub, ['es', 'en'], 'de', []); + new SimpleProvider(['es', 'en'], 'de', []); } public function testRequiredLocaleAreInLocales(): void { - $stub = self::getStubBuilder(SimpleProvider::class) - ->disableOriginalConstructor() - ->getStub() - ; - - $this->expectException('InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Required locales should be contained in locales'); - $reflectedClass = new \ReflectionClass(SimpleProvider::class); - $constructor = $reflectedClass->getConstructor(); - $constructor->invoke($stub, ['es', 'en'], 'en', ['en', 'pt']); + new SimpleProvider(['es', 'en'], 'en', ['en', 'pt']); } public function testGetLocales(): void From 1f4fb771af9c76b26dfa8b141aa7156f9373740d Mon Sep 17 00:00:00 2001 From: aurelien Date: Mon, 20 Apr 2026 14:59:35 +0200 Subject: [PATCH 6/6] refactor: use fully qualified class name for InvalidArgumentException in SimpleProviderTest --- tests/Locale/SimpleProviderTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/Locale/SimpleProviderTest.php b/tests/Locale/SimpleProviderTest.php index 347b0ab..47b3ec4 100644 --- a/tests/Locale/SimpleProviderTest.php +++ b/tests/Locale/SimpleProviderTest.php @@ -14,7 +14,6 @@ namespace A2lix\TranslationFormBundle\Tests\Locale; use A2lix\TranslationFormBundle\Locale\SimpleProvider; -use InvalidArgumentException; use PHPUnit\Framework\TestCase; /** @@ -38,7 +37,7 @@ protected function setUp(): void public function testDefaultLocaleIsInLocales(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Default locale `de` not found within the configured locales `[es,en]`.' .' Perhaps you need to add it to your `a2lix_translation_form.locales` bundle configuration?'); @@ -47,7 +46,7 @@ public function testDefaultLocaleIsInLocales(): void public function testRequiredLocaleAreInLocales(): void { - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Required locales should be contained in locales'); new SimpleProvider(['es', 'en'], 'en', ['en', 'pt']);