├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── psalm.xml ├── rector.php ├── src ├── A2lixAutoFormBundle.php ├── DependencyInjection │ ├── A2lixAutoFormExtension.php │ └── Configuration.php ├── Form │ ├── EventListener │ │ └── AutoFormListener.php │ ├── Manipulator │ │ ├── DoctrineORMManipulator.php │ │ └── FormManipulatorInterface.php │ └── Type │ │ └── AutoFormType.php ├── ObjectInfo │ └── DoctrineORMInfo.php └── Resources │ └── config │ ├── a2lix_form.xml │ └── object_info.xml └── tests ├── Fixtures └── Entity │ ├── Media.php │ └── Product.php └── Form ├── Type ├── AutoFormTypeAdvancedTest.php └── AutoFormTypeSimpleTest.php └── TypeTestCase.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: ["push", "pull_request"] 4 | 5 | env: 6 | COMPOSER_ALLOW_SUPERUSER: '1' 7 | SYMFONY_DEPRECATIONS_HELPER: max[self]=0 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | container: 14 | image: php:8.3-alpine 15 | options: >- 16 | --tmpfs /tmp:exec 17 | --tmpfs /var/tmp:exec 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Install Composer 22 | run: wget -qO - https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet 23 | - name: Get Composer Cache Directory 24 | id: composer-cache 25 | run: | 26 | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 27 | - uses: actions/cache@v3 28 | with: 29 | path: ${{ steps.composer-cache.outputs.dir }} 30 | key: ${{ runner.os }}-composer-8.3-highest-${{ hashFiles('**/composer.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-composer-8.3-highest 33 | - name: Validate Composer 34 | run: composer validate 35 | - name: Install highest dependencies with Composer 36 | run: composer update --no-progress --no-suggest --ansi 37 | - name: Disable PHP memory limit 38 | run: echo 'memory_limit=-1' >> /usr/local/etc/php/php.ini 39 | - name: Run CS-Fixer 40 | run: vendor/bin/php-cs-fixer fix --dry-run --diff --format=checkstyle 41 | 42 | phpunit: 43 | name: PHPUnit (PHP ${{ matrix.php }} Deps ${{ matrix.dependencies }}) 44 | runs-on: ubuntu-latest 45 | container: 46 | image: php:${{ matrix.php }}-alpine 47 | options: >- 48 | --tmpfs /tmp:exec 49 | --tmpfs /var/tmp:exec 50 | strategy: 51 | matrix: 52 | php: 53 | - '8.1' 54 | - '8.2' 55 | - '8.3' 56 | dependencies: 57 | - 'lowest' 58 | - 'highest' 59 | include: 60 | - php: '8.1' 61 | phpunit-version: 10 62 | - php: '8.2' 63 | phpunit-version: 10 64 | - php: '8.3' 65 | phpunit-version: 10 66 | fail-fast: false 67 | steps: 68 | - name: Checkout 69 | uses: actions/checkout@v4 70 | - name: Install Composer 71 | run: wget -qO - https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet 72 | - name: Get Composer Cache Directory 73 | id: composer-cache 74 | run: | 75 | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 76 | - uses: actions/cache@v3 77 | with: 78 | path: ${{ steps.composer-cache.outputs.dir }} 79 | key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }} 80 | restore-keys: | 81 | ${{ runner.os }}-composer-${{ matrix.php }}-${{ matrix.dependencies }} 82 | - name: Install lowest dependencies with Composer 83 | if: matrix.dependencies == 'lowest' 84 | run: composer update --no-progress --no-suggest --prefer-stable --prefer-lowest --ansi 85 | - name: Install highest dependencies with Composer 86 | if: matrix.dependencies == 'highest' 87 | run: composer update --no-progress --no-suggest --ansi 88 | - name: Run tests with PHPUnit 89 | env: 90 | SYMFONY_MAX_PHPUNIT_VERSION: ${{ matrix.phpunit-version }} 91 | run: vendor/bin/simple-phpunit --colors=always 92 | 93 | # coverage: 94 | # name: Coverage (PHP 8.3) 95 | # runs-on: ubuntu-latest 96 | # container: 97 | # image: php:8.3-alpine 98 | # options: >- 99 | # --tmpfs /tmp:exec 100 | # --tmpfs /var/tmp:exec 101 | # steps: 102 | # - name: Checkout 103 | # uses: actions/checkout@v4 104 | # - name: Install pcov PHP extension 105 | # run: | 106 | # apk add $PHPIZE_DEPS 107 | # pecl install pcov 108 | # docker-php-ext-enable pcov 109 | # - name: Install Composer 110 | # run: wget -qO - https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet 111 | # - name: Get Composer Cache Directory 112 | # id: composer-cache 113 | # run: | 114 | # echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 115 | # - uses: actions/cache@v3 116 | # with: 117 | # path: ${{ steps.composer-cache.outputs.dir }} 118 | # key: ${{ runner.os }}-composer-8.3-highest-${{ hashFiles('**/composer.json') }} 119 | # restore-keys: | 120 | # ${{ runner.os }}-composer-8.3-highest 121 | # - name: Install highest dependencies with Composer 122 | # run: composer update --no-progress --no-suggest --ansi 123 | # - name: Run coverage with PHPUnit 124 | # run: vendor/bin/simple-phpunit --coverage-clover ./coverage.xml --colors=always 125 | # - name: Send code coverage report to Codecov.io 126 | # uses: codecov/codecov-action@v3 127 | # with: 128 | # token: ${{ secrets.CODECOV_TOKEN }} 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .php_cs.cache 2 | .php-cs-fixer.cache 3 | psalm-phpqa.xml 4 | .phpunit.result.cache 5 | composer.lock 6 | vendor/* 7 | 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | For the full copyright and license information, please view the LICENSE 9 | file that was distributed with this source code. 10 | HEADER; 11 | 12 | 13 | $finder = (new PhpCsFixer\Finder()) 14 | ->in(['src', 'tests']) 15 | ; 16 | 17 | return (new PhpCsFixer\Config()) 18 | ->setRiskyAllowed(true) 19 | ->registerCustomFixers(new PhpCsFixerCustomFixers\Fixers()) 20 | ->setRules([ 21 | '@PHP82Migration' => true, 22 | '@PhpCsFixer' => true, 23 | '@PhpCsFixer:risky' => true, 24 | 25 | // From https://github.com/symfony/demo/blob/main/.php-cs-fixer.dist.php 26 | 'linebreak_after_opening_tag' => true, 27 | // 'mb_str_functions' => true, 28 | 'no_php4_constructor' => true, 29 | 'no_unreachable_default_argument_value' => true, 30 | 'no_useless_else' => true, 31 | 'no_useless_return' => true, 32 | 'php_unit_strict' => false, 33 | 'php_unit_internal_class' => false, 34 | 'php_unit_test_class_requires_covers' => false, 35 | 'phpdoc_order' => true, 36 | 'strict_comparison' => true, 37 | 'strict_param' => true, 38 | 'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arrays', 'parameters']], 39 | 'statement_indentation' => true, 40 | 'method_chaining_indentation' => true, 41 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline', 'attribute_placement' => 'ignore'], 42 | 43 | PhpCsFixerCustomFixers\Fixer\ConstructorEmptyBracesFixer::name() => true, 44 | PhpCsFixerCustomFixers\Fixer\MultilineCommentOpeningClosingAloneFixer::name() => true, 45 | PhpCsFixerCustomFixers\Fixer\MultilinePromotedPropertiesFixer::name() => true, 46 | PhpCsFixerCustomFixers\Fixer\NoDuplicatedImportsFixer::name() => true, 47 | PhpCsFixerCustomFixers\Fixer\NoImportFromGlobalNamespaceFixer::name() => true, 48 | PhpCsFixerCustomFixers\Fixer\PhpdocSingleLineVarFixer::name() => true, 49 | ]) 50 | ->setFinder($finder) 51 | ; 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2020 David ALLIX 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A2lix Auto Form Bundle 2 | 3 | Automate form building. 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/a2lix/auto-form-bundle/v/stable)](https://packagist.org/packages/a2lix/auto-form-bundle) 6 | [![Latest Unstable Version](https://poser.pugx.org/a2lix/auto-form-bundle/v/unstable)](https://packagist.org/packages/a2lix/auto-form-bundle) 7 | [![License](https://poser.pugx.org/a2lix/auto-form-bundle/license)](https://packagist.org/packages/a2lix/auto-form-bundle) 8 | 9 | [![Total Downloads](https://poser.pugx.org/a2lix/auto-form-bundle/downloads)](https://packagist.org/packages/a2lix/auto-form-bundle) 10 | [![Monthly Downloads](https://poser.pugx.org/a2lix/auto-form-bundle/d/monthly)](https://packagist.org/packages/a2lix/auto-form-bundle) 11 | [![Daily Downloads](https://poser.pugx.org/a2lix/auto-form-bundle/d/daily)](https://packagist.org/packages/a2lix/auto-form-bundle) 12 | 13 | | Branch | Tools | 14 | | --- | --- | 15 | | master | [![Build Status][ci_badge]][ci_link] [![Coverage Status][coverage_badge]][coverage_link] | 16 | 17 | ## Installation 18 | 19 | Use composer: 20 | 21 | ```bash 22 | composer require a2lix/auto-form-bundle 23 | ``` 24 | 25 | After the successful installation, add/check the bundle registration: 26 | 27 | ```php 28 | // bundles.php is automatically updated if flex is installed. 29 | // ... 30 | A2lix\AutoFormBundle\A2lixAutoFormBundle::class => ['all' => true], 31 | // ... 32 | ``` 33 | 34 | ## Configuration 35 | 36 | There is no minimal configuration, so this part is optional. Full list: 37 | 38 | ```yaml 39 | # Create a dedicated a2lix.yaml in config/packages with: 40 | 41 | a2lix_auto_form: 42 | excluded_fields: [id, locale, translatable] # [1] 43 | ``` 44 | 45 | 1. Optional. 46 | 47 | ## Usage 48 | 49 | ### In a classic formType 50 | 51 | ```php 52 | use A2lix\AutoFormBundle\Form\Type\AutoFormType; 53 | ... 54 | $builder->add('medias', AutoFormType::class); 55 | ``` 56 | 57 | ### Advanced examples 58 | 59 | ```php 60 | use A2lix\AutoFormBundle\Form\Type\AutoFormType; 61 | ... 62 | $builder->add('medias', AutoFormType::class, [ 63 | 'fields' => [ // [2] 64 | 'description' => [ // [3.a] 65 | 'field_type' => 'textarea', // [4] 66 | 'label' => 'descript.', // [4] 67 | 'locale_options' => [ // [3.b] 68 | 'es' => ['label' => 'descripción'] // [4] 69 | 'fr' => ['display' => false] // [4] 70 | ] 71 | ] 72 | ], 73 | 'excluded_fields' => ['details'] // [2] 74 | ]); 75 | ``` 76 | 77 | 2. Optional. If set, override the default value from config.yml 78 | 3. Optional. If set, override the auto configuration of fields 79 | - [3.a] Optional. - For a field, applied to all locales 80 | - [3.b] Optional. - For a specific locale of a field 81 | 4. Optional. Common options of symfony forms (max_length, required, trim, read_only, constraints, ...), which was added 'field_type' and 'display' 82 | 83 | ## Additional 84 | 85 | ### Example 86 | 87 | See [Demo Bundle](https://github.com/a2lix/Demo) for more examples. 88 | 89 | ## Contribution help 90 | 91 | ``` 92 | docker run --rm --interactive --tty --volume $PWD:/app --user $(id -u):$(id -g) composer install --ignore-platform-reqs 93 | docker run --rm --interactive --tty --volume $PWD:/app --user $(id -u):$(id -g) composer run-script phpunit 94 | docker run --rm --interactive --tty --volume $PWD:/app --user $(id -u):$(id -g) composer run-script cs-fixer 95 | ``` 96 | 97 | ## License 98 | 99 | This package is available under the [MIT license](LICENSE). 100 | 101 | [ci_badge]: https://github.com/a2lix/AutoFormBundle/actions/workflows/ci.yml/badge.svg 102 | [ci_link]: https://github.com/a2lix/AutoFormBundle/actions/workflows/ci.yml 103 | [coverage_badge]: https://codecov.io/gh/a2lix/AutoFormBundle/branch/master/graph/badge.svg 104 | [coverage_link]: https://codecov.io/gh/a2lix/AutoFormBundle/branch/master 105 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a2lix/auto-form-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Automate form building", 5 | "keywords": ["symfony", "form", "field", "automate", "automation", "magic", "building"], 6 | "homepage": "https://github.com/a2lix/AutoFormBundle", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "David ALLIX", 11 | "homepage": "http://a2lix.fr" 12 | }, 13 | { 14 | "name": "Contributors", 15 | "homepage": "https://github.com/a2lix/AutoFormBundle/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "doctrine/persistence": "^2.0|^3.0|^4.0", 21 | "symfony/config": "^5.4.30|^6.3|^7.0", 22 | "symfony/dependency-injection": "^5.4.30|^6.3|^7.0", 23 | "symfony/doctrine-bridge": "^5.4.30|^6.3|^7.0", 24 | "symfony/form": "^5.4.30|^6.3|^7.0", 25 | "symfony/http-kernel": "^5.4.30|^6.3|^7.0" 26 | }, 27 | "require-dev": { 28 | "doctrine/orm": "^2.15|^3.0", 29 | "friendsofphp/php-cs-fixer": "^3.45", 30 | "kubawerlos/php-cs-fixer-custom-fixers": "^3.18", 31 | "phpstan/phpstan": "^1.10", 32 | "rector/rector": "^0.18", 33 | "symfony/cache": "^5.4.30|^6.3|^7.0", 34 | "symfony/phpunit-bridge": "^5.4.30|^6.3|^7.0", 35 | "symfony/validator": "^5.4.30|^6.3|^7.0", 36 | "vimeo/psalm": "^5.18" 37 | }, 38 | "suggest": { 39 | "a2lix/translation-form-bundle": "For translation form" 40 | }, 41 | "scripts": { 42 | "cs-fixer": [ 43 | "php-cs-fixer fix --verbose" 44 | ], 45 | "psalm": [ 46 | "psalm" 47 | ], 48 | "phpunit": [ 49 | "SYMFONY_DEPRECATIONS_HELPER=max[self]=0 simple-phpunit" 50 | ] 51 | }, 52 | "config": { 53 | "sort-packages": true, 54 | "allow-plugins": { 55 | "composer/package-versions-deprecated": true 56 | } 57 | }, 58 | "autoload": { 59 | "psr-4": { "A2lix\\AutoFormBundle\\": "src/" } 60 | }, 61 | "autoload-dev": { 62 | "psr-4": { "A2lix\\AutoFormBundle\\Tests\\": "tests/" } 63 | }, 64 | "extra": { 65 | "branch-alias": { 66 | "dev-master": "0.x-dev" 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | src/ 13 | 14 | 15 | src/Resources 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | tests/ 31 | tests/Fixtures 32 | tests/tmp 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | parallel(); 17 | $rectorConfig->paths([ 18 | __DIR__.'/src', 19 | __DIR__.'/tests', 20 | ]); 21 | $rectorConfig->importNames(); 22 | $rectorConfig->importShortClasses(false); 23 | 24 | $rectorConfig->phpVersion(PhpVersion::PHP_82); 25 | $rectorConfig->sets([ 26 | LevelSetList::UP_TO_PHP_82, 27 | 28 | DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, 29 | // DoctrineSetList::DOCTRINE_CODE_QUALITY, 30 | DoctrineSetList::DOCTRINE_ORM_214, 31 | DoctrineSetList::DOCTRINE_DBAL_30, 32 | 33 | PHPUnitLevelSetList::UP_TO_PHPUNIT_91, 34 | // PHPUnitSetList::PHPUNIT_CODE_QUALITY, 35 | // PHPUnitSetList::PHPUNIT_YIELD_DATA_PROVIDER, 36 | ]); 37 | }; 38 | -------------------------------------------------------------------------------- /src/A2lixAutoFormBundle.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle; 15 | 16 | use Symfony\Component\HttpKernel\Bundle\Bundle; 17 | 18 | class A2lixAutoFormBundle extends Bundle {} 19 | -------------------------------------------------------------------------------- /src/DependencyInjection/A2lixAutoFormExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\DependencyInjection; 15 | 16 | use Symfony\Component\Config\Definition\Processor; 17 | use Symfony\Component\Config\FileLocator; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 20 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 21 | 22 | class A2lixAutoFormExtension extends Extension 23 | { 24 | public function load(array $configs, ContainerBuilder $container): void 25 | { 26 | $processor = new Processor(); 27 | $config = $processor->processConfiguration(new Configuration(), $configs); 28 | 29 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 30 | $loader->load('a2lix_form.xml'); 31 | $loader->load('object_info.xml'); 32 | 33 | $definition = $container->getDefinition('a2lix_auto_form.form.manipulator.doctrine_orm_manipulator'); 34 | $definition->replaceArgument(1, $config['excluded_fields']); 35 | 36 | $container->setAlias('a2lix_auto_form.manipulator.default', 'a2lix_auto_form.form.manipulator.doctrine_orm_manipulator'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\DependencyInjection; 15 | 16 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 17 | use Symfony\Component\Config\Definition\ConfigurationInterface; 18 | 19 | class Configuration implements ConfigurationInterface 20 | { 21 | public function getConfigTreeBuilder(): TreeBuilder 22 | { 23 | $treeBuilder = new TreeBuilder('a2lix_auto_form'); 24 | $rootNode = method_exists(TreeBuilder::class, 'getRootNode') ? $treeBuilder->getRootNode() : $treeBuilder->root('a2lix_auto_form'); 25 | 26 | $rootNode 27 | ->children() 28 | ->arrayNode('excluded_fields') 29 | ->defaultValue(['id', 'locale', 'translatable']) 30 | ->beforeNormalization() 31 | ->ifString() 32 | ->then(static fn ($v) => preg_split('/\s*,\s*/', (string) $v)) 33 | ->end() 34 | ->prototype('scalar') 35 | ->info('Global list of fields to exclude from form generation. (Default: id, locale, translatable)')->end() 36 | ->end() 37 | ->end() 38 | ; 39 | 40 | return $treeBuilder; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Form/EventListener/AutoFormListener.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\Form\EventListener; 15 | 16 | use A2lix\AutoFormBundle\Form\Manipulator\FormManipulatorInterface; 17 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 18 | use Symfony\Component\Form\FormEvent; 19 | use Symfony\Component\Form\FormEvents; 20 | 21 | class AutoFormListener implements EventSubscriberInterface 22 | { 23 | public function __construct( 24 | private readonly FormManipulatorInterface $formManipulator, 25 | ) {} 26 | 27 | public static function getSubscribedEvents(): array 28 | { 29 | return [ 30 | FormEvents::PRE_SET_DATA => 'preSetData', 31 | ]; 32 | } 33 | 34 | public function preSetData(FormEvent $event): void 35 | { 36 | $form = $event->getForm(); 37 | 38 | $fieldsOptions = $this->formManipulator->getFieldsConfig($form); 39 | foreach ($fieldsOptions as $fieldName => $fieldConfig) { 40 | $fieldType = $fieldConfig['field_type'] ?? null; 41 | unset($fieldConfig['field_type']); 42 | 43 | $form->add($fieldName, $fieldType, $fieldConfig); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Form/Manipulator/DoctrineORMManipulator.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\Form\Manipulator; 15 | 16 | use A2lix\AutoFormBundle\ObjectInfo\DoctrineORMInfo; 17 | use Symfony\Component\Form\FormInterface; 18 | 19 | class DoctrineORMManipulator implements FormManipulatorInterface 20 | { 21 | public function __construct( 22 | private readonly DoctrineORMInfo $doctrineORMInfo, 23 | private readonly array $globalExcludedFields = [], 24 | ) {} 25 | 26 | public function getFieldsConfig(FormInterface $form): array 27 | { 28 | $class = $this->getDataClass($form); 29 | $formOptions = $form->getConfig()->getOptions(); 30 | 31 | // Filtering to remove excludedFields 32 | $objectFieldsConfig = $this->doctrineORMInfo->getFieldsConfig($class); 33 | $validObjectFieldsConfig = $this->filteringValidObjectFields($objectFieldsConfig, $formOptions['excluded_fields']); 34 | 35 | if (empty($formOptions['fields'])) { 36 | return $validObjectFieldsConfig; 37 | } 38 | 39 | $fields = []; 40 | 41 | foreach ($formOptions['fields'] as $formFieldName => $formFieldConfig) { 42 | $this->checkFieldIsValid($formFieldName, $formFieldConfig, $validObjectFieldsConfig, $class); 43 | 44 | if (null === $formFieldConfig) { 45 | continue; 46 | } 47 | 48 | // If display undesired, remove 49 | if (false === ($formFieldConfig['display'] ?? true)) { 50 | continue; 51 | } 52 | 53 | // Override with formFieldsConfig priority 54 | $fields[$formFieldName] = $formFieldConfig; 55 | 56 | if (isset($validObjectFieldsConfig[$formFieldName])) { 57 | $fields[$formFieldName] += $validObjectFieldsConfig[$formFieldName]; 58 | } 59 | } 60 | 61 | return $fields + $validObjectFieldsConfig; 62 | } 63 | 64 | private function getDataClass(FormInterface $form): string 65 | { 66 | // Simple case, data_class from current form (with ORM Proxy management) 67 | if (null !== $dataClass = $form->getConfig()->getDataClass()) { 68 | if (false === $pos = strrpos((string) $dataClass, '\\__CG__\\')) { 69 | return $dataClass; 70 | } 71 | 72 | return substr((string) $dataClass, $pos + 8); 73 | } 74 | 75 | // Advanced case, loop parent form to get closest fill data_class 76 | while (null !== $formParent = $form->getParent()) { 77 | if (null === $dataClass = $formParent->getConfig()->getDataClass()) { 78 | $form = $formParent; 79 | 80 | continue; 81 | } 82 | 83 | return $this->doctrineORMInfo->getAssociationTargetClass($dataClass, (string) $form->getPropertyPath()); 84 | } 85 | 86 | throw new \RuntimeException('Unable to get dataClass'); 87 | } 88 | 89 | private function filteringValidObjectFields(array $objectFieldsConfig, array $formExcludedFields): array 90 | { 91 | $excludedFields = array_merge($this->globalExcludedFields, $formExcludedFields); 92 | 93 | $validFields = []; 94 | foreach ($objectFieldsConfig as $fieldName => $fieldConfig) { 95 | if (\in_array($fieldName, $excludedFields, true)) { 96 | continue; 97 | } 98 | 99 | $validFields[$fieldName] = $fieldConfig; 100 | } 101 | 102 | return $validFields; 103 | } 104 | 105 | private function checkFieldIsValid($formFieldName, $formFieldConfig, $validObjectFieldsConfig, $class): void 106 | { 107 | if (isset($validObjectFieldsConfig[$formFieldName])) { 108 | return; 109 | } 110 | 111 | if (false === ($formFieldConfig['mapped'] ?? true)) { 112 | return; 113 | } 114 | 115 | throw new \RuntimeException(sprintf("Field '%s' doesn't exist in %s", $formFieldName, $class)); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Form/Manipulator/FormManipulatorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\Form\Manipulator; 15 | 16 | use Symfony\Component\Form\FormInterface; 17 | 18 | interface FormManipulatorInterface 19 | { 20 | public function getFieldsConfig(FormInterface $form): array; 21 | } 22 | -------------------------------------------------------------------------------- /src/Form/Type/AutoFormType.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\Form\Type; 15 | 16 | use A2lix\AutoFormBundle\Form\EventListener\AutoFormListener; 17 | use Symfony\Component\Form\AbstractType; 18 | use Symfony\Component\Form\FormBuilderInterface; 19 | use Symfony\Component\OptionsResolver\Options; 20 | use Symfony\Component\OptionsResolver\OptionsResolver; 21 | 22 | class AutoFormType extends AbstractType 23 | { 24 | public function __construct( 25 | private readonly AutoFormListener $autoFormListener, 26 | ) {} 27 | 28 | public function buildForm(FormBuilderInterface $builder, array $options): void 29 | { 30 | $builder->addEventSubscriber($this->autoFormListener); 31 | } 32 | 33 | public function configureOptions(OptionsResolver $resolver): void 34 | { 35 | $resolver->setDefaults([ 36 | 'fields' => [], 37 | 'excluded_fields' => [], 38 | ]); 39 | 40 | $resolver->setNormalizer('data_class', static function (Options $options, $value): string { 41 | if (empty($value)) { 42 | throw new \RuntimeException('Missing "data_class" option of "AutoFormType".'); 43 | } 44 | 45 | return $value; 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ObjectInfo/DoctrineORMInfo.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\ObjectInfo; 15 | 16 | use A2lix\AutoFormBundle\Form\Type\AutoFormType; 17 | use Doctrine\Persistence\Mapping\ClassMetadata; 18 | use Doctrine\Persistence\Mapping\ClassMetadataFactory; 19 | use Symfony\Component\Form\Extension\Core\Type\CollectionType; 20 | 21 | class DoctrineORMInfo 22 | { 23 | public function __construct( 24 | private readonly ClassMetadataFactory $classMetadataFactory, 25 | ) {} 26 | 27 | public function getFieldsConfig(string $class): array 28 | { 29 | $fieldsConfig = []; 30 | 31 | $metadata = $this->classMetadataFactory->getMetadataFor($class); 32 | 33 | if (!empty($fields = $metadata->getFieldNames())) { 34 | $fieldsConfig = array_fill_keys($fields, []); 35 | } 36 | 37 | if (!empty($assocNames = $metadata->getAssociationNames())) { 38 | $fieldsConfig += $this->getAssocsConfig($metadata, $assocNames); 39 | } 40 | 41 | return $fieldsConfig; 42 | } 43 | 44 | public function getAssociationTargetClass(string $class, string $fieldName): string 45 | { 46 | $metadata = $this->classMetadataFactory->getMetadataFor($class); 47 | 48 | if (!$metadata->hasAssociation($fieldName)) { 49 | throw new \RuntimeException(sprintf('Unable to find the association target class of "%s" in %s.', $fieldName, $class)); 50 | } 51 | 52 | return $metadata->getAssociationTargetClass($fieldName); 53 | } 54 | 55 | private function getAssocsConfig(ClassMetadata $metadata, array $assocNames): array 56 | { 57 | $assocsConfigs = []; 58 | 59 | foreach ($assocNames as $assocName) { 60 | $associationMapping = $metadata->getAssociationMapping($assocName); 61 | 62 | if (isset($associationMapping['inversedBy'])) { 63 | $assocsConfigs[$assocName] = []; 64 | 65 | continue; 66 | } 67 | 68 | $class = $metadata->getAssociationTargetClass($assocName); 69 | 70 | if ($metadata->isSingleValuedAssociation($assocName)) { 71 | $assocsConfigs[$assocName] = [ 72 | 'field_type' => AutoFormType::class, 73 | 'data_class' => $class, 74 | 'required' => false, 75 | ]; 76 | 77 | continue; 78 | } 79 | 80 | $assocsConfigs[$assocName] = [ 81 | 'field_type' => CollectionType::class, 82 | 'entry_type' => AutoFormType::class, 83 | 'entry_options' => [ 84 | 'data_class' => $class, 85 | ], 86 | 'allow_add' => true, 87 | 'by_reference' => false, 88 | ]; 89 | } 90 | 91 | return $assocsConfigs; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Resources/config/a2lix_form.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Resources/config/object_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/Fixtures/Entity/Media.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\Tests\Fixtures\Entity; 15 | 16 | use Doctrine\ORM\Mapping as ORM; 17 | 18 | #[ORM\Entity] 19 | class Media 20 | { 21 | #[ORM\Id] 22 | #[ORM\Column(type: 'integer')] 23 | #[ORM\GeneratedValue(strategy: 'AUTO')] 24 | private ?int $id = null; 25 | 26 | #[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'medias')] 27 | private Product $product; 28 | 29 | #[ORM\Column(nullable: true)] 30 | private ?string $url = null; 31 | 32 | #[ORM\Column(nullable: true)] 33 | private ?string $description = null; 34 | 35 | public function getId(): ?int 36 | { 37 | return $this->id; 38 | } 39 | 40 | public function getProduct(): Product 41 | { 42 | return $this->product; 43 | } 44 | 45 | public function setProduct(Product $product): self 46 | { 47 | $this->product = $product; 48 | 49 | return $this; 50 | } 51 | 52 | public function getUrl(): ?string 53 | { 54 | return $this->url; 55 | } 56 | 57 | public function setUrl(?string $url): self 58 | { 59 | $this->url = $url; 60 | 61 | return $this; 62 | } 63 | 64 | public function getDescription(): ?string 65 | { 66 | return $this->description; 67 | } 68 | 69 | public function setDescription(?string $description): self 70 | { 71 | $this->description = $description; 72 | 73 | return $this; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Fixtures/Entity/Product.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\Tests\Fixtures\Entity; 15 | 16 | use Doctrine\Common\Collections\ArrayCollection; 17 | use Doctrine\Common\Collections\Collection; 18 | use Doctrine\ORM\Mapping as ORM; 19 | 20 | #[ORM\Entity] 21 | class Product 22 | { 23 | #[ORM\Id] 24 | #[ORM\Column(type: 'integer')] 25 | #[ORM\GeneratedValue(strategy: 'AUTO')] 26 | private ?int $id = null; 27 | 28 | #[ORM\Column(nullable: true)] 29 | private ?string $title = null; 30 | 31 | #[ORM\Column(type: 'text', nullable: true)] 32 | private ?string $description = null; 33 | 34 | #[ORM\Column(nullable: true)] 35 | private ?string $url = null; 36 | 37 | #[ORM\ManyToOne(targetEntity: Media::class)] 38 | private Media $mainMedia; 39 | 40 | #[ORM\OneToMany(targetEntity: Media::class, mappedBy: 'product', cascade: ['all'], orphanRemoval: true)] 41 | private ArrayCollection $medias; 42 | 43 | public function __construct() 44 | { 45 | $this->medias = new ArrayCollection(); 46 | } 47 | 48 | public function getId(): ?int 49 | { 50 | return $this->id; 51 | } 52 | 53 | public function getTitle(): ?string 54 | { 55 | return $this->title; 56 | } 57 | 58 | public function setTitle(?string $title): self 59 | { 60 | $this->title = $title; 61 | 62 | return $this; 63 | } 64 | 65 | public function getDescription(): ?string 66 | { 67 | return $this->description; 68 | } 69 | 70 | public function setDescription(?string $description): self 71 | { 72 | $this->description = $description; 73 | 74 | return $this; 75 | } 76 | 77 | public function getUrl(): ?string 78 | { 79 | return $this->url; 80 | } 81 | 82 | public function setUrl(?string $url): self 83 | { 84 | $this->url = $url; 85 | 86 | return $this; 87 | } 88 | 89 | public function getMainMedia(): ?Media 90 | { 91 | return $this->mainMedia; 92 | } 93 | 94 | public function setMainMedia(?Media $mainMedia): self 95 | { 96 | $this->mainMedia = $mainMedia; 97 | 98 | return $this; 99 | } 100 | 101 | public function getMedias(): Collection 102 | { 103 | return $this->medias; 104 | } 105 | 106 | public function addMedia(Media $media): self 107 | { 108 | if (!$this->medias->contains($media)) { 109 | $media->setProduct($this); 110 | $this->medias->add($media); 111 | } 112 | 113 | return $this; 114 | } 115 | 116 | public function removeMedia(Media $media): self 117 | { 118 | $this->medias->removeElement($media); 119 | 120 | return $this; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/Form/Type/AutoFormTypeAdvancedTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\Tests\Form\Type; 15 | 16 | use A2lix\AutoFormBundle\Form\Type\AutoFormType; 17 | use A2lix\AutoFormBundle\Tests\Fixtures\Entity\Media; 18 | use A2lix\AutoFormBundle\Tests\Fixtures\Entity\Product; 19 | use A2lix\AutoFormBundle\Tests\Form\TypeTestCase; 20 | use Symfony\Component\Form\Extension\Core\Type\SubmitType; 21 | use Symfony\Component\Form\PreloadedExtension; 22 | 23 | /** 24 | * @internal 25 | */ 26 | final class AutoFormTypeAdvancedTest extends TypeTestCase 27 | { 28 | public function testCreationFormWithOverriddenFieldsLabel(): Product 29 | { 30 | $form = $this->factory->createBuilder(AutoFormType::class, new Product(), [ 31 | 'fields' => [ 32 | 'mainMedia' => [ 33 | 'label' => 'Main Media', 34 | ], 35 | 'url' => [ 36 | 'label' => 'URL/URI', 37 | ], 38 | ], 39 | ]) 40 | ->add('create', SubmitType::class) 41 | ->getForm() 42 | ; 43 | 44 | $media1 = new Media(); 45 | $media1->setUrl('http://example.org/media1') 46 | ->setDescription('media1 desc') 47 | ; 48 | $media2 = new Media(); 49 | $media2->setUrl('http://example.org/media2') 50 | ->setDescription('media2 desc') 51 | ; 52 | $media3 = new Media(); 53 | $media3->setUrl('http://example.org/media3') 54 | ->setDescription('media3 desc') 55 | ; 56 | 57 | $product = new Product(); 58 | $product 59 | ->setUrl('a2lix.fr') 60 | ->setMainMedia($media3) 61 | ->addMedia($media1) 62 | ->addMedia($media2) 63 | ; 64 | 65 | $formData = [ 66 | 'url' => 'a2lix.fr', 67 | 'mainMedia' => [ 68 | 'url' => 'http://example.org/media3', 69 | 'description' => 'media3 desc', 70 | ], 71 | 'medias' => [ 72 | [ 73 | 'url' => 'http://example.org/media1', 74 | 'description' => 'media1 desc', 75 | ], 76 | [ 77 | 'url' => 'http://example.org/media2', 78 | 'description' => 'media2 desc', 79 | ], 80 | ], 81 | ]; 82 | 83 | $form->submit($formData); 84 | self::assertTrue($form->isSynchronized()); 85 | self::assertEquals($product, $form->getData()); 86 | self::assertEquals('URL/URI', $form->get('url')->getConfig()->getOptions()['label']); 87 | 88 | return $product; 89 | } 90 | 91 | public function testCreationFormWithOverriddenFieldsMappedFalse(): Product 92 | { 93 | $form = $this->factory->createBuilder(AutoFormType::class, new Product(), [ 94 | 'fields' => [ 95 | 'color' => [ 96 | 'mapped' => false, 97 | ], 98 | ], 99 | ]) 100 | ->add('create', SubmitType::class) 101 | ->getForm() 102 | ; 103 | 104 | $media1 = new Media(); 105 | $media1->setUrl('http://example.org/media1') 106 | ->setDescription('media1 desc') 107 | ; 108 | $media2 = new Media(); 109 | $media2->setUrl('http://example.org/media2') 110 | ->setDescription('media2 desc') 111 | ; 112 | 113 | $product = new Product(); 114 | $product->setUrl('a2lix.fr') 115 | ->addMedia($media1) 116 | ->addMedia($media2) 117 | ; 118 | 119 | $formData = [ 120 | 'url' => 'a2lix.fr', 121 | 'color' => 'blue', 122 | 'medias' => [ 123 | [ 124 | 'url' => 'http://example.org/media1', 125 | 'description' => 'media1 desc', 126 | ], 127 | [ 128 | 'url' => 'http://example.org/media2', 129 | 'description' => 'media2 desc', 130 | ], 131 | ], 132 | ]; 133 | 134 | $form->submit($formData); 135 | self::assertTrue($form->isSynchronized()); 136 | self::assertEquals($product, $form->getData()); 137 | self::assertEquals('blue', $form->get('color')->getData()); 138 | 139 | return $product; 140 | } 141 | 142 | protected function getExtensions(): array 143 | { 144 | $autoFormType = $this->getConfiguredAutoFormType(); 145 | 146 | return [new PreloadedExtension([ 147 | $autoFormType, 148 | ], [])]; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/Form/Type/AutoFormTypeSimpleTest.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\Tests\Form\Type; 15 | 16 | use A2lix\AutoFormBundle\Form\Type\AutoFormType; 17 | use A2lix\AutoFormBundle\Tests\Fixtures\Entity\Media; 18 | use A2lix\AutoFormBundle\Tests\Fixtures\Entity\Product; 19 | use A2lix\AutoFormBundle\Tests\Form\TypeTestCase; 20 | use Symfony\Component\Form\Extension\Core\Type\SubmitType; 21 | use Symfony\Component\Form\PreloadedExtension; 22 | 23 | /** 24 | * @internal 25 | */ 26 | final class AutoFormTypeSimpleTest extends TypeTestCase 27 | { 28 | public function testEmptyForm(): void 29 | { 30 | $form = $this->factory->createBuilder(AutoFormType::class, new Product()) 31 | ->add('create', SubmitType::class) 32 | ->getForm() 33 | ; 34 | 35 | self::assertEquals(['create', 'title', 'description', 'url', 'mainMedia', 'medias'], array_keys($form->all()), 'Fields should matches Product fields'); 36 | 37 | $mediasFormOptions = $form->get('medias')->getConfig()->getOptions(); 38 | self::assertEquals(AutoFormType::class, $mediasFormOptions['entry_type'], 'Media type should be an AutoType'); 39 | self::assertEquals(Media::class, $mediasFormOptions['entry_options']['data_class'], 'Media should have its right data_class'); 40 | } 41 | 42 | public function testCreationForm(): Product 43 | { 44 | $form = $this->factory->createBuilder(AutoFormType::class, new Product()) 45 | ->add('create', SubmitType::class) 46 | ->getForm() 47 | ; 48 | 49 | $media1 = new Media(); 50 | $media1->setUrl('http://example.org/media1') 51 | ->setDescription('media1 desc') 52 | ; 53 | $media2 = new Media(); 54 | $media2->setUrl('http://example.org/media2') 55 | ->setDescription('media2 desc') 56 | ; 57 | 58 | $product = new Product(); 59 | $product->setUrl('a2lix.fr') 60 | ->addMedia($media1) 61 | ->addMedia($media2) 62 | ; 63 | 64 | $formData = [ 65 | 'url' => 'a2lix.fr', 66 | 'medias' => [ 67 | [ 68 | 'url' => 'http://example.org/media1', 69 | 'description' => 'media1 desc', 70 | ], 71 | [ 72 | 'url' => 'http://example.org/media2', 73 | 'description' => 'media2 desc', 74 | ], 75 | ], 76 | ]; 77 | 78 | $form->submit($formData); 79 | self::assertTrue($form->isSynchronized()); 80 | self::assertEquals($product, $form->getData()); 81 | 82 | return $product; 83 | } 84 | 85 | /** 86 | * @depends testCreationForm 87 | */ 88 | public function testEditionForm(Product $product): void 89 | { 90 | $product->getMedias()[0]->setUrl('http://example.org/media1-edit'); 91 | $product->getMedias()[1]->setDescription('media2 desc edit'); 92 | 93 | $formData = [ 94 | 'url' => 'a2lix.fr', 95 | 'medias' => [ 96 | [ 97 | 'url' => 'http://example.org/media1-edit', 98 | 'description' => 'media1 desc', 99 | ], 100 | [ 101 | 'url' => 'http://example.org/media2', 102 | 'description' => 'media2 desc edit', 103 | ], 104 | ], 105 | ]; 106 | 107 | $form = $this->factory->createBuilder(AutoFormType::class, new Product()) 108 | ->add('create', SubmitType::class) 109 | ->getForm() 110 | ; 111 | 112 | $form->submit($formData); 113 | self::assertTrue($form->isSynchronized()); 114 | self::assertEquals($product, $form->getData()); 115 | 116 | $view = $form->createView(); 117 | $children = $view->children; 118 | 119 | foreach (array_keys($formData) as $key) { 120 | self::assertArrayHasKey($key, $children); 121 | } 122 | } 123 | 124 | protected function getExtensions(): array 125 | { 126 | $autoFormType = $this->getConfiguredAutoFormType(); 127 | 128 | return [new PreloadedExtension([ 129 | $autoFormType, 130 | ], [])]; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/Form/TypeTestCase.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace A2lix\AutoFormBundle\Tests\Form; 15 | 16 | use A2lix\AutoFormBundle\Form\EventListener\AutoFormListener; 17 | use A2lix\AutoFormBundle\Form\Manipulator\DoctrineORMManipulator; 18 | use A2lix\AutoFormBundle\Form\Type\AutoFormType; 19 | use A2lix\AutoFormBundle\ObjectInfo\DoctrineORMInfo; 20 | use Doctrine\DBAL\DriverManager; 21 | use Doctrine\ORM\EntityManager; 22 | use Doctrine\ORM\ORMSetup; 23 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; 24 | use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension; 25 | use Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser; 26 | use Symfony\Component\Form\FormBuilder; 27 | use Symfony\Component\Form\Forms; 28 | use Symfony\Component\Form\Test\TypeTestCase as BaseTypeTestCase; 29 | use Symfony\Component\Validator\ConstraintViolationList; 30 | use Symfony\Component\Validator\Validator\ValidatorInterface; 31 | 32 | abstract class TypeTestCase extends BaseTypeTestCase 33 | { 34 | protected ?DoctrineORMManipulator $doctrineORMManipulator = null; 35 | 36 | protected function setUp(): void 37 | { 38 | parent::setUp(); 39 | 40 | $validator = $this->createMock(ValidatorInterface::class); 41 | $validator->method('validate')->willReturn(new ConstraintViolationList()); 42 | 43 | $this->factory = Forms::createFormFactoryBuilder() 44 | ->addExtensions($this->getExtensions()) 45 | ->addTypeExtension( 46 | new FormTypeValidatorExtension($validator) 47 | ) 48 | ->addTypeGuesser( 49 | $this->createMock(ValidatorTypeGuesser::class) 50 | ) 51 | ->getFormFactory() 52 | ; 53 | 54 | $this->dispatcher = $this->createMock(EventDispatcherInterface::class); 55 | $this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory); 56 | } 57 | 58 | protected function getDoctrineORMManipulator(): DoctrineORMManipulator 59 | { 60 | if (null !== $this->doctrineORMManipulator) { 61 | return $this->doctrineORMManipulator; 62 | } 63 | 64 | $config = ORMSetup::createAttributeMetadataConfiguration([__DIR__.'/../Fixtures/Entity'], true); 65 | $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config); 66 | $entityManager = new EntityManager($connection, $config); 67 | $doctrineORMInfo = new DoctrineORMInfo($entityManager->getMetadataFactory()); 68 | 69 | return $this->doctrineORMManipulator = new DoctrineORMManipulator($doctrineORMInfo, ['id', 'locale', 'translatable']); 70 | } 71 | 72 | protected function getConfiguredAutoFormType(): AutoFormType 73 | { 74 | $autoFormListener = new AutoFormListener($this->getDoctrineORMManipulator()); 75 | 76 | return new AutoFormType($autoFormListener); 77 | } 78 | } 79 | --------------------------------------------------------------------------------