├── .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 | [](https://packagist.org/packages/a2lix/auto-form-bundle)
6 | [](https://packagist.org/packages/a2lix/auto-form-bundle)
7 | [](https://packagist.org/packages/a2lix/auto-form-bundle)
8 |
9 | [](https://packagist.org/packages/a2lix/auto-form-bundle)
10 | [](https://packagist.org/packages/a2lix/auto-form-bundle)
11 | [](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 |
--------------------------------------------------------------------------------