Skip to content

Instantly share code, notes, and snippets.

@noetix
Created July 18, 2012 00:10
Show Gist options
  • Save noetix/3133085 to your computer and use it in GitHub Desktop.
Save noetix/3133085 to your computer and use it in GitHub Desktop.
Translated Field Type for Symfony 2.1
<?php
namespace Acme\DemoBundle\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'];
return new $className($locale, $field, $content);
}
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(
$binded['fieldName'],
$this->options['widget'],
$binded['translation']->getContent(),
array(
'label' => $binded['locale'],
'required' => in_array($binded['locale'], $this->options['required_locale']),
'property_path'=> false,
)
));
}
}
}
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="form.type.translatable" class="Acme\DemoBundle\Form\TranslatedFieldType">
<tag name="form.type" alias="translatable_field"/>
<argument type="service" id="service_container" />
</service>
</services>
</container>
<?php
namespace Acme\DemoBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Acme\DemoBundle\Form\EventListener\addTranslatedFieldSubscriber;
class TranslatedFieldType extends AbstractType
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function buildForm(FormBuilderInterface $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', 'fr'); //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 'translatable_field';
}
}
@noetix
Copy link
Author

noetix commented Jul 18, 2012

Use as follows:

->add('title', 'translatable_field', array(
    'field'          => 'title',
    'property_path'  => 'translations',
    'personal_translation' => 'Acme\DemoBundle\Entity\ExampleTranslation',
))

@toooni
Copy link

toooni commented Aug 14, 2012

any idea how to use that in a collection? something like: Product <1-many> ProductInformation <1-many-> ProductInformationTranslation. The problem is that the prototype has no fields generated when there is no ProductInformation entry..
i really need a solution there :S

@skonsoft
Copy link

$subscriber = new AddTranslatedFieldSubscriber($builder->getFormFactory(), $this->container, $options);

not

$subscriber = new addTranslatedFieldSubscriber($builder->getFormFactory(), $this->container, $options);

@gintro
Copy link

gintro commented Jan 22, 2013

This approach leaves the default translatable Entity field blank!
Which defeats the purpose of not having extra queries when requesting Entity in default locale.

Also the FormEvents::BIND_NORM_DATA is deprecated since 2.1 see https://github.com/symfony/Form/blob/master/FormEvents.php,
and DataEvent (also deprecated since 2.1) should become Symfony\Component\Form\FormEvent.

I do appreciate the effort you made on this!

@nnmer
Copy link

nnmer commented Dec 18, 2015

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