Symfony 3 上的 Sonata Admin 实体翻译

Posted

技术标签:

【中文标题】Symfony 3 上的 Sonata Admin 实体翻译【英文标题】:Sonata Admin entities translation on Symfony 3 【发布时间】:2018-01-20 05:53:02 【问题描述】:

有人在 Symfony 3 上成功翻译了 Sonata Admin 实体(实际上我使用的是 3.3)。

我尝试了不同的解决方案,但没有一个真正奏效。 使用 gedmo 翻译,主要问题是在数据库中保存了不同语言的翻译,但是在管理员中(也列出了结尾表单),尽管单击了不同的标志/翻译,但 Sonata 包仅显示默认的语言环境翻译/选择。

我也尝试使用 KNP tarnslation 捆绑包和 A2lix 翻译,但是这两个有相同的问题:当您(在管理类中)将一个字段设置为“可排序”时,然后在您尝试排序时在记录列表中通过该字段,Symfony 抛出错误,因为翻译系统试图创建与另一个不存在的字段的关联!

无论如何,留在 Gedmo 灵魂,主要问题是(因为我已经提到的问题,将 A2lix 解决方案分开)我不知道如何在管理类中将字段设置为可翻译(BlogPostAdmin.php)因为仅使用配置文件以及实体和翻译类似乎不起作用。如前所述,问题在于翻译保存在数据库中,但未显示在管理列表/表单中。

这是我的配置和实体文件:

AppKernel.php

<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel

    public function registerBundles()
    
        $bundles = [
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new Symfony\Bundle\SecurityBundle\SecurityBundle(),
            new Symfony\Bundle\TwigBundle\TwigBundle(),
            new Symfony\Bundle\MonologBundle\MonologBundle(),
            new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
            new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
            new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
            new AppBundle\AppBundle(),
            /// These are the other bundles the SonataAdminBundle relies on
            new Sonata\CoreBundle\SonataCoreBundle(),
            new Sonata\BlockBundle\SonataBlockBundle(),
            new Knp\Bundle\MenuBundle\KnpMenuBundle(),
            new Sonata\TranslationBundle\SonataTranslationBundle(),

            // And finally, the storage and SonataAdminBundle
            new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
            new Sonata\AdminBundle\SonataAdminBundle(),

            // stof [used in Sonata translations]
            new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),

            // assetic
            new Symfony\Bundle\AsseticBundle\AsseticBundle(),
        ];

        if (in_array($this->getEnvironment(), ['dev', 'test'], true)) 
            $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
            $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
            $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();

            if ('dev' === $this->getEnvironment()) 
                $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
                $bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle();
            
        

        return $bundles;
    

    public function getRootDir()
    
        return __DIR__;
    

    public function getCacheDir()
    
        return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
    

    public function getLogDir()
    
        return dirname(__DIR__).'/var/logs';
    

    public function registerContainerConfiguration(LoaderInterface $loader)
    
        $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
    

config.yml

imports:
    -  resource: parameters.yml 
    -  resource: security.yml 
    -  resource: services.yml 

# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
    locale: it

framework:
    #esi: ~
    translator:  fallbacks: ['%locale%'] 
    secret: '%secret%'
    router:
        resource: '%kernel.project_dir%/app/config/routing.yml'
        strict_requirements: ~
    form: ~
    csrf_protection: ~
    validation:  enable_annotations: true 
    #serializer:  enable_annotations: true 
    templating:
        engines: ['twig']
    default_locale: '%locale%'
    trusted_hosts: ~
    session:
        # https://symfony.com/doc/current/reference/configuration/framework.html#handler-id
        handler_id: session.handler.native_file
        save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
    fragments: ~
    http_method_override: true
    assets: ~
    php_errors:
        log: true

# Twig Configuration
twig:
    debug: '%kernel.debug%'
    strict_variables: '%kernel.debug%'

# Doctrine Configuration
doctrine:
    dbal:
        driver: pdo_mysql
        host: '%database_host%'
        port: '%database_port%'
        dbname: '%database_name%'
        user: '%database_user%'
        password: '%database_password%'
        charset: UTF8
        # if using pdo_sqlite as your database driver:
        #   1. add the path in parameters.yml
        #     e.g. database_path: "%kernel.project_dir%/var/data/data.sqlite"
        #   2. Uncomment database_path in parameters.yml.dist
        #   3. Uncomment next line:
        #path: '%database_path%'

    orm:
        auto_generate_proxy_classes: '%kernel.debug%'
        naming_strategy: doctrine.orm.naming_strategy.underscore
        auto_mapping: true
#        mappings:
#            # Doctrine extensions
#            translatable:
#                type: annotation
#                alias: Gedmo
#                prefix: Gedmo\Translatable\Entity
#                dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity/MappedSuperclass"

# Swiftmailer Configuration
swiftmailer:
    transport: '%mailer_transport%'
    host: '%mailer_host%'
    username: '%mailer_user%'
    password: '%mailer_password%'
    spool:  type: memory 

sonata_block:
    default_contexts: [cms]
    blocks:
        # enable the SonataAdminBundle block
        sonata.admin.block.admin_list:
            contexts: [admin]

sonata_translation:
    locales: [it, en]
    default_locale: %locale%
    # here enable the types you need
    gedmo:
         enabled: true
#    knplabs:
#        enabled: true
    #phpcr:
    #    enabled: true

sonata_admin:
    templates:
        layout: admin/layout.html.twig

assetic:
    debug:          '%kernel.debug%'
    use_controller: '%kernel.debug%'
    filters:
        cs-s-rewrite: ~

#stof_doctrine_extensions:
#    #default_locale: %locale%
#    orm:
#        default:
#            sluggable: true
#            timestampable: true

services.yml

# Learn more about services, parameters and containers at
# https://symfony.com/doc/current/service_container.html
parameters:
    locale: 'it'
    locales: ['it', 'en']

services:
    # default configuration for services in *this* file
    _defaults:
        # automatically injects dependencies in your services
        autowire: true
        # automatically registers your services as commands, event subscribers, etc.
        autoconfigure: true
        # this means you cannot fetch services directly from the container via $container->get()
        # if you need to do this, you can override this setting on individual services
        public: false

    # makes classes in src/AppBundle available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    AppBundle\:
        resource: '../../src/AppBundle/*'
        # you can exclude directories or files
        # but if a service is unused, it's removed anyway
        exclude: '../../src/AppBundle/Entity,Repository,Tests'

    # controllers are imported separately to make sure they're public
    # and have a tag that allows actions to type-hint services
    AppBundle\Controller\:
        resource: '../../src/AppBundle/Controller'
        public: true
        tags: ['controller.service_arguments']

    # add more services, or override services that need manual wiring
    # AppBundle\Service\ExampleService:
    #     arguments:
    #         $someArgument: 'some_value'

    admin.category:
            class: AppBundle\Admin\CategoryAdmin
            arguments: [~, AppBundle\Entity\Category, ~]
            tags:
                -  name: sonata.admin, manager_type: orm, label: Category 
            public: true

    admin.blog_post:
        class: AppBundle\Admin\BlogPostAdmin
        arguments: [~, AppBundle\Entity\BlogPost, ~]
        tags:
            -  name: sonata.admin, manager_type: orm, label: Blog post 
        public: true


    # Doctrine Extension listeners to handle behaviors
    gedmo.listener.translatable:
        class: Gedmo\Translatable\TranslatableListener
        tags:
            -  name: doctrine.event_subscriber, connection: default 
        calls:
            #- [ setAnnotationReader, [ @annotation_reader ] ]
            - [ setDefaultLocale, [ it ] ]
            - [ setTranslationFallback, [ false ] ]
            - [ setPersistDefaultLocaleTranslation, [ false ] ]

BlogPost.php

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Sonata\TranslationBundle\Model\Gedmo\AbstractPersonalTranslatable;
use Gedmo\Mapping\Annotation as Gedmo;
use Sonata\TranslationBundle\Model\Gedmo\TranslatableInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Sonata\TranslationBundle\Model\Gedmo\AbstractPersonalTranslation;
use Sonata\TranslationBundle\Traits\Gedmo\PersonalTranslatableTrait;

/**
 * BlogPost
 *
 * @ORM\Table(name="blog_post")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\BlogPostRepository")
 * @Gedmo\TranslationEntity(class="AppBundle\Entity\Translations\BlogPostTr")
 * @ORM\HasLifecycleCallbacks
 */

class BlogPost implements TranslatableInterface

    use PersonalTranslatableTrait;

    /**
     * Post locale
     * Used locale to override Translation listener's locale
     *
     * @Gedmo\Locale
     */
    protected $locale;

    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="blogPosts")
     */
    private $category;

    public function setCategory(Category $category)
    
        $this->category = $category;
    

    public function getCategory()
    
        return $this->category;
    

    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="title", type="string", length=255)
     * @Gedmo\Translatable
     */
    private $title;

    /**
     * @var string
     *
     * @ORM\Column(name="body", type="text")
     * @Gedmo\Translatable
     */
    private $body;

    /**
     * @var bool
     *
     * @ORM\Column(name="draft", type="boolean")
     */
    private $draft = false;


    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    
        return $this->id;
    

    /**
     * Set title
     *
     * @param string $title
     *
     * @return BlogPost
     */
    public function setTitle($title)
    
        $this->title = $title;

        return $this;
    

    /**
     * Get title
     *
     * @return string
     */
    public function getTitle()
    
        return $this->title;
    

    /**
     * Set body
     *
     * @param string $body
     *
     * @return BlogPost
     */
    public function setBody($body)
    
        $this->body = $body;

        return $this;
    

    /**
     * Get body
     *
     * @return string
     */
    public function getBody()
    
        return $this->body;
    

    /**
     * Set draft
     *
     * @param boolean $draft
     *
     * @return BlogPost
     */
    public function setDraft($draft)
    
        $this->draft = $draft;

        return $this;
    

    /**
     * Get draft
     *
     * @return bool
     */
    public function getDraft()
    
        return $this->draft;
    

    // TRANSLATION
    /**
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\Translations\BlogPostTr", mappedBy="object", cascade="persist", "remove")
     */
    protected $translations;


    public function __construct()
    
        $this->translations = new ArrayCollection;
    

    public function getTranslations()
    
        return $this->translations;
    

    public function addTranslation(AbstractPersonalTranslation $t)
    
        $this->translations->add($t);
        $t->setObject($this);
    

    public function removeTranslation(AbstractPersonalTranslation $t)
    
        $this->translations->removeElement($t);
    

    public function setTranslations($translations)
    
        $this->translations = $translations;
    

    /**
     * Sets translatable locale
     *
     * @param string $locale
     */
    public function setTranslatableLocale($locale)
    
        $this->locale = $locale;
    

BlogPostTr.php

<?php
namespace AppBundle\Entity\Translations;

use Doctrine\ORM\Mapping as ORM;
use Sonata\TranslationBundle\Model\Gedmo\AbstractPersonalTranslation;

/**
 * @ORM\Entity
 * @ORM\Table(name="blog_post_translation",
 *     uniqueConstraints=@ORM\UniqueConstraint(name="lookup_unique_idx", columns=
 *         "locale", "object_id", "field"
 *     )
 * )
 */
class BlogPostTr extends AbstractPersonalTranslation

    /**
     * Convinient constructor
     *
     * @param string $locale
     * @param string $field
     * @param string $content
     */
    public function __construct($locale = null, $field = null, $content = null)
    
        $this->setLocale($locale);
        $this->setField($field);
        $this->setContent($content);
    

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\BlogPost", inversedBy="translations")
     * @ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE")
     */
    protected $object;

BlogPostAdmin.php

<?php

namespace AppBundle\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;

class BlogPostAdmin extends AbstractAdmin

    protected function configureFormFields(FormMapper $formMapper)
    
        $formMapper
            ->tab('Post')
            ->with('Content', array('class' => 'col-md-9'))
            ->add('title', 'text')
//            ->add('title', 'translatable_field', array(
//                'allow_extra_fields' => true,
//                'field' => 'title',
//                'personal_translation' => 'AppBundle\Entity\Translations\BlogPostTr',
//                'property_path' => 'translations',
//            ))
            ->add('body', 'textarea')
            ->end()
            ->end()
            ->tab('Publishing options')
            ->with('Meta data', array('class' => 'col-md-3'))
            ->add('category', 'sonata_type_model', array(
                'class' => 'AppBundle\Entity\Category',
                'property' => 'name',
            ))
            ->end()
            ->end();
    





//    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
//    
//        $datagridMapper->add('title');
//    

    protected function configureListFields(ListMapper $listMapper)
    
        $listMapper->addIdentifier('title');
    

    public function toString($object)
    
        return $object instanceof BlogPost
            ? $object->getTitle()
            : 'Blog Post'; // shown in the breadcrumb on the create view
    

请帮忙!

【问题讨论】:

【参考方案1】:

有同样的问题。其实我有两个问题:

    实体未实现 Sonata\TranslationBundle\Model\Gedmo\TranslatableInterface。并且即使稍后添加,symfony 缓存也需要清除。

    我发现在 Sonata Admin 安装或奏鸣曲捆绑包之一期间,我添加了像 shown here 这样的 DoctrineExtensionListener。在该侦听器内部,Gedmo 启动当前语言环境,当然它对 SonataTranslateBundle 一无所知。

所以我建议从请求参数、奏鸣曲翻译语言环境和 gedmo 启动的语言环境中转储(调试)symfony 语言环境,并检查它们是否同步。

最后,我将语言环境设置为 stored in user session,并更新了 DoctrineExtensionListener 以使用会话中的语言环境。

【讨论】:

以上是关于Symfony 3 上的 Sonata Admin 实体翻译的主要内容,如果未能解决你的问题,请参考以下文章

Symfony 2.3.6 和 Sonata Admin Bundle:空仪表板,没有错误

在 Symfony 4 上的 Sonata 管理页面中创建一个新页面

Symfony 3 Sonata Admin 使用注释创建管理员

在Symfony 4上的Sonata管理页面中创建新页面

如何在 Sonata Admin(Symfony 3.3、PHP 7)中使用自定义 javascript 为模态窗口扩展模板?

Symfony - Sonata “在管理池中找不到管理服务“app.admin.post”。”