-
-
Save noetix/3133085 to your computer and use it in GitHub Desktop.
<?php | |
namespace Acme\DemoBundle\Form\EventListener; | |
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(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 bindNormData(DataEvent $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 postBind(DataEvent $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.entity_manager')->remove($Translation); | |
} | |
} | |
} | |
elseif(NULL !== $content) | |
{ | |
//add it to entity | |
$entity->addTranslation($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; | |
} | |
foreach($this->bindTranslations($data) as $binded) | |
{ | |
$form->add($this->factory->createNamed( | |
$binded['fieldName'], | |
$this->options['widget'], | |
$binded['translation']->getContent(), | |
array( | |
'label' => $binded['locale'], | |
'required' => in_array($binded['locale'], $this->options['required_locale']), | |
'property_path'=> false, | |
) | |
)); | |
} | |
} | |
} |
<?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> |
<?php | |
namespace Acme\DemoBundle\Form; | |
use Symfony\Component\Form\AbstractType; | |
use Symfony\Component\Form\FormBuilderInterface; | |
use Symfony\Component\DependencyInjection\ContainerInterface; | |
use Acme\DemoBundle\Form\EventListener\addTranslatedFieldSubscriber; | |
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 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', 'fr'); //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 'translatable_field'; | |
} | |
} |
any idea how to use that in a collection? something like: Product <1-many> ProductInformation <1-many-> ProductInformationTranslation. The problem is that the prototype has no fields generated when there is no ProductInformation entry..
i really need a solution there :S
$subscriber = new AddTranslatedFieldSubscriber($builder->getFormFactory(), $this->container, $options);
not
$subscriber = new addTranslatedFieldSubscriber($builder->getFormFactory(), $this->container, $options);
This approach leaves the default translatable Entity field blank!
Which defeats the purpose of not having extra queries when requesting Entity in default locale.
Also the FormEvents::BIND_NORM_DATA is deprecated since 2.1 see https://github.com/symfony/Form/blob/master/FormEvents.php,
and DataEvent (also deprecated since 2.1) should become Symfony\Component\Form\FormEvent.
I do appreciate the effort you made on this!
adapted for Symfony 2.8
Use as follows: