358 lines
12 KiB
PHP
358 lines
12 KiB
PHP
<?php namespace RainLab\Translate\Behaviors;
|
|
|
|
use Db;
|
|
use DbDongle;
|
|
use RainLab\Translate\Classes\TranslatableBehavior;
|
|
|
|
/**
|
|
* Translatable model extension
|
|
*
|
|
* Usage:
|
|
*
|
|
* In the model class definition:
|
|
*
|
|
* public $implement = ['@RainLab.Translate.Behaviors.TranslatableModel'];
|
|
*
|
|
* public $translatable = ['name', 'content'];
|
|
*
|
|
*/
|
|
class TranslatableModel extends TranslatableBehavior
|
|
{
|
|
public function __construct($model)
|
|
{
|
|
parent::__construct($model);
|
|
|
|
$model->morphMany['translations'] = [
|
|
'RainLab\Translate\Models\Attribute',
|
|
'name' => 'model'
|
|
];
|
|
|
|
// October v2.0
|
|
if (class_exists('System')) {
|
|
$this->extendFileModels('attachOne');
|
|
$this->extendFileModels('attachMany');
|
|
}
|
|
|
|
// Clean up indexes when this model is deleted
|
|
$model->bindEvent('model.afterDelete', function() use ($model) {
|
|
Db::table('rainlab_translate_attributes')
|
|
->where('model_id', $model->getKey())
|
|
->where('model_type', get_class($model))
|
|
->delete();
|
|
|
|
Db::table('rainlab_translate_indexes')
|
|
->where('model_id', $model->getKey())
|
|
->where('model_type', get_class($model))
|
|
->delete();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* extendFileModels will swap the standard File model with MLFile instead
|
|
*/
|
|
protected function extendFileModels(string $relationGroup)
|
|
{
|
|
foreach ($this->model->$relationGroup as $relationName => $relationObj) {
|
|
$relationClass = is_array($relationObj) ? $relationObj[0] : $relationObj;
|
|
if ($relationClass === \System\Models\File::class) {
|
|
if (is_array($relationObj)) {
|
|
$this->model->$relationGroup[$relationName][0] = \RainLab\Translate\Models\MLFile::class;
|
|
}
|
|
else {
|
|
$this->model->$relationGroup[$relationName] = \RainLab\Translate\Models\MLFile::class;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* scopeTransWhere applies a translatable index to a basic query. This scope will join the
|
|
* index table and can be executed neither more than once, nor with scopeTransOrder.
|
|
* @param Builder $query
|
|
* @param string $index
|
|
* @param string $value
|
|
* @param string $locale
|
|
* @return Builder
|
|
*/
|
|
public function scopeTransWhere($query, $index, $value, $locale = null, $operator = '=')
|
|
{
|
|
return $this->transWhereInternal($query, $index, $value, [
|
|
'locale' => $locale,
|
|
'operator' => $operator
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* scopeTransWhereNoFallback is identical to scopeTransWhere except it will not
|
|
* use a fallback query when there are no indexes found.
|
|
* @see scopeTransWhere
|
|
*/
|
|
public function scopeTransWhereNoFallback($query, $index, $value, $locale = null, $operator = '=')
|
|
{
|
|
// Ignore translatable indexes in default locale context
|
|
if(($locale ?: $this->translatableContext) === $this->translatableDefault) {
|
|
return $query->where($index, $operator, $value);
|
|
}
|
|
|
|
return $this->transWhereInternal($query, $index, $value, [
|
|
'locale' => $locale,
|
|
'operator' => $operator,
|
|
'noFallback' => true
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* transWhereInternal
|
|
* @link https://github.com/rainlab/translate-plugin/pull/623
|
|
*/
|
|
protected function transWhereInternal($query, $index, $value, $options = [])
|
|
{
|
|
extract(array_merge([
|
|
'locale' => null,
|
|
'operator' => '=',
|
|
'noFallback' => false
|
|
], $options));
|
|
|
|
if (!$locale) {
|
|
$locale = $this->translatableContext;
|
|
}
|
|
|
|
// Separate query into two separate queries for improved performance
|
|
$translateIndexes = Db::table('rainlab_translate_indexes')
|
|
->where('rainlab_translate_indexes.model_type', '=', $this->getClass())
|
|
->where('rainlab_translate_indexes.locale', '=', $locale)
|
|
->where('rainlab_translate_indexes.item', $index)
|
|
->where('rainlab_translate_indexes.value', $operator, $value)
|
|
->pluck('model_id')
|
|
;
|
|
|
|
if ($translateIndexes->count() || $noFallback) {
|
|
$query->whereIn($this->model->getQualifiedKeyName(), $translateIndexes);
|
|
}
|
|
else {
|
|
$query->where($index, $operator, $value);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Applies a sort operation with a translatable index to a basic query. This scope will join the index table.
|
|
* @param Builder $query
|
|
* @param string $index
|
|
* @param string $direction
|
|
* @param string $locale
|
|
* @return Builder
|
|
*/
|
|
public function scopeTransOrderBy($query, $index, $direction = 'asc', $locale = null)
|
|
{
|
|
if (!$locale) {
|
|
$locale = $this->translatableContext;
|
|
}
|
|
$indexTableAlias = 'rainlab_translate_indexes_' . $index . '_' . $locale;
|
|
|
|
$query->select(
|
|
$this->model->getTable().'.*',
|
|
Db::raw('COALESCE(' . $indexTableAlias . '.value, '. $this->model->getTable() .'.'.$index.') AS translate_sorting_key')
|
|
);
|
|
|
|
$query->orderBy('translate_sorting_key', $direction);
|
|
|
|
$this->joinTranslateIndexesTable($query, $locale, $index, $indexTableAlias);
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Joins the translatable indexes table to a query.
|
|
* @param Builder $query
|
|
* @param string $locale
|
|
* @param string $indexTableAlias
|
|
* @return Builder
|
|
*/
|
|
protected function joinTranslateIndexesTable($query, $locale, $index, $indexTableAlias)
|
|
{
|
|
$joinTableWithAlias = 'rainlab_translate_indexes as ' . $indexTableAlias;
|
|
// check if table with same name and alias is already joined
|
|
if (collect($query->getQuery()->joins)->contains('table', $joinTableWithAlias)) {
|
|
return $query;
|
|
}
|
|
|
|
$query->leftJoin($joinTableWithAlias, function($join) use ($locale, $index, $indexTableAlias) {
|
|
$join
|
|
->on(Db::raw(DbDongle::cast($this->model->getQualifiedKeyName(), 'TEXT')), '=', $indexTableAlias . '.model_id')
|
|
->where($indexTableAlias . '.model_type', '=', $this->getClass())
|
|
->where($indexTableAlias . '.item', '=', $index)
|
|
->where($indexTableAlias . '.locale', '=', $locale);
|
|
});
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Saves the translation data in the join table.
|
|
* @param string $locale
|
|
* @return void
|
|
*/
|
|
protected function storeTranslatableData($locale = null)
|
|
{
|
|
if (!$locale) {
|
|
$locale = $this->translatableContext;
|
|
}
|
|
|
|
/*
|
|
* Model doesn't exist yet, defer this logic in memory
|
|
*/
|
|
if (!$this->model->exists) {
|
|
$this->model->bindEventOnce('model.afterCreate', function() use ($locale) {
|
|
$this->storeTranslatableData($locale);
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* @event model.translate.resolveComputedFields
|
|
* Resolve computed fields before saving
|
|
*
|
|
* Example usage:
|
|
*
|
|
* Override Model's __construct method
|
|
*
|
|
* public function __construct(array $attributes = [])
|
|
* {
|
|
* parent::__construct($attributes);
|
|
*
|
|
* $this->bindEvent('model.translate.resolveComputedFields', function ($locale) {
|
|
* return [
|
|
* 'content_html' =>
|
|
* self::formatHtml($this->asExtension('TranslatableModel')
|
|
* ->getAttributeTranslated('content', $locale))
|
|
* ];
|
|
* });
|
|
* }
|
|
*
|
|
*/
|
|
$computedFields = $this->model->fireEvent('model.translate.resolveComputedFields', [$locale], true);
|
|
if (is_array($computedFields)) {
|
|
$this->translatableAttributes[$locale] = array_merge($this->translatableAttributes[$locale], $computedFields);
|
|
}
|
|
|
|
$this->storeTranslatableBasicData($locale);
|
|
$this->storeTranslatableIndexData($locale);
|
|
}
|
|
|
|
/**
|
|
* Saves the basic translation data in the join table.
|
|
* @param string $locale
|
|
* @return void
|
|
*/
|
|
protected function storeTranslatableBasicData($locale = null)
|
|
{
|
|
$data = json_encode($this->translatableAttributes[$locale], JSON_UNESCAPED_UNICODE);
|
|
|
|
$obj = Db::table('rainlab_translate_attributes')
|
|
->where('locale', $locale)
|
|
->where('model_id', $this->model->getKey())
|
|
->where('model_type', $this->getClass());
|
|
|
|
if ($obj->count() > 0) {
|
|
$obj->update(['attribute_data' => $data]);
|
|
}
|
|
else {
|
|
Db::table('rainlab_translate_attributes')->insert([
|
|
'locale' => $locale,
|
|
'model_id' => $this->model->getKey(),
|
|
'model_type' => $this->getClass(),
|
|
'attribute_data' => $data
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves the indexed translation data in the join table.
|
|
* @param string $locale
|
|
* @return void
|
|
*/
|
|
protected function storeTranslatableIndexData($locale = null)
|
|
{
|
|
$optionedAttributes = $this->getTranslatableAttributesWithOptions();
|
|
if (!count($optionedAttributes)) {
|
|
return;
|
|
}
|
|
|
|
$data = $this->translatableAttributes[$locale];
|
|
|
|
foreach ($optionedAttributes as $attribute => $options) {
|
|
if (!array_get($options, 'index', false)) {
|
|
continue;
|
|
}
|
|
|
|
$value = array_get($data, $attribute);
|
|
|
|
$obj = Db::table('rainlab_translate_indexes')
|
|
->where('locale', $locale)
|
|
->where('model_id', $this->model->getKey())
|
|
->where('model_type', $this->getClass())
|
|
->where('item', $attribute);
|
|
|
|
$recordExists = $obj->count() > 0;
|
|
|
|
if (!strlen($value)) {
|
|
if ($recordExists) {
|
|
$obj->delete();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($recordExists) {
|
|
$obj->update(['value' => $value]);
|
|
}
|
|
else {
|
|
Db::table('rainlab_translate_indexes')->insert([
|
|
'locale' => $locale,
|
|
'model_id' => $this->model->getKey(),
|
|
'model_type' => $this->getClass(),
|
|
'item' => $attribute,
|
|
'value' => $value
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the translation data from the join table.
|
|
* @param string $locale
|
|
* @return array
|
|
*/
|
|
protected function loadTranslatableData($locale = null)
|
|
{
|
|
if (!$locale) {
|
|
$locale = $this->translatableContext;
|
|
}
|
|
|
|
if (!$this->model->exists) {
|
|
return $this->translatableAttributes[$locale] = [];
|
|
}
|
|
|
|
$obj = $this->model->translations->first(function ($value, $key) use ($locale) {
|
|
return $value->attributes['locale'] === $locale;
|
|
});
|
|
|
|
$result = $obj ? json_decode($obj->attribute_data, true) : [];
|
|
|
|
return $this->translatableOriginals[$locale] = $this->translatableAttributes[$locale] = $result;
|
|
}
|
|
|
|
/**
|
|
* Returns the class name of the model. Takes any
|
|
* custom morphMap aliases into account.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function getClass()
|
|
{
|
|
return $this->model->getMorphClass();
|
|
}
|
|
}
|