├── .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 | [![Build Status](https://travis-ci.org/codete/FormGeneratorBundle.svg?branch=master)](https://travis-ci.org/codete/FormGeneratorBundle) 5 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/8893e0c9-ed68-498e-aa86-63320ac43a62/mini.png)](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 | --------------------------------------------------------------------------------