Berkarar/plugins/rainlab/translate/behaviors/TranslatableModel.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();
}
}