Skip to content

Instantly share code, notes, and snippets.

@pakaufmann
Forked from qrizly/TranslatedFieldType.php
Created May 15, 2012 13:16
Show Gist options
  • Save pakaufmann/2701676 to your computer and use it in GitHub Desktop.
Save pakaufmann/2701676 to your computer and use it in GitHub Desktop.
Changed a couple of things. Languages are now dynamic and a default language can be used which won't be written into a translation object but directly into the object itself so it can be used like in this blog post: http://knplabs.fr/blog/I%28blah...blah.
<?php
namespace Edge5\TestprojectBundle\Form;
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 the translations object is null, skip it (happens when a subform is called on a new entity)
if($Translation == null)
{
return;
}
if(strtolower($Translation->getProperty()) == 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->setProperty($field);
$Translation->setValue($content);
return $Translation;
}
public function bindNormData(DataEvent $event)
{
//Validates the submitted form
$data = $event->getData();
$form = $event->getForm();
$validator = $this->container->get('validator');
//if the form count is null, skip it (happens when a subform is called on a new entity)
if($form->count() == 0)
{
return;
}
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 validation then set the corresponding Personal Translations
$form = $event->getForm();
$data = $form->getData();
$entity = $form->getParent()->getData();
//if the entity is null, skip it (happens when the subform is called with a new entity instead of an updated one)
if($entity == null)
{
return;
}
foreach($this->bindTranslations($data) as $binded)
{
$content = $form->get($binded['fieldName'])->getData();
$Translation = $binded['translation'];
//if default translation don't add the translation, instead write it directly into the entity
if($Translation->getLocale() === $this->options['default_locale'])
{
$setFunction = 'set'.$Translation->getProperty();
$entity->$setFunction($content);
}
else
{
// set the submitted content
$Translation->setValue($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)
{
$class = explode('\\', get_class($Translation));
$translationFunction = 'add'.end($class);
$entity->$translationFunction($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;
}
$entity = $form->getParent()->getData();
foreach($this->bindTranslations($data) as $binded)
{
$t = $binded['translation'];
if($t->getLocale() === $this->options['default_locale'])
{
//get it out of the actual entity instead of the translation object
$getFunction = 'get'.$t->getProperty();
$translation = $entity->$getFunction();
}
else
{
$translation = $binded['translation']->getValue();
}
$form->add($this->factory->createNamed(
$this->options['widget'],
$binded['fieldName'],
$translation,
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 Edge5\TestProjectBundle\Form\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['default_locale'] = 'en'; //the default language to use
$options['csrf_protection'] = false;
$options['personal_translation'] = false; //Personal Translation class
$options['locales'] = array('en'); //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';
}
}
@Tii
Copy link

Tii commented Jun 22, 2012

Hello,
I followed the same knp blogpost and I used your file but I keep getting this error :

An exception has been thrown during the rendering of a template ("Notice: Undefined offset: -1 in /.../vendor/symfony/src/Symfony/Bridge/Twig/Extension/FormExtension.php line 245") in SonataAdminBundle:Form:form_admin_fields.html.twig at line 81.

when I try to edit my translatable entity in sonata admin !

I really don't get it, any idea ?

@pakaufmann
Copy link
Author

Yeah, I know. That's a problem of in the current release of the Sonata Admin generator. They broke the display behaviour of 1 to n relations (which also broke this field type) . You can resolve it with the following code:

Replace the following lines in the block "field_row" in the file vendor/bundles/Sonata/AdminBundle/Resources/views/Form/form_admin_fields.html.twig.

{% if sonata_admin is not defined or not sonata_admin_enabled or not sonata_admin.field_description %}
    {% form_row(form) %}
{% else %}

with the following code:

{% if sonata_admin is not defined or not sonata_admin_enabled or not sonata_admin.field_description %}
    <div class="clearfix{% if errors|length > 0%} error{%endif%}" id="sonata-ba-field-container-{{ id }}">
        {{ form_label(form) }}

        <div class="input">
            {{ form_widget(form) }}

            {% if errors|length > 0 %}
                <div class="sonata-ba-field-error-messages">
                    {{ form_errors(form) }}
                </div>
            {% endif %}
        </div>
    </div>
{% else %}

This should fix it.

@Tii
Copy link

Tii commented Jun 24, 2012 via email

@pakaufmann
Copy link
Author

Hm, I never had a problem with the translation relation not getting set correctly.

What does your entity mapping information look like?

@Tii
Copy link

Tii commented Jun 25, 2012 via email

@pakaufmann
Copy link
Author

Yeah sorry, my bad. I had this problem with all one-to-many relationships in Sonata. I fixed this problem by overwriting the prePersist and preUpdate methods in the Sonata Admin class. There I loop over all translations and set the translatable to the correct entity.

@velikanov
Copy link

and I keep getting
The options "field", "personal_translation" do not exist

weird… what am I doing wrong?

@velikanov
Copy link

oh, I've just returned an array in setDefaultOptions :)
you need to do $resolver->setDefaults();

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