CakePHP 3:按语言排序的分页器(具有 i18n 翻译行为)

CakePHP 3: Paginator sort by languages (with i18n translate behavior)

我有一个 table,其中包含所有条目,包括所有多语言翻译:如何在翻译字段上创建分页排序链接? (蛋糕 3.1.6)

总结:这行不通,我不能这样按翻译排序:

$this->Paginator->sort('_translations.es.title', 'Spanish')

长版:

| Title ENGLISH    | Title SPANISH    | Title GERMAN    |  = pagination sort links
| ---------------- | ---------------- | --------------- |
| Christmas        | Navidad          | Weihnachten     |
| Spring           | Primavera        | Frühling        |
| ...

所以这是我的简化测试设置:

Table 文章只有一个字段title需要翻译。
i18n table 默认设置为 described in the book

烘焙Tableclass/src/Model/Table/ArticlesTable.php,添加翻译行为

public function initialize(array $config) {
  // ... default config (removed in this post to simplify code)
  $this->addBehavior('Translate', ['fields' => ['title']]);  // added this line
}

烘焙实体class/src/Model/Entity/Article.php,添加TranslateTrait:

namespace App\Model\Entity;
use Cake\ORM\Behavior\Translate\TranslateTrait; // added this line
use Cake\ORM\Entity;
class Article extends Entity {
  protected $_accessible = [
    '*' => true,
    'id' => false,
  ];
  use TranslateTrait; // added this line
}

烘焙Controller/src/Controller/ArticlesController.php,修改如下:

namespace App\Controller;
use App\Controller\AppController;
class ArticlesController extends AppController {
  public $paginate = [
    'sortWhitelist' => [  // Allow pagination sort on this fields:
      'title',
      '_translations.es.title',
      '_translations.de.title'
    ]
  ];
  public function index() {
    $query = $this->Articles->find('translations'); // Retrieve All Translations
    $this->set('articles', $this->paginate($query));
  }
}

烘焙查看/src/Template/Articles/index.ctp,modified/simplified:

<table>
  <tr>
    <th><?= $this->Paginator->sort('title', 'English') ?></th>
    <th><?= $this->Paginator->sort('_translations.es.title', 'Spanish') ?></th>
    <th><?= $this->Paginator->sort('_translations.de.title', 'German') ?></th>
  </tr>
<?php foreach ($articles as $article): ?>
  <tr>
    <td><?= h($article->title) ?></td>
    <td><?= h($article->_translations['es']->title) ?></td>
    <td><?= h($article->_translations['de']->title) ?></td>
  </tr>
<?php endforeach; ?>
</table>

table 中的翻译显示正确,但无法按翻译字段排序。当我点击翻译的分页链接时,出现以下错误:

SQLSTATE[42S22]: Column not found: 1054 Unknown column '_translations.es' in 'order clause'

SQL Query:
SELECT Articles.id AS `Articles__id`,
       Articles.title AS `Articles__title`,
       Articles.created AS `Articles__created`,
       Articles.modified AS `Articles__modified`
FROM articles Articles 
ORDER BY _translations.es asc LIMIT 20 OFFSET 0

中使用了格式Posts_title_translation.content——我不知道这是从哪里来的,但我也试过这种方式(当然我也添加了字段名称的变体在分页器白名单中):

$this->Paginator->sort('Articles_title_translation', 'Spanish')
$this->Paginator->sort('Articles_title_translation.es', 'Spanish')
$this->Paginator->sort('Articles_title_translation.content', 'Spanish')
$this->Paginator->sort('Articles_title_translation.es.content', 'Spanish')

None 他们正在工作...(显然)

如何根据标题字段的国际化翻译对 table 项进行排序?

对已翻译字段进行排序需要连接

通常您只能对连接到主查询中的字段进行排序!

翻译后的字段仅与非默认的当前语言环境结合

默认情况下,只有在当前语言环境 (I18n::locale()) 与默认语言环境 (I18N::defaultLocale()intl.default_locale) 不匹配的情况下才加入翻译字段,即当有其实就是需要翻译一下。

一旦将当前语言环境更改为非默认语言环境

I18n::locale('de');
$query = $this->Articles->find('translations');
// ...

翻译行为将包含与翻译内容的关联,这就是 TableAlias_field_translation 别名的来源,该行为使用该命名方案为每个翻译字段创建 hasOne 关联。

这些字段随后可用于分页,但是这一次只会加入一个区域设置!

确保分页器使用正确的字段

由于并不总是包含关联,因此您必须采取适当的措施来确保分页器根据区域设置使用正确的字段。类似这样的东西(请注意,这只是用于说明目的的未经测试的示例代码)

public function index()
{
    $sort = $this->request->query('sort');
    if ($sort) {
        $fieldMap = [
            'Articles_title_translation.content' => 'Articles.title'
        ];
        if (
            isset($fieldMap[$sort]) &&
            $this->Articles->locale() ===
                $this->Articles->behaviors()->get('Translate')->config('defaultLocale')
        ) {
            $this->request->query['sort'] = $fieldMap[$sort];
        }
    }

    $query = $this->Articles->find('translations');
    $this->set('articles', $this->paginate($query));
}

这会将字段映射到从已翻译字段排序到原始字段,以防不包含任何翻译。

加入并排序一个字段的所有translations/languages

以上适用于单个字段的排序,可能会翻译也可能不会翻译。将字段的 所有 翻译用于排序不在翻译行为范围内。虽然行为 确实 加载所有翻译,但它是通过使用 hasMany 关联来实现的,即使用单独的查询,因此它们不能用于排序。加入所有的翻译都需要手动完成。

这可能是一个功能请求,我不确定这是否是可以证明此类核心修改合理的常见用例,您可能想在 GitHub or ask on IRC 上提出问题。

话虽如此,这是一个基本示例,一个扩展的翻译行为,它几乎可以完成 TranslateBehavior::setupFieldAssociations()TranslateBehavior::beforeFind() 所做的工作,只是稍作修改。该行为需要一个 locales 选项,该选项需要包含所有应该加入的语言环境,因为它们无法自动识别。

src/Model/Table/ArticlesTable.php

// Remove $this->addBehavior('Translate', ['fields' => ['title']]);
// and load the custom behavior instead (otherwise there will be an
// error about "duplicate translation finders"

$this->addBehavior('MyTranslate', [
    'fields' => ['title'],
    'locales' => ['es', 'de']
]);

src/Model/Behavior/MyTranslateBehavior.php

namespace App\Model\Behavior;

use Cake\ORM\Behavior\TranslateBehavior;
use Cake\ORM\Query;
use Cake\ORM\Table;

class MyTranslateBehavior extends TranslateBehavior
{
    protected $_associations = [];

    public function __construct(Table $table, array $config)
    {
        $config += [
            'locales' => []
        ];

        parent::__construct($table, $config);
    }

    public function setupFieldAssociations($fields, $table, $model, $strategy)
    {
        parent::setupFieldAssociations($fields, $table, $model, $strategy);

        $alias = $this->_table->alias();
        $tableLocator = $this->tableLocator();
        $locales = $this->config('locales');

        $this->_associations = [];
        foreach ($fields as $field) {
            foreach ($locales as $locale) {
                $name = $alias . '_' . $field . '_translation_' . $locale;

                if (!$tableLocator->exists($name)) {
                    $fieldTable = $tableLocator->get($name, [
                        'className' => $table,
                        'alias' => $name,
                        'table' => $this->_translationTable->table()
                    ]);
                } else {
                    $fieldTable = $tableLocator->get($name);
                }

                $conditions = [
                    $name . '.locale' => $locale,
                    $name . '.model' => $model,
                    $name . '.field' => $field
                ];

                $this->_table->hasOne($name, [
                    'targetTable' => $fieldTable,
                    'foreignKey' => 'foreign_key',
                    'joinType' => 'LEFT',
                    'conditions' => $conditions,
                    'propertyName' => $field . '_translation_' . $locale
                ]);

                $this->_associations[] = $name;
            }
        }
    }

    public function findTranslations(Query $query, array $options)
    {
        $query->contain($this->_associations);
        return parent::findTranslations($query, $options);
    }
}

它的作用应该相对容易理解,它将简单地为所有已配置语言环境中的所有字段创建并包含 hasOne 关联。别名将使用 TableAlias_field_translation_locale 格式,例如 Articles_title_translation_es,这是随后需要在排序白名单和分页器排序链接中使用的格式。

应该注意加入的新字段可能需要默认排序顺序,否则它可能会对用于查询翻译的子查询进行排序主要查询,导致检索到错误的翻译!

public $paginate = [
    'order' => ['Articles.title' => 'ASC']
    'sortWhitelist' => [
        'Articles.title',
        'Articles_title_translation_de.content',
        'Articles_title_translation_es.content'
    ]
];
$this->Paginator->sort('Articles.title', 'English');
$this->Paginator->sort('Articles_title_translation_es.content', 'Spanish');
$this->Paginator->sort('Articles_title_translation_de.content', 'German');

另见