-
-
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'; | |
} | |
} |
Thank you so much, you save my life!!! 😭
Thank you. I've been looking for this for so long.
Any advice on how to use it with sonata_formatter_type
?
@peter-gribanov Thank you man!
All work in Symfony 4/Flex with validator fix.
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.
@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.
@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
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
So uh, it's strange. Doctrine trying to insert into object field, but not object_id, and I'm getting no column object