-
-
Save pakaufmann/2701676 to your computer and use it in GitHub Desktop.
<?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'; | |
} | |
} |
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.
Hm, I never had a problem with the translation relation not getting set correctly.
What does your entity mapping information look like?
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.
and I keep getting
The options "field", "personal_translation" do not exist
weird… what am I doing wrong?
oh, I've just returned an array in setDefaultOptions :)
you need to do $resolver->setDefaults();
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 ?