-
-
Save qrizly/2437078 to your computer and use it in GitHub Desktop.
| <?php | |
| namespace ExampleBundle\Form\EventListener; | |
| use Symfony\Component\Form\Event\DataEvent; | |
| use Symfony\Component\Form\FormFactoryInterface; | |
| use Symfony\Component\EventDispatcher\EventSubscriberInterface; | |
| use Symfony\Component\Form\FormEvents; | |
| use Symfony\Component\DependencyInjection\ContainerInterface; | |
| use Symfony\Component\Form\FormError; | |
| class AddTranslatedFieldSubscriber implements EventSubscriberInterface | |
| { | |
| private $factory; | |
| private $options; | |
| private $container; | |
| public function __construct(FormFactoryInterface $factory, ContainerInterface $container, Array $options) | |
| { | |
| $this->factory = $factory; | |
| $this->options = $options; | |
| $this->container = $container; | |
| } | |
| public static function getSubscribedEvents() | |
| { | |
| // Tells the dispatcher that we want to listen on the form.pre_set_data | |
| // , form.post_data and form.bind_norm_data event | |
| return array( | |
| FormEvents::PRE_SET_DATA => 'preSetData', | |
| FormEvents::POST_BIND => 'postBind', | |
| FormEvents::BIND_NORM_DATA => 'bindNormData' | |
| ); | |
| } | |
| private function bindTranslations($data) | |
| { | |
| //Small helper function to extract all Personal Translation | |
| //from the Entity for the field we are interested in | |
| //and combines it with the fields | |
| $collection = array(); | |
| $availableTranslations = array(); | |
| foreach($data as $Translation) | |
| { | |
| if(strtolower($Translation->getField()) == strtolower($this->options['field'])) | |
| { | |
| $availableTranslations[ strtolower($Translation->getLocale()) ] = $Translation; | |
| } | |
| } | |
| foreach($this->getFieldNames() as $locale => $fieldName) | |
| { | |
| if(isset($availableTranslations[ strtolower($locale) ])) | |
| { | |
| $Translation = $availableTranslations[ strtolower($locale) ]; | |
| } | |
| else | |
| { | |
| $Translation = $this->createPersonalTranslation($locale, $this->options['field'], NULL); | |
| } | |
| $collection[] = array( | |
| 'locale' => $locale, | |
| 'fieldName' => $fieldName, | |
| 'translation' => $Translation, | |
| ); | |
| } | |
| return $collection; | |
| } | |
| private function getFieldNames() | |
| { | |
| //helper function to generate all field names in format: | |
| // '<locale>' => '<field>|<locale>' | |
| $collection = array(); | |
| foreach($this->options['locales'] as $locale) | |
| { | |
| $collection[ $locale ] = $this->options['field'] ."|". $locale; | |
| } | |
| return $collection; | |
| } | |
| private function createPersonalTranslation($locale, $field, $content) | |
| { | |
| //creates a new Personal Translation | |
| $className = $this->options['personal_translation']; | |
| $Translation = new $className(); | |
| $Translation->setLocale($locale); | |
| $Translation->setField($field); | |
| $Translation->setContent($content); | |
| return $Translation; | |
| } | |
| public function bindNormData(DataEvent $event) | |
| { | |
| //Validates the submitted form | |
| $data = $event->getData(); | |
| $form = $event->getForm(); | |
| $validator = $this->container->get('validator'); | |
| foreach($this->getFieldNames() as $locale => $fieldName) | |
| { | |
| $content = $form->get($fieldName)->getData(); | |
| if( | |
| NULL === $content && | |
| in_array($locale, $this->options['required_locale'])) | |
| { | |
| $form->addError(new FormError(sprintf("Field '%s' for locale '%s' cannot be blank", $this->options['field'], $locale))); | |
| } | |
| else | |
| { | |
| $Translation = $this->createPersonalTranslation($locale, $fieldName, $content); | |
| $errors = $validator->validate($Translation, array(sprintf("%s:%s", $this->options['field'], $locale))); | |
| if(count($errors) > 0) | |
| { | |
| foreach($errors as $error) | |
| { | |
| $form->addError(new FormError($error->getMessage())); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| public function postBind(DataEvent $event) | |
| { | |
| //if the form passed the validattion then set the corresponding Personal Translations | |
| $form = $event->getForm(); | |
| $data = $form->getData(); | |
| $entity = $form->getParent()->getData(); | |
| foreach($this->bindTranslations($data) as $binded) | |
| { | |
| $content = $form->get($binded['fieldName'])->getData(); | |
| $Translation = $binded['translation']; | |
| // set the submitted content | |
| $Translation->setContent($content); | |
| //test if its new | |
| if($Translation->getId()) | |
| { | |
| //Delete the Personal Translation if its empty | |
| if( | |
| NULL === $content && | |
| $this->options['remove_empty'] | |
| ) | |
| { | |
| $data->removeElement($Translation); | |
| if($this->options['entity_manager_removal']) | |
| { | |
| $this->container->get('doctrine.orm.entity_manager')->remove($Translation); | |
| } | |
| } | |
| } | |
| elseif(NULL !== $content) | |
| { | |
| //add it to entity | |
| $entity->addTranslation($Translation); | |
| if(! $data->contains($Translation)) | |
| { | |
| $data->add($Translation); | |
| } | |
| } | |
| } | |
| } | |
| public function preSetData(DataEvent $event) | |
| { | |
| //Builds the custom 'form' based on the provided locales | |
| $data = $event->getData(); | |
| $form = $event->getForm(); | |
| // During form creation setData() is called with null as an argument | |
| // by the FormBuilder constructor. We're only concerned with when | |
| // setData is called with an actual Entity object in it (whether new, | |
| // or fetched with Doctrine). This if statement let's us skip right | |
| // over the null condition. | |
| if (null === $data) | |
| { | |
| return; | |
| } | |
| foreach($this->bindTranslations($data) as $binded) | |
| { | |
| $form->add($this->factory->createNamed( | |
| $this->options['widget'], | |
| $binded['fieldName'], | |
| $binded['translation']->getContent(), | |
| array( | |
| 'label' => $binded['locale'], | |
| 'required' => in_array($binded['locale'], $this->options['required_locale']), | |
| 'property_path'=> false, | |
| ) | |
| )); | |
| } | |
| } | |
| } |
| services: | |
| form.type.translatable: | |
| class: ExampleBundle\Form\TranslatedFieldType | |
| arguments: [ @service_container ] | |
| tags: | |
| - { name: form.type, alias: translatable_field } |
| <?php | |
| namespace ExampleBundle\Form; | |
| use Symfony\Component\Form\AbstractType; | |
| use Symfony\Component\Form\FormBuilder; | |
| use Symfony\Component\DependencyInjection\ContainerInterface; | |
| use ExampleBundle\Form\EventListener\addTranslatedFieldSubscriber; | |
| class TranslatedFieldType extends AbstractType | |
| { | |
| protected $container; | |
| public function __construct(ContainerInterface $container) | |
| { | |
| $this->container = $container; | |
| } | |
| public function buildForm(FormBuilder $builder, array $options) | |
| { | |
| if(! class_exists($options['personal_translation'])) | |
| { | |
| Throw new \InvalidArgumentException(sprintf("Unable to find personal translation class: '%s'", $options['personal_translation'])); | |
| } | |
| if(! $options['field']) | |
| { | |
| Throw new \InvalidArgumentException("You should provide a field to translate"); | |
| } | |
| $subscriber = new addTranslatedFieldSubscriber($builder->getFormFactory(), $this->container, $options); | |
| $builder->addEventSubscriber($subscriber); | |
| } | |
| public function getDefaultOptions(array $options = array()) | |
| { | |
| $options['remove_empty'] = true; //Personal Translations without content are removed | |
| $options['csrf_protection'] = false; | |
| $options['personal_translation'] = false; //Personal Translation class | |
| $options['locales'] = array('en', 'nl'); //the locales you wish to edit | |
| $options['required_locale'] = array('en'); //the required locales cannot be blank | |
| $options['field'] = false; //the field that you wish to translate | |
| $options['widget'] = "text"; //change this to another widget like 'texarea' if needed | |
| $options['entity_manager_removal'] = true; //auto removes the Personal Translation thru entity manager | |
| return $options; | |
| } | |
| public function getName() | |
| { | |
| return 'translator'; | |
| } | |
| } |
i havent used sf 2.1 so far, but i'll look into it
for use in 2.1, there are the following changes in addTranslatedFieldSubscriber.php:
Line 81:
$collection[ $locale ] = $this->options['field'] ."|". $locale;
to
$collection[ $locale ] = $this->options['field'] .":". $locale;
Line 199-201:
$form->add($this->factory->createNamed(
$this->options['widget'],
$binded['fieldName'],
to
$form->add($this->factory->createNamed(
$binded['fieldName'],
$this->options['widget'],
btw. thanks for the awesome work :)
now i have a question again..
does anybody use this in a collection? becuase, if this form is created in the subscriber there is just an empty prototype.. :S
Here is what I had to do to get it working in Symfony 2.1:
If you want to use the "attr" index in the field builder, you need to edit preSetData method:
public function preSetData(DataEvent $event)
{
//Builds the custom 'form' based on the provided locales
$data = $event->getData();
$form = $event->getForm();
// During form creation setData() is called with null as an argument
// by the FormBuilder constructor. We're only concerned with when
// setData is called with an actual Entity object in it (whether new,
// or fetched with Doctrine). This if statement let's us skip right
// over the null condition.
if (null === $data)
{
return;
}
foreach ($this->bindTranslations($data) as $binded)
{
$form->add($this->factory->createNamed(
$binded['fieldName'], $this->options['widget'], $binded['translation']->getContent(), array(
'label' => $binded['locale'],
'required' => in_array($binded['locale'], $this->options['required_locale']),
'property_path' => false,
'attr' => $this->options['attr'],
)
));
}
}
HI People
I use Symfony 2.6 and the path "Symfony\Component\Form\Event\DataEvent" is bad.
how can i do to use this three files?
Hi lekeutch,
you have to replace the DataEvent with Symfony\Component\Form\FormEvent.
In the TranslatedFieldType class you have to replace the getDefaultOptions method with setDefaultOptions(OptionsResolverInterface $resolver) and return in the getName method "translatable_field". That's all.
adapted for Symfony 2.8
My fork
https://gist.github.com/peter-gribanov/06aeffbf10b94b998fc3
Changes:
- Add support Symfony 2.7 (Symfony 3 not tested, but should work)
- Correct code style to PSR
- Add annotations for autocomplete in IDE
- Add attr option (can change CSS class for field)
- Rename field translatable_field -> translatable
- Rename translatable classes
- ExampleBundle\Form\EventListener\AddTranslatedFieldSubscriber -> ExampleBundle\Form\Event\Subscriber\Translatable
- ExampleBundle\Form\TranslatedFieldType -> ExampleBundle\Form\TranslatableType
- Get supported locales from service parameters
- Not use DI container
- Add example usage
- Auto set the field option from the form field name (_field_ option is not a required)
To handle the Collection type, you can use the peter-gribanov fork (above)
and Add the script that i posted in its thread
First, thank you all for your awesome work.
My question : Is there a way to specify a global template renderer for the translatable fields ?
Thank you.
How to make it work with @Assert from entities?
Hi! I need to set original field to null. I set required =>false but every time I do submit with empty translations the original filed didn't changed
it seems that this isn't working in sf 2.1-beta2 (Could not load type "field|de") . is there a workaround?