Skip to content

Instantly share code, notes, and snippets.

@nnmer
Forked from noetix/TranslatedFieldType.php
Last active May 22, 2019 09:20
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nnmer/d08d5a8db7346db7df5e to your computer and use it in GitHub Desktop.
Save nnmer/d08d5a8db7346db7df5e to your computer and use it in GitHub Desktop.
Translated Field Type for Symfony 2.8
<?php
namespace Acme\DemoBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
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_SUBMIT => 'postSubmit',
FormEvents::SUBMIT => 'submit'
);
}
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(is_object($Translation) && 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 submit(FormEvent $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 postSubmit(FormEvent $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.default_entity_manager')->remove($Translation);
}
}
}
elseif(NULL !== $content)
{
//add it to entity
$entity->addTranslation($Translation);
if(! $data->contains($Translation))
{
$data->add($Translation);
}
}
}
foreach ($data as $rec) { // remove string elements from "translations", we need only objects
if (!is_object($rec)){
$data->removeElement($rec);
continue;
}
if (is_array($this->options['required_locale']) && count($this->options['required_locale'])>0) {
if ($rec->getLocale() == $this->options['required_locale'][0]){
$method = "set".ucfirst($rec->getField());
$entity->$method($rec->getContent());
}
}
}
}
public function preSetData(FormEvent $event)
{
//Builds the custom 'form' based on the provided locales
$data = $event->getData();
$form = $event->getForm();
$entity = $form->getParent()->getData();
// 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)
{
$filedValue = $binded['translation']->getContent();
if (is_array($this->options['required_locale']) && count($this->options['required_locale'])>0) {
if ($binded['translation']->getLocale() == $this->options['required_locale'][0] && null == $filedValue) {
$method = "get" . ucfirst($this->options['field']);
$filedValue = $entity->$method();
}
}
$form->add($this->factory->createNamed(
$binded['fieldName'],
$this->options['widget'],
$filedValue,
array(
'auto_initialize'=> false,
'label' => $binded['locale'],
'required' => in_array($binded['locale'], $this->options['required_locale']),
'property_path'=> null,
'attr' => $this->options['attr'],
)
));
}
}
}
<?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>
services:
form.type.translatable:
class: Acme\DemoBundle\Form\TranslatedFieldType
arguments: [ @service_container ]
tags:
- { name: form.type, alias: translatable_field }
<?php
namespace Acme\DemoBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use SMPlatform\CommonBundle\Form\EventListener\AddTranslatedFieldSubscriber;
use Symfony\Component\OptionsResolver\OptionsResolver;
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 configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults($this->getDefaultOptions());
}
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', 'zh_CN', 'zh_HK'); //the locales you wish to edit
$options['required_locale'] = array($this->container->getParameter('locale')); //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
$options['attr'] = [];
return $options;
}
public function getName()
{
return 'translatable_field';
}
}
@nnmer
Copy link
Author

nnmer commented Dec 18, 2015

Use as follows:

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

Update (2016-05-07):

  • add attr option for the field
  • required_locale array take only 1 value from parameters.yml locale
  • fix save the default locale values (default is the first value from required_locale array) to the original entity
  • when edit entity with translations and if default locale translations does not exist (default is the first value from required_locale array) then take the content from original entity translatable fields, if content available

@fliespl
Copy link

fliespl commented Feb 16, 2016

Is there anyway to add attributes to the widget? I.e.

'attr' => array('class' => 'tinymce'),

@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.

@WildChildForLife
Copy link

I have another question : Actually by following your gist, my translations get nicely inserted in the database, but not the values of the default locale, which get inserted with NULL value, any lead please ?

@nnmer
Copy link
Author

nnmer commented May 7, 2016

@fliespl added, see updated code

@nnmer
Copy link
Author

nnmer commented May 7, 2016

@WildChildForLife to write and read default values added, see updated code

@nnmer
Copy link
Author

nnmer commented Aug 31, 2016

depends on the requirements, now you can set 'required_locale'=> [] at the form field parameters to allow field translations to be skiped as sometimes field can be empty without any data

@filipe-carvalho
Copy link

filipe-carvalho commented Mar 15, 2018

Hello :)
I need you help, i am using this implemenation of form for backend for symfony 2.8 and i am facing a problem

My problem focus on the sluggable and translatable behavior of doctrine extensions bundle with the implementaion of this form.


MY CASE:

I have an entity (FAQ) with 2 fields translatable ( Implementation of doctrine entensions bunlde - Personal Translation) and 1 slugabble field.
The sluggable field (slug) is also a translatable field

So i have in my database my main table (faq) and my translation table (faq_translation - stores the translation of all fields that are translatable)


THE PROBLEM:

When using this form the sluggable field doesnt crreate a slug into every translation entry.
So i want the slug to be persisted into the translation entity and that is not happening.

Imagine that i have a field question in my table (faq) and my slug depends on the value of the question to generate the slug
Imagine also that i am working with 2 locales (EN and PT) and my EN is my default locale

if i fill the question form field with values in EN and PT locales and submit the form this field will be persisted in the translation entity (faq_translation) but my slug will only be generated for the my default locale (EN) and will not be generated for the other locales that i have


IN the end i will have this in my Database:

[FAQ]
question - example question ?
slug - example-question

[FAQ translation]
field - question
locale - PT
content - questao exemplo ?


What i want to happen:

[FAQ]
question - example question ?
slug - example-question

[FAQ translation]
field - question
locale - PT
content - questao exemplo ?

field - slug
locale - PT
content - questao-exemplo


Can you help me ?
Do You have any solution for my problem

Thanks for the attention :)

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