-
-
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'; | |
} | |
} |
Use it as follow:
->add('title', 'translatable_field', array(
'field' => 'title',
'property_path' => 'translations',
'personal_translation' => 'ACME\\ExampleBundle\\Entity\\PersonalTranslation\\ProductTranslation',
))
it seems that this isn't working in sf 2.1-beta2 (Could not load type "field|de") . is there a workaround?
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
nice job!, could you contribute this to the extension documentation?