Berkarar/plugins/rainlab/translate/classes/TranslatableBehavior.php

495 lines
14 KiB
PHP

<?php namespace RainLab\Translate\Classes;
use Str;
use RainLab\Translate\Classes\Translator;
use October\Rain\Extension\ExtensionBase;
use October\Rain\Html\Helper as HtmlHelper;
/**
* Base class for model behaviors.
*
* @package october\translate
* @author Alexey Bobkov, Samuel Georges
*/
abstract class TranslatableBehavior extends ExtensionBase
{
/**
* @var \October\Rain\Database\Model Reference to the extended model.
*/
protected $model;
/**
* @var string Active language for translations.
*/
protected $translatableContext;
/**
* @var string Default system language.
*/
protected $translatableDefault;
/**
* @var bool Determines if empty translations should be replaced by default values.
*/
protected $translatableUseFallback = true;
/**
* @var array Data store for translated attributes.
*/
protected $translatableAttributes = [];
/**
* @var array Data store for original translated attributes.
*/
protected $translatableOriginals = [];
/**
* {@inheritDoc}
*/
protected $requiredProperties = ['translatable'];
/**
* Constructor
* @param \October\Rain\Database\Model $model The extended model.
*/
public function __construct($model)
{
$this->model = $model;
$this->initTranslatableContext();
$this->model->bindEvent('model.beforeGetAttribute', function ($key) use ($model) {
if ($this->isTranslatable($key)) {
$value = $this->getAttributeTranslated($key);
if ($model->hasGetMutator($key)) {
$method = 'get' . Str::studly($key) . 'Attribute';
$value = $model->{$method}($value);
}
return $value;
}
});
$this->model->bindEvent('model.beforeSetAttribute', function ($key, $value) use ($model) {
if ($this->isTranslatable($key)) {
$value = $this->setAttributeTranslated($key, $value);
if ($model->hasSetMutator($key)) {
$method = 'set' . Str::studly($key) . 'Attribute';
$value = $model->{$method}($value);
}
return $value;
}
});
$this->model->bindEvent('model.saveInternal', function() {
$this->syncTranslatableAttributes();
});
}
/**
* Initializes this class, sets the default language code to use.
* @return void
*/
public function initTranslatableContext()
{
$translate = Translator::instance();
$this->translatableContext = $translate->getLocale();
$this->translatableDefault = $translate->getDefaultLocale();
}
/**
* Checks if an attribute should be translated or not.
* @param string $key
* @return boolean
*/
public function isTranslatable($key)
{
if ($key === 'translatable' || $this->translatableDefault == $this->translatableContext) {
return false;
}
return in_array($key, $this->model->getTranslatableAttributes());
}
/**
* Disables translation fallback locale.
* @return self
*/
public function noFallbackLocale()
{
$this->translatableUseFallback = false;
return $this->model;
}
/**
* Enables translation fallback locale.
* @return self
*/
public function withFallbackLocale()
{
$this->translatableUseFallback = true;
return $this->model;
}
/**
* Returns a translated attribute value.
*
* The base value must come from 'attributes' on the model otherwise the process
* can possibly loop back to this event, then method triggered by __get() magic.
*
* @param string $key
* @param string $locale
* @return string
*/
public function getAttributeTranslated($key, $locale = null)
{
if ($locale == null) {
$locale = $this->translatableContext;
}
/*
* Result should not return NULL to successfully hook beforeGetAttribute event
*/
$result = '';
/*
* Default locale
*/
if ($locale == $this->translatableDefault) {
$result = $this->getAttributeFromData($this->model->attributes, $key);
}
/*
* Other locale
*/
else {
if (!array_key_exists($locale, $this->translatableAttributes)) {
$this->loadTranslatableData($locale);
}
if ($this->hasTranslation($key, $locale)) {
$result = $this->getAttributeFromData($this->translatableAttributes[$locale], $key);
}
elseif ($this->translatableUseFallback) {
$result = $this->getAttributeFromData($this->model->attributes, $key);
}
}
/*
* Handle jsonable attributes, default locale may return the value as a string
*/
if (
is_string($result) &&
method_exists($this->model, 'isJsonable') &&
$this->model->isJsonable($key)
) {
$result = json_decode($result, true);
}
return $result;
}
/**
* Returns all translated attribute values.
* @param string $locale
* @return array
*/
public function getTranslateAttributes($locale)
{
if (!array_key_exists($locale, $this->translatableAttributes)) {
$this->loadTranslatableData($locale);
}
return array_get($this->translatableAttributes, $locale, []);
}
/**
* Returns whether the attribute is translatable (has a translation) for the given locale.
* @param string $key
* @param string $locale
* @return bool
*/
public function hasTranslation($key, $locale)
{
/*
* If the default locale is passed, the attributes are retreived from the model,
* otherwise fetch the attributes from the $translatableAttributes property
*/
if ($locale == $this->translatableDefault) {
$translatableAttributes = $this->model->attributes;
}
else {
/*
* Ensure that the translatableData has been loaded
* @see https://github.com/rainlab/translate-plugin/issues/302
*/
if (!isset($this->translatableAttributes[$locale])) {
$this->loadTranslatableData($locale);
}
$translatableAttributes = $this->translatableAttributes[$locale];
}
return !!$this->getAttributeFromData($translatableAttributes, $key);
}
/**
* Sets a translated attribute value.
* @param string $key Attribute
* @param string $value Value to translate
* @return string Translated value
*/
public function setAttributeTranslated($key, $value, $locale = null)
{
if ($locale == null) {
$locale = $this->translatableContext;
}
if ($locale == $this->translatableDefault) {
return $this->setAttributeFromData($this->model->attributes, $key, $value);
}
if (!array_key_exists($locale, $this->translatableAttributes)) {
$this->loadTranslatableData($locale);
}
return $this->setAttributeFromData($this->translatableAttributes[$locale], $key, $value);
}
/**
* Restores the default language values on the model and
* stores the translated values in the attributes table.
* @return void
*/
public function syncTranslatableAttributes()
{
/*
* Spin through the known locales, store the translations if necessary
*/
$knownLocales = array_keys($this->translatableAttributes);
foreach ($knownLocales as $locale) {
if (!$this->isTranslateDirty(null, $locale)) {
continue;
}
$this->storeTranslatableData($locale);
}
/*
* Saving the default locale, no need to restore anything
*/
if ($this->translatableContext == $this->translatableDefault) {
return;
}
/*
* Restore translatable values to models originals
*/
$original = $this->model->getOriginal();
$attributes = $this->model->getAttributes();
$translatable = $this->model->getTranslatableAttributes();
$originalValues = array_intersect_key($original, array_flip($translatable));
$this->model->attributes = array_merge($attributes, $originalValues);
}
/**
* Changes the active language for this model
* @param string $context
* @return void
*/
public function translateContext($context = null)
{
if ($context === null) {
return $this->translatableContext;
}
$this->translatableContext = $context;
}
/**
* Shorthand for translateContext method, and chainable.
* @param string $context
* @return self
*/
public function lang($context = null)
{
$this->translateContext($context);
return $this->model;
}
/**
* Checks if this model has transatable attributes.
* @return true
*/
public function hasTranslatableAttributes()
{
return is_array($this->model->translatable) &&
count($this->model->translatable) > 0;
}
/**
* Returns a collection of fields that will be hashed.
* @return array
*/
public function getTranslatableAttributes()
{
$translatable = [];
if (!is_array($this->model->translatable)) {
return [];
}
foreach ($this->model->translatable as $attribute) {
$translatable[] = is_array($attribute) ? array_shift($attribute) : $attribute;
}
return $translatable;
}
/**
* Returns the defined options for a translatable attribute.
* @return array
*/
public function getTranslatableAttributesWithOptions()
{
$attributes = [];
foreach ($this->model->translatable as $options) {
if (!is_array($options)) {
continue;
}
$attributeName = array_shift($options);
$attributes[$attributeName] = $options;
}
return $attributes;
}
/**
* Determine if the model or a given translated attribute has been modified.
* @param string|null $attribute
* @return bool
*/
public function isTranslateDirty($attribute = null, $locale = null)
{
$dirty = $this->getTranslateDirty($locale);
if (is_null($attribute)) {
return count($dirty) > 0;
}
else {
return array_key_exists($attribute, $dirty);
}
}
/**
* Get the locales that have changed, if any
*
* @return array
*/
public function getDirtyLocales()
{
$dirtyLocales = [];
$knownLocales = array_keys($this->translatableAttributes);
foreach ($knownLocales as $locale) {
if ($this->isTranslateDirty(null, $locale)) {
$dirtyLocales[] = $locale;
}
}
return $dirtyLocales;
}
/**
* Get the original values of the translated attributes.
* @param string|null $locale If `null`, the method will get the original data for all locales.
* @return array|null Returns locale data as an array, or `null` if an invalid locale is specified.
*/
public function getTranslatableOriginals($locale = null)
{
if (!$locale) {
return $this->translatableOriginals;
} else {
return $this->translatableOriginals[$locale] ?? null;
}
}
/**
* Get the translated attributes that have been changed since last sync.
* @return array
*/
public function getTranslateDirty($locale = null)
{
if (!$locale) {
$locale = $this->translatableContext;
}
if (!array_key_exists($locale, $this->translatableAttributes)) {
return [];
}
if (!array_key_exists($locale, $this->translatableOriginals)) {
return $this->translatableAttributes[$locale]; // All dirty
}
$dirty = [];
foreach ($this->translatableAttributes[$locale] as $key => $value) {
if (!array_key_exists($key, $this->translatableOriginals[$locale])) {
$dirty[$key] = $value;
}
elseif ($value != $this->translatableOriginals[$locale][$key]) {
$dirty[$key] = $value;
}
}
return $dirty;
}
/**
* Extracts a attribute from a model/array with nesting support.
* @param mixed $data
* @param string $attribute
* @return mixed
*/
protected function getAttributeFromData($data, $attribute)
{
$keyArray = HtmlHelper::nameToArray($attribute);
return array_get($data, implode('.', $keyArray));
}
/**
* Sets an attribute from a model/array with nesting support.
* @param mixed $data
* @param string $attribute
* @return mixed
*/
protected function setAttributeFromData(&$data, $attribute, $value)
{
$keyArray = HtmlHelper::nameToArray($attribute);
array_set($data, implode('.', $keyArray), $value);
return $value;
}
/**
* Saves the translation data for the model.
* @param string $locale
* @return void
*/
abstract protected function storeTranslatableData($locale = null);
/**
* Loads the translation data from the model.
* @param string $locale
* @return array
*/
abstract protected function loadTranslatableData($locale = null);
}