├── .gitignore
├── .travis.yml
├── AdjusterRegistry.php
├── Annotations
├── Embed.php
├── Field.php
└── Form.php
├── CodeteFormGeneratorBundle.php
├── DependencyInjection
├── CodeteFormGeneratorExtension.php
└── Compiler
│ ├── AbstractCompilerPass.php
│ ├── ConfigurationModifiersCompilerPass.php
│ ├── FieldResolversCompilerPass.php
│ └── ViewProvidersCompilerPass.php
├── Form
└── Type
│ └── EmbedType.php
├── FormConfigurationFactory.php
├── FormConfigurationModifierInterface.php
├── FormFieldResolverInterface.php
├── FormGenerator.php
├── FormViewProviderInterface.php
├── LICENSE
├── README.md
├── Resources
└── config
│ └── form_generator.xml
├── Tests
├── Annotations
│ └── FormTest.php
├── BaseTest.php
├── CodeteFormGeneratorBundleTest.php
├── DependencyInjection
│ ├── CodeteFormGeneratorExtensionTest.php
│ └── Compiler
│ │ ├── ConfigurationModifiersCompilerPassTest.php
│ │ ├── FieldResolversCompilerPassTest.php
│ │ └── ViewProvidersCompilerPassTest.php
├── FormConfigurationFactoryTest.php
├── FormConfigurationModifier
│ ├── InactivePersonModifier.php
│ └── NoPhotoPersonModifier.php
├── FormFieldResolver
│ └── PersonSalaryResolver.php
├── FormGeneratorTest.php
├── FormViewProvider
│ └── PersonAddFormView.php
├── Model
│ ├── ClassLevelFields.php
│ ├── Director.php
│ ├── DisplayOptions.php
│ ├── InheritanceTest.php
│ ├── Person.php
│ ├── Simple.php
│ ├── SimpleNotOverridingDefaultView.php
│ ├── SimpleOverridingDefaultView.php
│ └── SimpleParent.php
└── bootstrap.php
├── UPGRADE-2.0.md
├── composer.json
└── phpunit.xml.dist
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.1
5 | - 7.2
6 |
7 | env:
8 | - SYMFONY_VERSION=3.4.*
9 | - SYMFONY_VERSION=4.0.*
10 |
11 | before_script:
12 | - composer require symfony/symfony:${SYMFONY_VERSION} --prefer-source
13 | - composer install --dev --prefer-source
14 |
15 | script:
16 | - ./vendor/bin/phpunit
17 |
--------------------------------------------------------------------------------
/AdjusterRegistry.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class AdjusterRegistry
11 | {
12 | /** @var FormConfigurationModifierInterface[][] */
13 | private $formConfigurationModifiers = [];
14 |
15 | /** @var FormFieldResolverInterface[][] */
16 | private $formFieldResolvers = [];
17 |
18 | /** @var FormViewProviderInterface[][] */
19 | private $formViewProviders = [];
20 |
21 | /** @var FormConfigurationModifierInterface[] */
22 | private $sortedFormConfigurationModifiers = [];
23 |
24 | /** @var FormFieldResolverInterface[] */
25 | private $sortedFormFieldResolvers = [];
26 |
27 | /** @var FormViewProviderInterface[] */
28 | private $sortedFormViewProviders = [];
29 |
30 | /** @var bool */
31 | private $needsSorting = false;
32 |
33 | /**
34 | * Adds modifier for form's configuration
35 | *
36 | * @param FormConfigurationModifierInterface $modifier
37 | * @param int $priority
38 | */
39 | public function addFormConfigurationModifier(FormConfigurationModifierInterface $modifier, $priority = 0)
40 | {
41 | $this->formConfigurationModifiers[$priority][] = $modifier;
42 | $this->needsSorting = true;
43 | }
44 |
45 | /**
46 | * Adds resolver for form's fields
47 | *
48 | * @param FormFieldResolverInterface $resolver
49 | * @param int $priority
50 | */
51 | public function addFormFieldResolver(FormFieldResolverInterface $resolver, $priority = 0)
52 | {
53 | $this->formFieldResolvers[$priority][] = $resolver;
54 | $this->needsSorting = true;
55 | }
56 |
57 | /**
58 | * Adds provider for defining default fields for form
59 | *
60 | * @param FormViewProviderInterface $provider
61 | * @param int $priority
62 | */
63 | public function addFormViewProvider(FormViewProviderInterface $provider, $priority = 0)
64 | {
65 | $this->formViewProviders[$priority][] = $provider;
66 | $this->needsSorting = true;
67 | }
68 |
69 | /**
70 | * Gets FormConfigurationModifiers sorted by priority.
71 | *
72 | * @return FormConfigurationModifierInterface[]
73 | */
74 | public function getFormConfigurationModifiers()
75 | {
76 | if ($this->needsSorting) {
77 | $this->sortRegisteredServices();
78 | }
79 | return $this->sortedFormConfigurationModifiers;
80 | }
81 |
82 | /**
83 | * Gets FormFieldResolvers sorted by priority.
84 | *
85 | * @return FormFieldResolverInterface[]
86 | */
87 | public function getFormFieldResolvers()
88 | {
89 | if ($this->needsSorting) {
90 | $this->sortRegisteredServices();
91 | }
92 | return $this->sortedFormFieldResolvers;
93 | }
94 |
95 | /**
96 | * Gets FormViewProviders sorted by priority.
97 | *
98 | * @return FormViewProviderInterface[]
99 | */
100 | public function getFormViewProviders()
101 | {
102 | if ($this->needsSorting) {
103 | $this->sortRegisteredServices();
104 | }
105 | return $this->sortedFormViewProviders;
106 | }
107 |
108 | /**
109 | * Sorts all registered adjusters by priority.
110 | */
111 | private function sortRegisteredServices()
112 | {
113 | krsort($this->formConfigurationModifiers);
114 | if ( ! empty($this->formConfigurationModifiers)) {
115 | $this->sortedFormConfigurationModifiers = call_user_func_array('array_merge', $this->formConfigurationModifiers);
116 | }
117 | krsort($this->formFieldResolvers);
118 | if ( ! empty($this->formFieldResolvers)) {
119 | $this->sortedFormFieldResolvers = call_user_func_array('array_merge', $this->formFieldResolvers);
120 | }
121 | krsort($this->formViewProviders);
122 | if ( ! empty($this->formViewProviders)) {
123 | $this->sortedFormViewProviders = call_user_func_array('array_merge', $this->formViewProviders);
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Annotations/Embed.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class Field extends \Doctrine\Common\Annotations\Annotation
11 | {
12 | /**
13 | * Unlike original Annotation we accept all variables.
14 | * If one of them is wrong FormBuilderInterface will let
15 | * us know about it.
16 | *
17 | * @param string $name
18 | * @param mixed $value
19 | */
20 | public function __set($name, $value)
21 | {
22 | $this->$name = $value;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Annotations/Form.php:
--------------------------------------------------------------------------------
1 |
10 | */
11 | class Form extends \Doctrine\Common\Annotations\Annotation
12 | {
13 | private $forms = [];
14 |
15 | public function __set($name, $value)
16 | {
17 | $this->forms[$name] = $value;
18 | }
19 |
20 | public function getForm($form)
21 | {
22 | if (!isset($this->forms[$form])) {
23 | if ($form === 'default') {
24 | return [];
25 | }
26 | throw new \InvalidArgumentException("Unknown form '$form'");
27 | }
28 | return $this->forms[$form];
29 | }
30 |
31 | public function hasForm($form)
32 | {
33 | return isset($this->forms[$form]);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/CodeteFormGeneratorBundle.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class CodeteFormGeneratorBundle extends Bundle
15 | {
16 | /**
17 | * {@inheritDoc}
18 | */
19 | public function build(ContainerBuilder $container)
20 | {
21 | $container->addCompilerPass(new ConfigurationModifiersCompilerPass());
22 | $container->addCompilerPass(new FieldResolversCompilerPass());
23 | $container->addCompilerPass(new ViewProvidersCompilerPass());
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/DependencyInjection/CodeteFormGeneratorExtension.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | class CodeteFormGeneratorExtension extends Extension
20 | {
21 | public function load(array $configs, ContainerBuilder $container)
22 | {
23 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
24 | $loader->load('form_generator.xml');
25 |
26 | $container->registerForAutoconfiguration(FormConfigurationModifierInterface::class)
27 | ->addTag(ConfigurationModifiersCompilerPass::TAG);
28 | $container->registerForAutoconfiguration(FormViewProviderInterface::class)
29 | ->addTag(ViewProvidersCompilerPass::TAG);
30 | $container->registerForAutoconfiguration(FormFieldResolverInterface::class)
31 | ->addTag(FieldResolversCompilerPass::TAG);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/AbstractCompilerPass.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | abstract class AbstractCompilerPass implements CompilerPassInterface
14 | {
15 | /**
16 | * Gets name of method that should be called.
17 | *
18 | * @return string
19 | */
20 | abstract protected function getMethodToCall();
21 |
22 | /**
23 | * Gets tag name.
24 | *
25 | * @return string
26 | */
27 | abstract protected function getTagName();
28 |
29 | /**
30 | * @inheritdoc
31 | */
32 | public function process(ContainerBuilder $container)
33 | {
34 | if (! $container->hasDefinition(FormGenerator::class)) {
35 | return;
36 | }
37 | $formGenerator = $container->getDefinition(FormGenerator::class);
38 | foreach ($container->findTaggedServiceIds($this->getTagName()) as $id => $tags) {
39 | foreach ($tags as $attributes) {
40 | if (! isset($attributes['priority'])) {
41 | $attributes['priority'] = 0;
42 | }
43 | $formGenerator->addMethodCall(
44 | $this->getMethodToCall(),
45 | [new Reference($id), (int) $attributes['priority']]
46 | );
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/ConfigurationModifiersCompilerPass.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class ConfigurationModifiersCompilerPass extends AbstractCompilerPass
9 | {
10 | const TAG = 'form_generator.configuration_modifier';
11 |
12 | /**
13 | * @inheritdoc
14 | */
15 | protected function getMethodToCall()
16 | {
17 | return 'addFormConfigurationModifier';
18 | }
19 |
20 | /**
21 | * @inheritdoc
22 | */
23 | protected function getTagName()
24 | {
25 | return self::TAG;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/FieldResolversCompilerPass.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class FieldResolversCompilerPass extends AbstractCompilerPass
9 | {
10 | const TAG = 'form_generator.field_resolver';
11 |
12 | /**
13 | * @inheritdoc
14 | */
15 | protected function getMethodToCall()
16 | {
17 | return 'addFormFieldResolver';
18 | }
19 |
20 | /**
21 | * @inheritdoc
22 | */
23 | protected function getTagName()
24 | {
25 | return self::TAG;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/DependencyInjection/Compiler/ViewProvidersCompilerPass.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | class ViewProvidersCompilerPass extends AbstractCompilerPass
9 | {
10 | const TAG = 'form_generator.view_provider';
11 |
12 | /**
13 | * @inheritdoc
14 | */
15 | protected function getMethodToCall()
16 | {
17 | return 'addFormViewProvider';
18 | }
19 |
20 | /**
21 | * @inheritdoc
22 | */
23 | protected function getTagName()
24 | {
25 | return self::TAG;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Form/Type/EmbedType.php:
--------------------------------------------------------------------------------
1 | formGenerator = $formGenerator;
23 | }
24 |
25 | /**
26 | * @param FormBuilderInterface $builder
27 | * @param array $options
28 | */
29 | public function buildForm(FormBuilderInterface $builder, array $options)
30 | {
31 | $this->formGenerator->populateFormBuilder($builder, $options['model'], $options['view'], isset($options['context']) ? $options['context'] : []);
32 | }
33 |
34 | /**
35 | * @param OptionsResolver $resolver
36 | */
37 | public function configureOptions(OptionsResolver $resolver)
38 | {
39 | $resolver->setDefaults([
40 | 'view' => 'default',
41 | 'class' => '',
42 | 'context' => [],
43 | 'model' => null
44 | ]);
45 | }
46 |
47 | /**
48 | * @return string
49 | */
50 | public function getBlockPrefix()
51 | {
52 | return 'embed';
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/FormConfigurationFactory.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class FormConfigurationFactory
18 | {
19 | /**
20 | * @var AdjusterRegistry
21 | */
22 | private $adjusterRegistry;
23 |
24 | /**
25 | * @var Reader
26 | */
27 | private $annotationReader;
28 |
29 | /**
30 | * @var Instantiator
31 | */
32 | private $instantiator;
33 |
34 | /**
35 | * @param AdjusterRegistry $adjusterRegistry
36 | */
37 | public function __construct(AdjusterRegistry $adjusterRegistry)
38 | {
39 | $this->adjusterRegistry = $adjusterRegistry;
40 | $this->annotationReader = new AnnotationReader();
41 | $this->instantiator = new Instantiator();
42 | }
43 |
44 | /**
45 | * Generates initial form configuration.
46 | *
47 | * @param string $form
48 | * @param object $model
49 | * @param array $context
50 | * @return array
51 | */
52 | public function getConfiguration($form, $model, $context)
53 | {
54 | $fields = null;
55 | foreach ($this->adjusterRegistry->getFormViewProviders() as $provider) {
56 | if ($provider->supports($form, $model, $context)) {
57 | $fields = $provider->getFields($model, $context);
58 | break;
59 | }
60 | }
61 | if ($fields === null) {
62 | $fields = $this->getFields($model, $form);
63 | }
64 | $fields = $this->normalizeFields($fields);
65 | return $this->getFieldsConfiguration($model, $fields);
66 | }
67 |
68 | /**
69 | * Creates form configuration for $model for given $fields.
70 | *
71 | * @param object $model
72 | * @param array $fields
73 | * @return array
74 | */
75 | private function getFieldsConfiguration($model, $fields = [])
76 | {
77 | $configuration = $properties = [];
78 | $ro = new \ReflectionObject($model);
79 | if (empty($fields)) {
80 | $properties = $ro->getProperties();
81 | } else {
82 | foreach (array_keys($fields) as $field) {
83 | // setting the configuration to null guarantees the order of elements in there
84 | // to match the order specified in $fields
85 | $configuration[$field] = null;
86 | if (! $ro->hasProperty($field)) {
87 | continue; // most prob a class-level field, order will be maintained due to trick above
88 | }
89 | $properties[] = $ro->getProperty($field);
90 | }
91 | }
92 | $fieldConfigurations = [];
93 | // first are coming properties
94 | foreach ($properties as $property) {
95 | $propertyName = $property->getName();
96 | $propertyIsListed = array_key_exists($propertyName, $fields);
97 | if (!empty($fields) && !$propertyIsListed) {
98 | continue; // list of fields was specified and current one is not there
99 | }
100 | $fieldConfiguration = $this->annotationReader->getPropertyAnnotation($property, Field::class);
101 | if ($fieldConfiguration === null && !$propertyIsListed) {
102 | continue;
103 | }
104 | $fieldConfigurations[$propertyName] = $fieldConfiguration;
105 | }
106 | // later are coming class-level fields. We need to iterate through all annotations as there's no method
107 | // to get *all* occurrences of chosen annotation.
108 | foreach ($this->annotationReader->getClassAnnotations($ro) as $annotation) {
109 | if (! $annotation instanceof Field) {
110 | continue;
111 | }
112 | $propertyName = $annotation->value;
113 | if (!empty($fields) && !array_key_exists($propertyName, $fields)) {
114 | continue; // list of fields was specified and current one is not there
115 | }
116 | $fieldConfigurations[$propertyName] = $annotation;
117 | }
118 | foreach ($fieldConfigurations as $propertyName => $fieldConfiguration) {
119 | $configuration[$propertyName] = (array)$fieldConfiguration;
120 | if (isset($fields[$propertyName])) {
121 | $configuration[$propertyName] = array_replace_recursive($configuration[$propertyName], $fields[$propertyName]);
122 | }
123 | if ($configuration[$propertyName]['type'] === EmbedType::class) {
124 | if (! $ro->hasProperty($propertyName) || ($value = $ro->getProperty($propertyName)->getValue($model)) === null) {
125 | $value = $this->instantiator->instantiate($configuration[$propertyName]['class']);
126 | }
127 | $configuration[$propertyName]['data_class'] = $configuration[$propertyName]['class'];
128 | $configuration[$propertyName]['model'] = $value;
129 | }
130 | // this variable comes from Doctrine\Common\Annotations\Annotation
131 | unset($configuration[$propertyName]['value']);
132 | }
133 | return $configuration;
134 | }
135 |
136 | /**
137 | * Gets field list from $model basing on its Form annotation.
138 | *
139 | * @param object $model
140 | * @param string $form view
141 | * @return array list of fields (or empty for all fields)
142 | */
143 | private function getFields($model, $form)
144 | {
145 | $ro = new \ReflectionObject($model);
146 | $formAnnotation = $this->annotationReader->getClassAnnotation($ro, Form::class);
147 | if (($formAnnotation === null || !$formAnnotation->hasForm($form)) && $ro->getParentClass()) {
148 | while ($ro = $ro->getParentClass()) {
149 | $formAnnotation = $this->annotationReader->getClassAnnotation($ro, Form::class);
150 | if ($formAnnotation !== null && $formAnnotation->hasForm($form)) {
151 | break;
152 | }
153 | }
154 | }
155 | if ($formAnnotation === null) {
156 | $formAnnotation = new Annotations\Form([]);
157 | }
158 | return $formAnnotation->getForm($form);
159 | }
160 |
161 | /**
162 | * Normalizes $fields array.
163 | *
164 | * @param array $_fields
165 | * @return array
166 | */
167 | private function normalizeFields($_fields)
168 | {
169 | $fields = [];
170 | foreach ($_fields as $key => $value) {
171 | if (is_array($value)) {
172 | $fields[$key] = $value;
173 | } else {
174 | $fields[$value] = [];
175 | }
176 | }
177 | return $fields;
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/FormConfigurationModifierInterface.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | interface FormConfigurationModifierInterface
9 | {
10 | /**
11 | * Check if this FormConfigurationModifier can alter form's configuration
12 | *
13 | * @param object $model
14 | * @param array $configuration
15 | * @param array $context
16 | * @return bool
17 | */
18 | public function supports($model, $configuration, $context);
19 |
20 | /**
21 | * Modifies form's configuration
22 | *
23 | * @param object $model
24 | * @param array $configuration
25 | * @param array $context
26 | * @return array
27 | */
28 | public function modify($model, $configuration, $context);
29 | }
30 |
--------------------------------------------------------------------------------
/FormFieldResolverInterface.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | interface FormFieldResolverInterface
16 | {
17 | /**
18 | * Check if this FormFieldResolver can provide form field
19 | *
20 | * @param object $model
21 | * @param string $field
22 | * @param string $type
23 | * @param array $options
24 | * @param array $context
25 | * @return bool
26 | */
27 | public function supports($model, $field, $type, $options, $context);
28 |
29 | /**
30 | * Creates FormBuilderInterface representing a field to be added to parent form
31 | *
32 | * @param FormBuilderInterface $fb
33 | * @param string $field
34 | * @param string $type
35 | * @param array $options
36 | * @param array $context
37 | * @return FormBuilderInterface
38 | */
39 | public function getFormField(FormBuilderInterface $fb, $field, $type, $options, $context);
40 | }
41 |
--------------------------------------------------------------------------------
/FormGenerator.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | class FormGenerator
15 | {
16 | /** @var AdjusterRegistry */
17 | private $adjusterRegistry;
18 |
19 | /** @var FormConfigurationFactory */
20 | private $formConfigurationFactory;
21 |
22 | /** @var FormFactoryInterface */
23 | private $formFactory;
24 |
25 | /**
26 | * @param FormFactoryInterface $formFactory
27 | */
28 | public function __construct(FormFactoryInterface $formFactory)
29 | {
30 | $this->adjusterRegistry = new AdjusterRegistry();
31 | $this->formConfigurationFactory = new FormConfigurationFactory($this->adjusterRegistry);
32 | $this->formFactory = $formFactory;
33 | }
34 |
35 | /**
36 | * Adds modifier for form's configuration.
37 | *
38 | * @param FormConfigurationModifierInterface $modifier
39 | * @param int $priority
40 | */
41 | public function addFormConfigurationModifier(FormConfigurationModifierInterface $modifier, $priority = 0)
42 | {
43 | $this->adjusterRegistry->addFormConfigurationModifier($modifier, $priority);
44 | }
45 |
46 | /**
47 | * Adds resolver for form's fields.
48 | *
49 | * @param FormFieldResolverInterface $resolver
50 | * @param int $priority
51 | */
52 | public function addFormFieldResolver(FormFieldResolverInterface $resolver, $priority = 0)
53 | {
54 | $this->adjusterRegistry->addFormFieldResolver($resolver, $priority);
55 | }
56 |
57 | /**
58 | * Adds provider for defining default fields for form.
59 | *
60 | * @param FormViewProviderInterface $provider
61 | * @param int $priority
62 | */
63 | public function addFormViewProvider(FormViewProviderInterface $provider, $priority = 0)
64 | {
65 | $this->adjusterRegistry->addFormViewProvider($provider, $priority);
66 | }
67 |
68 | /**
69 | * Creates FormBuilder and populates it.
70 | *
71 | * @param object $model data object
72 | * @param string $form view to generate
73 | * @param array $context
74 | * @param array $options
75 | * @return FormBuilderInterface
76 | */
77 | public function createFormBuilder($model, $form = 'default', $context = [], $options=[])
78 | {
79 | $fb = $this->formFactory->createBuilder(FormType::class, $model, $options);
80 |
81 | $this->populateFormBuilder($fb, $model, $form, $context);
82 | return $fb;
83 | }
84 |
85 | /**
86 | * Creates named FormBuilder and populates it.
87 | *
88 | * @param string $name
89 | * @param object $model data object
90 | * @param string $form view to generate
91 | * @param array $context
92 | * @param array $options
93 | * @return FormBuilderInterface
94 | */
95 | public function createNamedFormBuilder($name, $model, $form = 'default', $context = [], $options=[])
96 | {
97 | $fb = $this->formFactory->createNamedBuilder($name, FormType::class, $model, $options);
98 |
99 | $this->populateFormBuilder($fb, $model, $form, $context);
100 | return $fb;
101 | }
102 |
103 | /**
104 | * Populates FormBuilder.
105 | *
106 | * @param FormBuilderInterface $fb
107 | * @param object $model
108 | * @param string $form view to generate
109 | * @param array $context
110 | */
111 | public function populateFormBuilder(FormBuilderInterface $fb, $model, $form = 'default', $context = [])
112 | {
113 | $configuration = $this->formConfigurationFactory->getConfiguration($form, $model, $context);
114 | foreach ($this->adjusterRegistry->getFormConfigurationModifiers() as $modifier) {
115 | if ($modifier->supports($model, $configuration, $context)) {
116 | $configuration = $modifier->modify($model, $configuration, $context);
117 | }
118 | }
119 | foreach ($configuration as $field => $options) {
120 | $type = null;
121 | if (isset($options['type'])) {
122 | $type = $options['type'];
123 | unset($options['type']);
124 | }
125 | foreach ($this->adjusterRegistry->getFormFieldResolvers() as $resolver) {
126 | if ($resolver->supports($model, $field, $type, $options, $context)) {
127 | $fb->add($resolver->getFormField($fb, $field, $type, $options, $context));
128 | continue 2;
129 | }
130 | }
131 | if (isset($options['options'])) {
132 | $options = $options['options'];
133 | }
134 | $fb->add($field, $type, $options);
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/FormViewProviderInterface.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | interface FormViewProviderInterface
14 | {
15 | /**
16 | * Check if this FormViewProvider can provide form view
17 | *
18 | * @param string $form
19 | * @param object $model
20 | * @param array $context
21 | * @return bool
22 | */
23 | public function supports($form, $model, $context);
24 |
25 | /**
26 | * Gets list of fields (with configuration eventually) that
27 | * should be included in Form. Configuration defined here
28 | * will override field's configuration for same attributes,
29 | * rest will be inherited (effectively array_replace_recursive
30 | * will be called)
31 | *
32 | * @param object $model
33 | * @param array $context
34 | * @return array array('foo', 'bar' => array(...), ...)
35 | */
36 | public function getFields($model, $context);
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Codete
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | FormGeneratorBundle
2 | ===================
3 |
4 | [](https://travis-ci.org/codete/FormGeneratorBundle)
5 | [](https://insight.sensiolabs.com/projects/8893e0c9-ed68-498e-aa86-63320ac43a62)
6 |
7 | We were extremely bored with writing/generating/keeping-up-to-date
8 | our FormType classes so we wanted to automate the process and limit
9 | required changes only to Entity/Document/Whatever class and get new
10 | form out of the box - this is how FormGenerator was invented.
11 |
12 | **You're looking at the documentation for version 2.0**
13 |
14 | - [go to 1.x documentation](https://github.com/codete/FormGeneratorBundle/blob/1.3.0/README.md)
15 | - [see UPGRADE.md for help with upgrading](https://github.com/codete/FormGeneratorBundle/blob/master/UPGRADE-2.0.md)
16 |
17 | Basic Usages
18 | ------------
19 |
20 | Consider a class
21 |
22 | ``` php
23 | use Codete\FormGeneratorBundle\Annotations as Form;
24 | // import Symfony form types so ::class will work
25 |
26 | /**
27 | * @Form\Form(
28 | * personal = { "title", "name", "surname", "photo", "active" },
29 | * work = { "salary" },
30 | * admin = { "id" = { "type" = NumberType::class }, "surname" }
31 | * )
32 | */
33 | class Person
34 | {
35 | public $id;
36 |
37 | /**
38 | * @Form\Field(type=ChoiceType::class, choices = { "Mr." = "mr", "Ms." = "ms" })
39 | */
40 | public $title;
41 |
42 | /**
43 | * @Form\Field(type=TextType::class)
44 | */
45 | public $name;
46 |
47 | /**
48 | * @Form\Field(type=TextType::class)
49 | */
50 | public $surname;
51 |
52 | /**
53 | * @Form\Field(type=FileType::class)
54 | */
55 | public $photo;
56 |
57 | /**
58 | * @Form\Field(type=CheckboxType::class)
59 | */
60 | public $active;
61 |
62 | /**
63 | * @Form\Field(type=MoneyType::class)
64 | */
65 | public $salary;
66 | }
67 | ```
68 |
69 | Now instead of writing whole ``PersonFormType`` and populating
70 | FormBuilder there we can use instead:
71 |
72 | ``` php
73 | use Codete\FormGeneratorBundle\FormGenerator;
74 |
75 | $generator = $this->get(FormGenerator::class);
76 |
77 | $person = new Person();
78 | $form = $generator->createFormBuilder($person)->getForm();
79 | $form->handleRequest($request);
80 | ```
81 |
82 | Voila! Form for editing all annotated properties is generated for us.
83 | We could even omit ``type=".."`` in annotations if Symfony will be
84 | able to guess the field's type for us.
85 |
86 | Specifying Field Options
87 | ------------------------
88 |
89 | By default everything you specify in `@Form\Field` (except for `type`) annotation
90 | will be passed as an option to generated form type. To illustrate:
91 |
92 | ```php
93 | /**
94 | * @Form\Field(type=ChoiceType::class, choices = { "Mr." = "mr", "Ms." = "ms" }, "attr" = { "class" = "foo" })
95 | */
96 | public $title;
97 | ```
98 |
99 | is equivalent to:
100 |
101 | ```php
102 | $fb->add('title', ChoiceType::class, [
103 | 'choices' => [ 'Mr.' => 'mr', 'Ms.' => 'ms' ],
104 | 'attr' => [ 'class' => 'foo' ],
105 | ]);
106 | ```
107 |
108 | This approach has few advantages like saving you a bunch of keystrokes each time you
109 | are specifying options, but there are downsides too. First, if you have any custom
110 | option for one of your modifiers you forget to `unset`, Symfony will be unhappy and
111 | will let you know by throwing an exception. Another downside is that we have reserved
112 | `type` property and it's needed as an option for the repeated type. If you ever find
113 | yourself in one of described cases, or you just prefer to be explicit, you can put
114 | all Symfony fields' options into an `options` property:
115 |
116 | ```php
117 | /**
118 | * @Form\Field(
119 | * type=ChoiceType::class,
120 | * options={ "choices" = { "Mr." = "mr", "Ms." = "ms" }, "attr" = { "class" = "foo" } }
121 | * )
122 | */
123 | public $title;
124 | ```
125 |
126 | When Form Generator creates a form field and finds `options` property, it will pass
127 | them as that field's options to the `FormBuilder`. Effectively this allows you to
128 | separate field's options from options for your configuration modifiers which can be
129 | a gain on its own.
130 |
131 | Adding fields not mapped to a property
132 | --------------------------------------
133 |
134 | Sometimes you may need to add a field that will not be mapped to a property. An example
135 | of such use case is adding buttons to the form:
136 |
137 | ```php
138 | /**
139 | * The first value in Field annotation specifies field's name.
140 | *
141 | * @Form\Field("reset", type=ResetType::class)
142 | * @Form\Field("submit", type=SubmitType::class, "label"="Save")
143 | */
144 | class Person
145 | ```
146 |
147 | All fields added on the class level come last in the generated form, unless a form view
148 | (described below) specifies otherwise. Contrary to other class-level settings, `@Field`s
149 | will not be inherited by child classes.
150 |
151 | Form Views
152 | ----------
153 |
154 | In the example we have defined additional form views in ``@Form\Form``
155 | annotation so we can add another argument to ``createFormBuilder``
156 |
157 | ``` php
158 | $form = $generator->createFormBuilder($person, 'personal')->getForm();
159 | ```
160 |
161 | And we will get Form with properties specified in annotation. We can
162 | also add/override fields and their properties like this:
163 |
164 | ``` php
165 | /**
166 | * @Form\Form(
167 | * work = { "salary" = { "attr" = { "class" = "foo" } } }
168 | * )
169 | */
170 | class Person
171 | ```
172 |
173 | But if you need something more sophisticated than Annotations we
174 | have prepared few possibilities that can be either added manually
175 | or by tagging your services. For each of them FormGenerator allows
176 | you to pass any additional informations you want in optional
177 | ``$context`` argument. Both ways allows you to specify `priority`
178 | which defines order of execution (default is `0`, if two or more
179 | services have same priority then first added is executed first).
180 |
181 | **If you have enabled [Service autoconfiguration](http://symfony.com/blog/new-in-symfony-3-3-service-autoconfiguration)
182 | the bundle will automatically tag services for you.**
183 |
184 | FormViewProvider
185 | ----------------
186 |
187 | These are used to provide fields list and/or basic configuration
188 | for Forms and are doing exactly same thing as ``@Form\Form``
189 | annotation.
190 |
191 | Tag for service: ``form_generator.view_provider``
192 |
193 | FormConfigurationModifier
194 | -------------------------
195 |
196 | These can modify any form configuration provided by class
197 | itself or FormViewProviders. Feel free to remove or add more
198 | stuff to your Form or tweak existing configuration
199 |
200 | Tag for service: ``form_generator.configuration_modifier``
201 |
202 | ``` php
203 | class InactivePersonModifier implements FormConfigurationModifierInterface
204 | {
205 | public function modify($model, $configuration, $context)
206 | {
207 | unset($configuration['salary']);
208 | return $configuration;
209 | }
210 |
211 | public function supports($model, $configuration, $context)
212 | {
213 | return $model instanceof Person && $model->active === false;
214 | }
215 | }
216 | ```
217 |
218 | FormFieldResolver
219 | -----------------
220 |
221 | These are responsible for creating actual field in Form and can
222 | be used for instance to attach Transformers to your fields.
223 |
224 | Tag for service: ``form_generator.field_resolver``
225 |
226 | ``` php
227 | class PersonSalaryResolver implements FormFieldResolverInterface
228 | {
229 | public function getFormField(FormBuilderInterface $fb, $field, $type, $options, $context)
230 | {
231 | $transformer = new /* ... */;
232 | return $fb->create($field, $type, $options)
233 | ->addViewTransformer($transformer);
234 | }
235 |
236 | public function supports($model, $field, $type, $options, $context)
237 | {
238 | return $model instanceof Person && $field === 'salary';
239 | }
240 | }
241 | ```
242 |
243 | Embedded Forms
244 | --------------
245 |
246 | If you need embedded forms we got you covered:
247 |
248 | ``` php
249 | /**
250 | * @Form\Embed(class="Codete\FormGeneratorBundle\Tests\Model\Person")
251 | */
252 | public $person;
253 | ```
254 |
255 | Such sub-form will contain all annotated properties from given model.
256 | To specify a view for the generated embedded form just specify it in
257 | the configuration:
258 |
259 | ``` php
260 | /**
261 | * @Form\Embed(
262 | * class="Codete\FormGeneratorBundle\Tests\Model\Person",
263 | * view="work"
264 | * )
265 | */
266 | public $employee;
267 | ```
268 |
--------------------------------------------------------------------------------
/Resources/config/form_generator.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Tests/Annotations/FormTest.php:
--------------------------------------------------------------------------------
1 | assertSame([], $f->getForm('default'));
14 | }
15 |
16 | public function testDefaultCanBeOverwritten()
17 | {
18 | $d = ['foo' => 'bar'];
19 | $f = new Form(['default' => $d]);
20 | $this->assertSame($d, $f->getForm('default'));
21 | }
22 |
23 | /**
24 | * @expectedException \InvalidArgumentException
25 | * @expectedExceptionMessage Unknown form 'foo'
26 | */
27 | public function testUnknownFormThrowsException()
28 | {
29 | $f = new Form([]);
30 | $f->getForm('foo');
31 | }
32 |
33 | public function testNonDefaultForm()
34 | {
35 | $foo = ['foo' => 'bar', 'baz'];
36 | $f = new Form(['foo' => $foo]);
37 | $this->assertSame($foo, $f->getForm('foo'));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/BaseTest.php:
--------------------------------------------------------------------------------
1 | formFactoryBuilder = Forms::createFormFactoryBuilder();
29 |
30 | $this->formGenerator = new FormGenerator(
31 | $this->formFactoryBuilder->getFormFactory()
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/CodeteFormGeneratorBundleTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder')
12 | ->disableOriginalConstructor()
13 | ->getMock();
14 | $container->expects($this->exactly(3))
15 | ->method('addCompilerPass')
16 | ->withConsecutive(
17 | [$this->isInstanceOf('Codete\FormGeneratorBundle\DependencyInjection\Compiler\ConfigurationModifiersCompilerPass')],
18 | [$this->isInstanceOf('Codete\FormGeneratorBundle\DependencyInjection\Compiler\FieldResolversCompilerPass')],
19 | [$this->isInstanceOf('Codete\FormGeneratorBundle\DependencyInjection\Compiler\ViewProvidersCompilerPass')]
20 | );
21 | $bundle = new CodeteFormGeneratorBundle();
22 | $bundle->build($container);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Tests/DependencyInjection/CodeteFormGeneratorExtensionTest.php:
--------------------------------------------------------------------------------
1 | load([], $container);
24 |
25 | $this->assertTrue($container->has(FormGenerator::class));
26 | $this->assertTrue($container->has(EmbedType::class));
27 | $embedType = $container->getDefinition(EmbedType::class);
28 | $this->assertTrue($embedType->hasTag('form.type'));
29 | }
30 |
31 | public function testAutoconfigure()
32 | {
33 | $container = new ContainerBuilder();
34 | $extension = new CodeteFormGeneratorExtension();
35 | $extension->load([], $container);
36 | $autoconfigure = $container->getAutoconfiguredInstanceof();
37 |
38 | $this->assertArrayHasKey(FormConfigurationModifierInterface::class, $autoconfigure);
39 | $this->assertTrue($autoconfigure[FormConfigurationModifierInterface::class]->hasTag(ConfigurationModifiersCompilerPass::TAG));
40 |
41 | $this->assertArrayHasKey(FormViewProviderInterface::class, $autoconfigure);
42 | $this->assertTrue($autoconfigure[FormViewProviderInterface::class]->hasTag(ViewProvidersCompilerPass::TAG));
43 |
44 | $this->assertArrayHasKey(FormFieldResolverInterface::class, $autoconfigure);
45 | $this->assertTrue($autoconfigure[FormFieldResolverInterface::class]->hasTag(FieldResolversCompilerPass::TAG));
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/DependencyInjection/Compiler/ConfigurationModifiersCompilerPassTest.php:
--------------------------------------------------------------------------------
1 | addTag('form_generator.configuration_modifier');
18 | $importantModifier = new Definition();
19 | $importantModifier->addTag('form_generator.configuration_modifier', ['priority' => 255]);
20 |
21 | $container = new ContainerBuilder;
22 | $container->setDefinition(FormGenerator::class, $fg);
23 | $container->setDefinition('some.form.modifier', $modifier);
24 | $container->setDefinition('important.form_modifier', $importantModifier);
25 |
26 | $pass = new ConfigurationModifiersCompilerPass();
27 |
28 | $this->assertCount(0, $fg->getMethodCalls());
29 | $pass->process($container);
30 | $methodCalls = $fg->getMethodCalls();
31 | $this->assertCount(2, $methodCalls);
32 | // check if priorities are passed correctly
33 | $this->assertSame(0, $methodCalls[0][1][1]);
34 | $this->assertSame(255, $methodCalls[1][1][1]);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/DependencyInjection/Compiler/FieldResolversCompilerPassTest.php:
--------------------------------------------------------------------------------
1 | addTag('form_generator.field_resolver');
18 | $importantResolver = new Definition();
19 | $importantResolver->addTag('form_generator.field_resolver', ['priority' => 255]);
20 |
21 | $container = new ContainerBuilder;
22 | $container->setDefinition(FormGenerator::class, $fg);
23 | $container->setDefinition('some.field_resolver', $modifier);
24 | $container->setDefinition('important.field_resolver', $importantResolver);
25 |
26 | $pass = new FieldResolversCompilerPass();
27 |
28 | $this->assertCount(0, $fg->getMethodCalls());
29 | $pass->process($container);
30 | $methodCalls = $fg->getMethodCalls();
31 | $this->assertCount(2, $methodCalls);
32 | // check if priorities are passed correctly
33 | $this->assertSame(0, $methodCalls[0][1][1]);
34 | $this->assertSame(255, $methodCalls[1][1][1]);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/DependencyInjection/Compiler/ViewProvidersCompilerPassTest.php:
--------------------------------------------------------------------------------
1 | addTag('form_generator.view_provider');
18 | $importantProvider = new Definition();
19 | $importantProvider->addTag('form_generator.view_provider', ['priority' => 255]);
20 |
21 | $container = new ContainerBuilder;
22 | $container->setDefinition(FormGenerator::class, $fg);
23 | $container->setDefinition('some.form_provider', $provider);
24 | $container->setDefinition('important.form_provider', $importantProvider);
25 |
26 | $pass = new ViewProvidersCompilerPass();
27 |
28 | $this->assertCount(0, $fg->getMethodCalls());
29 | $pass->process($container);
30 | $methodCalls = $fg->getMethodCalls();
31 | $this->assertCount(2, $methodCalls);
32 | // check if priorities are passed correctly
33 | $this->assertSame(0, $methodCalls[0][1][1]);
34 | $this->assertSame(255, $methodCalls[1][1][1]);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/FormConfigurationFactoryTest.php:
--------------------------------------------------------------------------------
1 | getMethod('normalizeFields');
18 | $m->setAccessible(true);
19 | $this->assertSame($expected, $m->invoke($factory, $toNormalize));
20 | }
21 |
22 | public function provideFieldsNormalization()
23 | {
24 | return [
25 | [
26 | ['foo', 'bar'],
27 | ['foo' => [], 'bar' => []],
28 | ],
29 | [
30 | ['foo' => ['bar' => 'baz']],
31 | ['foo' => ['bar' => 'baz']],
32 | ],
33 | [
34 | ['foo', 'bar' => []],
35 | ['foo' => [], 'bar' => []],
36 | ],
37 | ];
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/FormConfigurationModifier/InactivePersonModifier.php:
--------------------------------------------------------------------------------
1 | active === false;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/FormConfigurationModifier/NoPhotoPersonModifier.php:
--------------------------------------------------------------------------------
1 | create($field, $type, $options)
16 | ->addViewTransformer($transformer);
17 | }
18 |
19 | public function supports($model, $field, $type, $options, $context)
20 | {
21 | return $model instanceof Person && $field === 'salary';
22 | }
23 | }
24 |
25 | class DummyDataTransformer implements DataTransformerInterface
26 | {
27 | public function reverseTransform($value) {
28 |
29 | }
30 |
31 | public function transform($value) {
32 |
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/FormGeneratorTest.php:
--------------------------------------------------------------------------------
1 | formGenerator);
25 |
26 | $this->embedTypeExtension = new PreloadedExtension([
27 | $embedType->getBlockPrefix() => $embedType,
28 | ], []);
29 | }
30 |
31 | /**
32 | * @dataProvider provideDefaultForm
33 | */
34 | public function testDefaultForm($model, $expectedFields, $additionalCheck = null)
35 | {
36 | $this->checkForm($model, $expectedFields, $additionalCheck, 'default');
37 | }
38 |
39 | public function provideDefaultForm()
40 | {
41 | return [
42 | [new Model\Simple(), ['title']],
43 | [new Model\SimpleNotOverridingDefaultView(), ['author', 'title']],
44 | [new Model\SimpleOverridingDefaultView(), ['title', 'author'], function($phpunit, $form) {
45 | $titleOptions = $form->get('title')->getConfig()->getOptions();
46 | $phpunit->assertEquals('foo', $titleOptions['attr']['class']);
47 | $authorConfig = $form->get('author')->getConfig();
48 | $phpunit->assertInstanceOf('Symfony\Component\Form\Extension\Core\Type\ChoiceType', $authorConfig->getType()->getInnerType());
49 | $authorOptions = $authorConfig->getOptions();
50 | $phpunit->assertSame(['foo' => 'foo', 'bar' => 'bar'], $authorOptions['choices']);
51 | }],
52 | [new Model\DisplayOptions(), ['normal', 'options', 'optionsIgnoreInlinedFields'], function($phpunit, $form) {
53 | $normal = $form->get('normal')->getConfig()->getOptions();
54 | $phpunit->assertEquals('foo', $normal['attr']['class']);
55 | $options = $form->get('options')->getConfig()->getOptions();
56 | $phpunit->assertEquals('foo', $options['attr']['class']);
57 | $optionsIgnoreInlinedFields = $form->get('optionsIgnoreInlinedFields')->getConfig()->getOptions();
58 | $phpunit->assertEquals('foo', $optionsIgnoreInlinedFields['attr']['class']);
59 | }],
60 | [new Model\ClassLevelFields(), ['title', 'reset', 'submit']],
61 | ];
62 | }
63 |
64 | public function testNamedForm()
65 | {
66 | $form = $this->formGenerator->createNamedFormBuilder('my_form', new Model\Simple());
67 | $this->assertSame('my_form', $form->getName());
68 | }
69 |
70 | public function testNamedFormWithOptionsMethodPut()
71 | {
72 | $form = $this->formGenerator->createNamedFormBuilder('my_form', new Model\Simple(), 'default', [],
73 | ['method' => 'PUT']);
74 | $this->assertSame('PUT', $form->getFormConfig()->getMethod());
75 | }
76 |
77 | public function testFormWithOptionsMethodPut()
78 | {
79 | $form = $this->formGenerator->createFormBuilder(new Model\Person(), 'work', [], ['method' => 'PUT']);
80 | $this->assertSame('PUT', $form->getFormConfig()->getMethod());
81 | }
82 |
83 | public function testFormViewDefinedInAnnotation()
84 | {
85 | $this->checkForm(new Model\SimpleOverridingDefaultView(), ['title'], null, 'only_title');
86 | }
87 |
88 | public function testFieldProvidedButNotAnnotated()
89 | {
90 | $this->checkForm(new Model\Person(), ['id', 'surname'], null, 'admin');
91 | }
92 |
93 | /**
94 | * @expectedException \InvalidArgumentException
95 | * @expectedExceptionMessage Unknown form 'foo'
96 | */
97 | public function testUnknownFormViewThrowsException()
98 | {
99 | $this->formGenerator->createFormBuilder(new Model\Simple(), 'foo');
100 | }
101 |
102 | public function testFormViewProvider()
103 | {
104 | $this->formGenerator->addFormViewProvider(new FormViewProvider\PersonAddFormView());
105 | $this->checkForm(new Model\Person(), ['surname'], null, 'add');
106 | }
107 |
108 | public function testFormViewProviderOrderMatters()
109 | {
110 | $this->formGenerator->addFormViewProvider(new FormViewProvider\PersonAddFormView());
111 | $notCalled = $this->getMockBuilder('Codete\FormGeneratorBundle\FormViewProviderInterface')
112 | ->getMock();
113 | $notCalled->expects($this->never())->method('supports');
114 | $this->formGenerator->addFormViewProvider($notCalled);
115 | $this->checkForm(new Model\Person(), ['surname'], null, 'add');
116 | }
117 |
118 | public function testFormViewProviderPriorityMatters()
119 | {
120 | $notCalled = $this->getMockBuilder('Codete\FormGeneratorBundle\FormViewProviderInterface')
121 | ->getMock();
122 | $notCalled->expects($this->never())->method('supports');
123 | $this->formGenerator->addFormViewProvider($notCalled);
124 | $this->formGenerator->addFormViewProvider(new FormViewProvider\PersonAddFormView(), 1);
125 | $this->checkForm(new Model\Person(), ['surname'], null, 'add');
126 | }
127 |
128 | /**
129 | * @depends testFormViewProviderPriorityMatters
130 | */
131 | public function testFormViewProviderPriorityAfterAnotherAdd()
132 | {
133 | $called = $this->getMockBuilder('Codete\FormGeneratorBundle\FormViewProviderInterface')
134 | ->getMock();
135 | $called->expects($this->once())->method('supports');
136 | $this->formGenerator->addFormViewProvider($called, 5);
137 | $this->formGenerator->createFormBuilder(new Model\Person(), 'work');
138 | }
139 |
140 | public function testFormConfigurationModifier()
141 | {
142 | $this->formGenerator->addFormConfigurationModifier(new FormConfigurationModifier\InactivePersonModifier());
143 | $model = new Model\Person();
144 | $model->active = false;
145 | $this->checkForm($model, ['title', 'name', 'surname', 'photo', 'active']);
146 | }
147 |
148 | public function testFormFieldResolver()
149 | {
150 | $this->formGenerator->addFormFieldResolver(new FormFieldResolver\PersonSalaryResolver());
151 | $this->checkForm(new Model\Person(), ['title', 'name', 'surname', 'photo', 'active', 'salary'], function($phpunit, $form) {
152 | $config = $form->get('salary')->getConfig();
153 | foreach ($config->getViewTransformers() as $t) {
154 | if ($t instanceof FormFieldResolver\DummyDataTransformer) {
155 | return true;
156 | }
157 | }
158 | throw new \Exception('DummyDataTransformer has not been found');
159 | });
160 | }
161 |
162 | public function testFormFieldResolverOrderMatters()
163 | {
164 | $this->formGenerator->addFormFieldResolver(new FormFieldResolver\PersonSalaryResolver());
165 | $notCalled = $this->getMockBuilder('Codete\FormGeneratorBundle\FormFieldResolverInterface')
166 | ->getMock();
167 | $notCalled->expects($this->never())->method('supports');
168 | $this->formGenerator->addFormFieldResolver($notCalled);
169 | $this->formGenerator->createFormBuilder(new Model\Person(), 'work');
170 | }
171 |
172 | public function testFormFieldResolverPriorityMatters()
173 | {
174 | $notCalled = $this->getMockBuilder('Codete\FormGeneratorBundle\FormFieldResolverInterface')
175 | ->getMock();
176 | $notCalled->expects($this->never())->method('supports');
177 | $this->formGenerator->addFormFieldResolver($notCalled);
178 | $this->formGenerator->addFormFieldResolver(new FormFieldResolver\PersonSalaryResolver(), 1);
179 | $this->formGenerator->createFormBuilder(new Model\Person(), 'work');
180 | }
181 |
182 | /**
183 | * @depends testFormFieldResolverPriorityMatters
184 | */
185 | public function testFormFieldResolverPriorityAfterAnotherAdd()
186 | {
187 | $called = $this->getMockBuilder('Codete\FormGeneratorBundle\FormFieldResolverInterface')
188 | ->getMock();
189 | $called->expects($this->once())->method('supports');
190 | $this->formGenerator->addFormFieldResolver($called, 5);
191 | $this->formGenerator->createFormBuilder(new Model\Person(), 'work');
192 | }
193 |
194 | public function testFormAnnotationViewIsInherited()
195 | {
196 | $this->checkForm(new Model\Director(), ['title', 'name', 'surname', 'photo', 'active'], null, 'personal');
197 | }
198 |
199 | public function testFormAnnotationViewCanBeOverridden()
200 | {
201 | $this->checkForm(new Model\Director(), ['salary', 'department'], null, 'work');
202 | }
203 |
204 | public function testAllParentsAreCheckedForDefaultFormView()
205 | {
206 | $this->checkForm(new Model\InheritanceTest(), ['title', 'author']);
207 | }
208 |
209 | public function testFormViewCanAffectClassLevelFields()
210 | {
211 | $this->checkForm(new Model\ClassLevelFields(), ['submit', 'title'], function($phpunit, $form) {
212 | $normal = $form->get('submit')->getConfig()->getOptions();
213 | $phpunit->assertEquals('Click me', $normal['label']);
214 | }, 'tweaked');
215 | }
216 |
217 | protected function checkForm($model, $expectedFields, callable $additionalCheck = null, $form = 'default', $context = [])
218 | {
219 | $form = $this->formGenerator->createFormBuilder($model, $form, $context)->getForm();
220 | $this->assertEquals(count($expectedFields), count($form));
221 | $cnt = 0;
222 | foreach ($form as $field) {
223 | $this->assertEquals($field->getName(), $expectedFields[$cnt++]);
224 | }
225 | if ($additionalCheck !== null) {
226 | $additionalCheck($this, $form);
227 | }
228 | }
229 |
230 | public function testEmbedForms()
231 | {
232 | $sp = new SimpleParent();
233 |
234 | $sp->employee = new Person();
235 | $sp->named = new Person();
236 |
237 | $sp->employee->salary = 1390.86;
238 | $sp->named->active = false;
239 |
240 | $sp->person = new Person('Bar', 'Baz');
241 |
242 | $fb = $this->formFactoryBuilder
243 | ->addExtension($this->embedTypeExtension)
244 | ->getFormFactory()
245 | ->createBuilder(FormType::class, $sp);
246 |
247 | $this->formGenerator->addFormConfigurationModifier(new NoPhotoPersonModifier());
248 | $this->formGenerator->addFormConfigurationModifier(new InactivePersonModifier());
249 |
250 | $this->formGenerator->populateFormBuilder($fb, $sp);
251 | $form = $fb->getForm();
252 | $this->assertEquals(count(['person', 'noName', 'anonymous', 'employee']), count($form));
253 | $this->assertEquals(count(['title', 'name', 'surname', 'photo', 'active', 'salary']), $form->get('person')->count());
254 | $this->assertEquals(count(['title', 'name', 'surname', 'photo', 'active']), $form->get('named')->count());
255 | $this->assertEquals(count(['title', 'name', 'surname', 'active', 'salary']), $form->get('anonymous')->count());
256 | $this->assertEquals(count(['salary']), $form->get('employee')->count());
257 | $this->assertEquals(1390.86, $form->get('employee')->get('salary')->getData());
258 | $this->assertNull($form->get('anonymous')->get('name')->getData());
259 | $this->assertEquals('Foo', $form->get('named')->get('name')->getData());
260 | $this->assertEquals('Bar', $form->get('person')->get('name')->getData());
261 | $this->assertEquals('Baz', $form->get('person')->get('surname')->getData());
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/Tests/FormViewProvider/PersonAddFormView.php:
--------------------------------------------------------------------------------
1 | name = $name;
62 | $this->surname = $surname;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Tests/Model/Simple.php:
--------------------------------------------------------------------------------
1 | `~1.2`
11 | - symfony/form `>=2.7` -> `~3.4|~4.0`
12 | - symfony/framework-bundle `>=2.7` -> `~3.4|~4.0`
13 |
14 | ## `@Form\Display` annotation has been removed
15 |
16 | With introduction of class-level field annotations the name "Display" no longer made
17 | sense, please use `@Form\Field` from now on. We understand inconvenience such change
18 | is causing but updating this goes hand in hand with another breaking change that may
19 | require your attention:
20 |
21 | ## `@Form\Field` no longer specifies required=false by default
22 |
23 | Assuming this setting is not right for several reasons, not being on par with Symfony's
24 | defaults is one of them. While changing `@Form\Display` to `@Form\Field` annotation
25 | please do check if you need to add `required=false` setting to your annotations.
26 |
27 | ## Symfony2 form types are no longer allowed
28 |
29 | With 1.x version of library it was possible to use names of form types and even when
30 | using newer version, the FormGeneratorBundle was mapping them to their FQCN counterparts:
31 |
32 | ```php
33 | /**
34 | * @Form\Field(type="choice", choices = { "Mr." = "mr", "Ms." = "ms" }, "attr" = { "class" = "foo" })
35 | */
36 | public $title;
37 | ```
38 |
39 | Each and every `type` must now contain the FQCN of the type to be used. We suggest to go
40 | with the `::class` notation:
41 |
42 | ```php
43 | use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
44 |
45 | /**
46 | * @Form\Field(type=ChoiceType::class, choices = { "Mr." = "mr", "Ms." = "ms" }, "attr" = { "class" = "foo" })
47 | */
48 | public $title;
49 | ```
50 |
51 | ## ChoiceType's choices format has been changed
52 |
53 | Although this change does not come from the bundle itself, it's a notable one. Please mind
54 | that Symfony changed the `choices` array format from `value => label` to `label => value`.
55 | Also Symfony 4.0 removed the `choices_as_values` flag.
56 |
57 | ## "form_generator" service is no longer available
58 |
59 | You can now obtain it using `Codete\FormGeneratorBundle\FormGenerator::class` name,
60 | or in a following way in a container aware environment like a controller:
61 |
62 | ``` php
63 | use Codete\FormGeneratorBundle\FormGenerator;
64 |
65 | $generator = $this->get(FormGenerator::class);
66 | ```
67 |
68 | For the record `form_generator.type.embed` is also no longer available.
69 |
70 | ## Other changes
71 |
72 | - The `Codete\FormGeneratorBundle\Form\Type\EmbedType::TYPE` constant has been removed
73 | as it no longer served any purpose
74 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "codete/form-generator-bundle",
3 | "type": "symfony-bundle",
4 | "description": "Symfony Bundle for dynamic Form generation",
5 | "keywords": ["form generator", "dynamic form"],
6 | "homepage": "http://codete.com",
7 | "license": "MIT",
8 | "authors": [
9 | {"name": "Maciej Malarz", "email": "malarzm@gmail.com"}
10 | ],
11 | "require": {
12 | "php": "^7.1",
13 | "doctrine/annotations": "~1.2",
14 | "symfony/form": "~3.4|~4.0",
15 | "symfony/framework-bundle": "~3.4|~4.0",
16 | "doctrine/instantiator": "^1.0"
17 | },
18 | "require-dev": {
19 | "symfony/expression-language": "~3.4|~4.0",
20 | "phpunit/phpunit": "^6.4"
21 | },
22 | "autoload": {
23 | "psr-4": {
24 | "Codete\\FormGeneratorBundle\\": ""
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 | ./Tests/
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------