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/.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/composer.json b/composer.json index 3a4c556..c955f35 100644 --- a/composer.json +++ b/composer.json @@ -30,31 +30,35 @@ } ], "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", + "amphp/dns": "^2.1.2", "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 +72,7 @@ "psalm" ], "phpunit": [ - "SYMFONY_DEPRECATIONS_HELPER=max[self]=0 simple-phpunit" + "SYMFONY_DEPRECATIONS_HELPER=max[self]=0 phpunit" ] }, "config": { @@ -82,7 +86,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "4.x-dev" } } } 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/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..0af27c4 --- /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..0dcf06f 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,28 +24,40 @@ 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 { + $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() @@ -54,42 +66,37 @@ 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); } - 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 +109,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, + ] + ); + } } diff --git a/tests/Locale/SimpleProviderTest.php b/tests/Locale/SimpleProviderTest.php index 2016b6b..47b3ec4 100644 --- a/tests/Locale/SimpleProviderTest.php +++ b/tests/Locale/SimpleProviderTest.php @@ -37,39 +37,19 @@ protected function setUp(): void public function testDefaultLocaleIsInLocales(): void { - // Get mock, without the constructor being called - $mock = $this->getMockBuilder(SimpleProvider::class) - ->disableOriginalConstructor() - ->getMock() - ; - - // Set expectations for constructor calls - $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?'); - // Now call the constructor - $reflectedClass = new \ReflectionClass(SimpleProvider::class); - $constructor = $reflectedClass->getConstructor(); - $constructor->invoke($mock, ['es', 'en'], 'de', []); + new SimpleProvider(['es', 'en'], 'de', []); } public function testRequiredLocaleAreInLocales(): void { - // Get mock, without the constructor being called - $mock = $this->getMockBuilder(SimpleProvider::class) - ->disableOriginalConstructor() - ->getMock() - ; - - // Set expectations for constructor calls - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $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']); + new SimpleProvider(['es', 'en'], 'en', ['en', 'pt']); } public function testGetLocales(): void