-
-
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'; | |
| } | |
| } |
So uh, it's strange. Doctrine trying to insert into object field, but not object_id, and I'm getting no column object
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
Thank you. It works.