Skip to content

Instantly share code, notes, and snippets.

@qrizly
Created April 21, 2012 13:29
Show Gist options
  • Save qrizly/2437078 to your computer and use it in GitHub Desktop.
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';
}
}
@l3pp4rd
Copy link

l3pp4rd commented Apr 21, 2012

nice job!, could you contribute this to the extension documentation?

@qrizly
Copy link
Author

qrizly commented Jun 10, 2012

Use it as follow:

            ->add('title', 'translatable_field', array(
                'field'          => 'title',
                'property_path'  => 'translations',
                'personal_translation' => 'ACME\\ExampleBundle\\Entity\\PersonalTranslation\\ProductTranslation',
            ))

@toooni
Copy link

toooni commented Jul 10, 2012

it seems that this isn't working in sf 2.1-beta2 (Could not load type "field|de") . is there a workaround?

@qrizly
Copy link
Author

qrizly commented Jul 10, 2012

i havent used sf 2.1 so far, but i'll look into it

@toooni
Copy link

toooni commented Jul 12, 2012

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 :)

@toooni
Copy link

toooni commented Jul 12, 2012

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

@noetix
Copy link

noetix commented Jul 18, 2012

Here is what I had to do to get it working in Symfony 2.1:

https://gist.github.com/3133085

@bampnet
Copy link

bampnet commented Jul 10, 2013

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'],
            )
        ));
}

}

@lekeutch
Copy link

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?

@salbers79
Copy link

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.

@nnmer
Copy link

nnmer commented Dec 18, 2015

@peter-gribanov
Copy link

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)

@Ronan-Lenor
Copy link

To handle the Collection type, you can use the peter-gribanov fork (above)
and Add the script that i posted in its thread

@WildChildForLife
Copy link

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.

@jazithedev
Copy link

How to make it work with @Assert from entities?

@Roman9961
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment