Skip to content

Instantly share code, notes, and snippets.

@peter-gribanov
Forked from qrizly/TranslatedFieldType.php
Last active June 18, 2021 08:24
Show Gist options
  • Save peter-gribanov/06aeffbf10b94b998fc3 to your computer and use it in GitHub Desktop.
Save peter-gribanov/06aeffbf10b94b998fc3 to your computer and use it in GitHub Desktop.
<?php
namespace ExampleBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Form\FormMapper;
use ExampleBundle\Entity\ExampleTranslation; // is a Personal Translation
class ExampleAdmin extends Admin
{
/**
* @param FormMapper $formMapper
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('title', 'translatable', [
'label' => 'form.example.title.label',
'personal_translation' => ExampleTranslation::class,
'property_path' => 'translations'
])
->add('description', 'translatable', [
'label' => 'form.example.description.label',
'personal_translation' => ExampleTranslation::class,
'property_path' => 'translations',
'field' => 'description',
'attr' => ['rows' => 5],
'widget' => 'textarea'
]);
}
// ...
}
parameters:
locale: 'en'
locales: ['en', 'nl']
services:
form.type.translatable:
class: ExampleBundle\Form\TranslatableType
arguments: [ '@doctrine.orm.default_entity_manager', '@validator', '%locales%', '%locale%' ]
tags:
- { name: form.type, alias: translatable }
<?php
namespace ExampleBundle\Form\Event\Subscriber;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormError;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Doctrine\ORM\EntityManagerInterface;
use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation;
class Translatable implements EventSubscriberInterface
{
/**
* @var FormFactoryInterface
*/
protected $factory;
/**
* @var EntityManagerInterface
*/
protected $em;
/**
* @var ValidatorInterface
*/
protected $validator;
/**
* @var array
*/
protected $options = [];
/**
* @param FormFactoryInterface $factory
* @param EntityManagerInterface $em
* @param ValidatorInterface $validator
* @param array $options
*/
public function __construct(
FormFactoryInterface $factory,
EntityManagerInterface $em,
ValidatorInterface $validator,
array $options
) {
$this->em = $em;
$this->options = $options;
$this->factory = $factory;
$this->validator = $validator;
}
/**
* @return array
*/
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 [
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::POST_SUBMIT => 'postSubmit',
FormEvents::SUBMIT => 'submit'
];
}
/**
* @param array $data
*
* @return array
*/
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 = [];
$available_translations = [];
foreach ($data as $translation) {
/* @var $translation AbstractPersonalTranslation */
if (is_object($translation) &&
strtolower($translation->getField()) == strtolower($this->options['field'])
) {
$available_translations[strtolower($translation->getLocale())] = $translation;
}
}
foreach ($this->getFieldNames() as $locale => $field_name) {
if (isset($available_translations[strtolower($locale)])) {
$translation = $available_translations[strtolower($locale) ];
} else {
$translation = $this->createPersonalTranslation($locale, $this->options['field'], null);
}
$collection[] = [
'locale' => $locale,
'fieldName' => $field_name,
'translation' => $translation,
];
}
return $collection;
}
/**
* @return array
*/
private function getFieldNames()
{
//helper function to generate all field names in format:
// '<locale>' => '<field>:<locale>'
$collection = [];
foreach ($this->options['locales'] as $locale) {
$collection[$locale] = $this->options['field'] . ':' . $locale;
}
return $collection;
}
/**
* @param string $locale
* @param string $field
* @param string $content
*
* @return mixed
*/
private function createPersonalTranslation($locale, $field, $content)
{
// creates a new Personal Translation
$class_name = $this->options['personal_translation'];
return new $class_name($locale, $field, $content);
}
/**
* @param FormEvent $event
*/
public function submit(FormEvent $event)
{
// Validates the submitted form
$form = $event->getForm();
foreach($this->getFieldNames() as $locale => $field_name) {
$content = $form->get($field_name)->getData();
if (null === $content && in_array($locale, $this->options['required_locale'])) {
$form->addError($this->getCannotBeBlankException($this->options['field'], $locale));
} else {
$errors = $this->validator->validate(
$this->createPersonalTranslation($locale, $field_name, $content),
[sprintf('%s:%s', $this->options['field'], $locale)]
);
foreach ($errors as $error) {
$form->addError(new FormError($error->getMessage()));
}
}
}
}
/**
* @param string $field
* @param string $locale
*
* @return FormError
*/
public function getCannotBeBlankException($field, $locale)
{
return new FormError(sprintf('Field "%s" for locale "%s" cannot be blank', $field, $locale));
}
/**
* @param FormEvent $event
*/
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();
/* @var $translation AbstractPersonalTranslation */
$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->em->remove($translation);
}
}
} elseif (null !== $content) {
// add it to entity
$entity->addTranslation($translation);
if (! $data->contains($translation)) {
$data->add($translation);
}
}
}
// remove string elements from "translations", we need only objects
foreach ($data as $rec) {
if (! is_object($rec)){
$data->removeElement($rec);
}
}
}
/**
* @param FormEvent $event
*/
public function preSetData(FormEvent $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) {
/* @var $translation AbstractPersonalTranslation */
$translation = $binded['translation'];
$form->add($this->factory->createNamed(
$binded['fieldName'],
$this->options['widget'],
$translation->getContent(),
[
'auto_initialize'=> false,
'label' => $binded['locale'],
'required' => in_array($binded['locale'], $this->options['required_locale']),
'property_path' => null,
'attr' => $this->options['attr']
]
));
}
}
}
<?php
namespace ExampleBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Doctrine\ORM\EntityManagerInterface;
use ExampleBundle\Form\Event\Subscriber\Translatable;
class TranslatableType extends AbstractType
{
/**
* @var EntityManagerInterface
*/
protected $em;
/**
* @var ValidatorInterface
*/
protected $validator;
/**
* @var array
*/
protected $locales;
/**
* @var string
*/
protected $locale;
/**
* @param EntityManagerInterface $em
* @param ValidatorInterface $validator
* @param array $locales
* @param string $locale
*/
public function __construct(EntityManagerInterface $em, ValidatorInterface $validator, array $locales, $locale)
{
$this->em = $em;
$this->validator = $validator;
$this->locales = $locales;
$this->locale = $locale;
}
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (! class_exists($options['personal_translation'])) {
throw $this->getNoPersonalTranslationException($options['personal_translation']);
}
$options['field'] = $options['field'] ?: $builder->getName();
$builder->addEventSubscriber(
new Translatable($builder->getFormFactory(), $this->em, $this->validator, $options)
);
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults($this->getDefaultOptions());
}
/**
* @param array $options
*
* @return array
*/
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'] = $this->locales; // the locales you wish to edit
$options['required_locale'] = [$this->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;
}
/**
* @param string $translation
*
* @return \InvalidArgumentException
*/
public function getNoPersonalTranslationException($translation)
{
return new \InvalidArgumentException(sprintf('Unable to find personal translation class: "%s"', $translation));
}
/**
* @return string
*/
public function getName()
{
return 'translatable';
}
}
@Bukashk0zzz
Copy link

Bukashk0zzz commented Jan 28, 2018

@peter-gribanov Thank you man!

All work in Symfony 4/Flex with validator fix.

@StefanGla
Copy link

How can I define different attributes to personal translations fields? I my case, I want to be that german and english is disabled but french should be editable. Currently all attributes are the same on each translated field, but I need some differences between them.

@Nugjii
Copy link

Nugjii commented Jan 12, 2021

@peter-gribanov Thank you man!

All work in Symfony 4/Flex with validator fix.

In Symfony 4.4 i have following error. How can i solve it, please suggest solution.

In DefinitionErrorExceptionPass.php line 54:

Cannot autowire service "App\Form\Event\Subscriber\Translatable": argument "$options" of method "__construct()" is
type-hinted "array", you should configure its value explicitly.

@peter-gribanov
Copy link
Author

@Nugjii this subscriber does not need to be configured as a service. It is used explicitly in a form type.

https://gist.github.com/peter-gribanov/06aeffbf10b94b998fc3#file-translatabletype-php-L61

@Muspi
Copy link

Muspi commented Jun 18, 2021

Hi, I'm trying to use this form via an api call (JSON format).

I try to send following data:
{ "name":{ "fr":"name fr", "en": "name en" } }

But i got this error:
Child "name:en" does not exist.

Any help please?
Thanks

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