我目前正在开发一个 symfony2.3 项目,其中的学说 2 试图在 Sonata 后端实现个人翻译管理。
翻译基于doctrine2可翻译行为模型:https ://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/translatable.md ,更准确地说是个人翻译。
管理表单使用TranslatedFieldType.php的 symfony2.3 版本。
我的实体类如下:
<?php
namespace Hr\OnlineBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @Gedmo\TranslationEntity(class="Hr\OnlineBundle\Entity\CategoryTranslation")
*/
class Category
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @Gedmo\Translatable
* @ORM\Column(length=64)
*/
private $title;
/**
* @Gedmo\Translatable
* @ORM\Column(type="text", nullable=true)
*/
private $description;
/**
* @ORM\OneToMany(
* targetEntity="CategoryTranslation",
* mappedBy="object",
* cascade={"persist", "remove"}
* )
*/
private $translations;
public function __construct()
{
$this->translations = new ArrayCollection();
}
public function getTranslations()
{
return $this->translations;
}
public function addTranslation(CategoryTranslation $t)
{
if (!$this->translations->contains($t)) {
$this->translations[] = $t;
$t->setObject($this);
}
}
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getDescription()
{
return $this->description;
}
public function __toString()
{
return $this->getTitle();
}
}
相关的个人翻译类是这样的:
<?php
namespace Hr\OnlineBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation;
/**
* @ORM\Entity
* @ORM\Table(name="category_translations",
* uniqueConstraints={@ORM\UniqueConstraint(name="lookup_unique_idx", columns={
* "locale", "object_id", "field"
* })}
* )
*/
class CategoryTranslation extends AbstractPersonalTranslation
{
/**
* Convinient constructor
*
* @param string $locale
* @param string $field
* @param string $value
*/
public function __construct($locale, $field, $value)
{
$this->setLocale($locale);
$this->setField($field);
$this->setContent($value);
}
/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="translations")
* @ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $object;
}
管理表单类是这样的:
<?php
namespace Hr\OnlineBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Hr\OnlineBundle\Form\Type\TranslatedFieldType;
class CategoryAdmin extends Admin
{
// Fields to be shown on create/edit forms
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('General')
->add('title', 'translatable_field', array(
'field' => 'title',
'personal_translation' => 'Hr\OnlineBundle\Entity\CategoryTranslation',
'property_path' => 'translations',
))
->end();
}
// Fields to be shown on filter forms
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('title')
;
}
// Fields to be shown on lists
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('title')
;
}
}
管理表单使用以下表单类型类:
<?php
namespace Hr\OnlineBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Hr\OnlineBundle\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 setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'remove_empty' => true,
'csrf_protection'=> false,
'field' => false,
'personal_translation' => false,
'locales'=>array('en', 'fr', 'de'),
'required_locale'=>array('en'),
'widget'=>'text',
'entity_manager_removal'=>true,
));
}
public function getName()
{
return 'translatable_field';
}
}
和相应的事件监听器:
<?php
namespace Hr\OnlineBundle\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\Form\FormEvent;
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 => '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(FormEvent $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(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();
$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(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)
{
$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']),
'auto_initialize' => false,
)
));
}
}
}
因此,表单显示了三个指定语言环境的三个字段,这很好,但是在提交表单时会出现以下错误:
FatalErrorException: Error: Call to a member function getField() on a non-object in C:\wamp\www\hronline\src\Hr\OnlineBundle\Form\EventListener\addTranslatedFieldSubscriber.php line 47
in C:\wamp\www\hronline\src\Hr\OnlineBundle\Form\EventListener\addTranslatedFieldSubscriber.php line 47
at ErrorHandler->handleFatal() in C:\wamp\www\hronline\vendor\symfony\symfony\src\Symfony\Component\Debug\ErrorHandler.php line 0
at AddTranslatedFieldSubscriber->bindTranslations() in C:\wamp\www\hronline\src\Hr\OnlineBundle\Form\EventListener\addTranslatedFieldSubscriber.php line 136
at AddTranslatedFieldSubscriber->postBind() in C:\wamp\www\hronline\app\cache\dev\classes.php line 1667
at ??call_user_func() in C:\wamp\www\hronline\app\cache\dev\classes.php line 1667
at EventDispatcher->doDispatch() in C:\wamp\www\hronline\app\cache\dev\classes.php line 1600
at EventDispatcher->dispatch() in C:\wamp\www\hronline\vendor\symfony\symfony\src\Symfony\Component\EventDispatcher\ImmutableEventDispatcher.php line 42
at ImmutableEventDispatcher->dispatch() in C:\wamp\www\hronline\vendor\symfony\symfony\src\Symfony\Component\Form\Form.php line 631
at Form->submit() in C:\wamp\www\hronline\vendor\symfony\symfony\src\Symfony\Component\Form\Form.php line 552
at Form->submit() in C:\wamp\www\hronline\vendor\symfony\symfony\src\Symfony\Component\Form\Form.php line 645
at Form->bind() in C:\wamp\www\hronline\vendor\sonata-project\admin-bundle\Sonata\AdminBundle\Controller\CRUDController.php line 498
at CRUDController->createAction() in C:\wamp\www\hronline\app\bootstrap.php.cache line 2844
at ??call_user_func_array() in C:\wamp\www\hronline\app\bootstrap.php.cache line 2844
at HttpKernel->handleRaw() in C:\wamp\www\hronline\app\bootstrap.php.cache line 2818
at HttpKernel->handle() in C:\wamp\www\hronline\app\bootstrap.php.cache line 2947
at ContainerAwareHttpKernel->handle() in C:\wamp\www\hronline\app\bootstrap.php.cache line 2249
at Kernel->handle() in C:\wamp\www\hronline\web\app_dev.php line 28
at ??{main}() in C:\wamp\www\hronline\web\app_dev.php line 0
似乎在事件监听器 AddTranslatedFieldSubscriber 的这一行:
if(strtolower($Translation->getField()) == strtolower($this->options['field']))
$Translation 变量以字符串形式出现(例如字符串'Lorem' (length=5))而不是对象。由于某种原因,表单数据被转换为字符串数组,而不是 CategoryTranslation 类型的对象数组。
这可能是什么原因?谢谢!