File: D:/HostingSpaces/SBogers10/stielman.komma.nl/vendor/komma/kms/src/Core/TranslationService.php
<?php declare(strict_types=1);
namespace Komma\KMS\Core;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Komma\KMS\Core\Sections\AttributeKey;
use Komma\KMS\Globalization\Languages\Kms\LanguageService;
use Komma\KMS\Globalization\Languages\Models\Language;
use Komma\KMS\Core\Attributes\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Support\Collection;
class TranslationService extends AbstractModelHandler implements TranslationServiceInterface, OutOfDateInterface
{
use CanSaveSlugsTrait;
/** @var string */
protected $modelClassName = null;
/**
* Make and injects empty but linked translations into a translatable
* for each kms site language so that you can assume their presence. If the database already has a translation
* it will be loaded instead of making an empty translation
* Notice that they are not saved to the database. You need to save them yourself.
*
* @param Model $model
* @return Model
*/
public function makeAndInjectEmptyTranslationsIntoTranslatableIfNeeded(Model $model): Model
{
if(!is_a($model, AbstractTranslatableModel::class)) return $model;
$languages = LanguageService::getAvailableLanguages();
if($model->exists) $model->load('translations');
$translationModels = new Collection();
foreach ($languages as $language) {
//Check if the translation already exists.
$translation = false;
if($model->exists) {
foreach ($model->translations as $translationFromModel) {
if ($translationFromModel->language_id == $language->id) {
$translation = $translationFromModel;
break;
}
}
}
//If it does not exist we create an empty one
if(!$translation) {
/** @var Language $language */
/** @var AbstractTranslationModel $translation */
$translation = new $this->modelClassName();
//set the language
$translation->language_id = $language->id;
}
//Push empty or loaded models into the collection
$translationModels->push($translation);
}
$model->setRelation('translations', $translationModels);
return $model;
}
/**
* Gets the model query
*
* @return Builder
*/
public function translationModels(): Builder
{
return $this->modelClassName::query();
}
/**
* Save the translations of the given AbstractTranslatableModel if they contain some data.
* Also links translations and translatable model with eachother
* If the force save callback returns true the translation will be saved, even if it does not have data.
* This because something else depends on the translation existing.
*
* @param Model $model
* @return Model
*/
public function saveModelTranslations(Model $model): Model
{
$self = $this;
if(!is_a($model,AbstractTranslatableModel::class)) return $model;
/** @var $model AbstractTranslatableModel */
$model->translations->each(function (AbstractTranslationModel $abstractTranslationModel) use ($model, $self) {
if(!$abstractTranslationModel->isEmpty()) {
if($model->exists) $abstractTranslationModel->translatable()->associate($model);
$abstractTranslationModel->save();
$this->saveSlugForModel($abstractTranslationModel);
}
});
return $model;
}
public function save(Model $model, Collection $attributes = null): Model
{
//Only process the model if attributes where given
if($attributes === null) {
$this->debug('Skipping saving translation model(s) for model "'.get_class($model).'". Because it did not receive any attributes.');
return $model;
}
$this->checkContainsAttributes($attributes);
//Skip the saving for the model if it does not extend the AbstractTranslatableModel class
if(!is_a($model, AbstractTranslatableModel::class)) {
$this->debug('Skipping saving translation model(s) for model "'.get_class($model).'". Because it does not extend "'.AbstractTranslatableModel::class.'"');
return $model;
}
$attributes->each((function(Attribute $attribute) use(&$model, $attributes) {
$valueFrom = $attribute->getsValueFrom();
$valueReference = $attribute->getsValueFromReference();
if($valueFrom !== Attribute::ValueFromTranslationModel) return;
$value = $attribute->getValue();
/** @var Language $language */
$language = $attribute->getAssociatedLanguage();
if(!is_a($model,AbstractTranslatableModel::class)) throw new \RuntimeException('Expected model to be a '.AbstractTranslatableModel::class.'. But was a '.get_class($model));
$translation = $this->getTranslationModelForModelByLanguage( $model, $language);
$translation->$valueReference = $value;
if($valueReference === 'name' && is_a($translation,HasSlugInterface::class)) {
/** @var HasSlugInterface $translation */
$translation->slug = $translation->suggestSlug();
}
})->bindTo($this));
$model = $this->saveModelTranslations($model);
return $model;
}
public function load(Model $model, Collection $attributes = null): Collection
{
//Only process the model if attributes where given
if($attributes === null) {
$this->debug('Skipping loading translation model(s) for model "'.get_class($model).'". Because it did not receive any attributes.');
return new Collection();
}
$this->checkContainsAttributes($attributes);
//Skip the saving for the model if it does extend the AbstractTranslatableModel class
if(!is_a($model, AbstractTranslatableModel::class)) {
$this->debug('Skipping loading translation model(s) for model "'.get_class($model).'". Because it does not extend "'.AbstractTranslatableModel::class.'"');
return new Collection();
}
return $attributes->map(function(Attribute $attribute) use($model) {
$valueFrom = $attribute->getsValueFrom();
$valueReference = $attribute->getsValueFromReference();
if($valueFrom !== Attribute::ValueFromTranslationModel) return $attribute;
$keys = array_keys($model->getAttributes());
if(!array_key_exists($valueReference, $keys) === false) throw new \RuntimeException("The value reference field name ('".$valueReference."') does not exist on the model with class name: ".(get_class($model)).". Please check the attribute configuration in the section. All available field names: ".implode(', ', $keys));
if($attribute->hasAssociatedLanguage())
{
if(!is_a($model, AbstractTranslatableModel::class)) throw new \RuntimeException("The model '".get_class($model)."' is not a model that is translatable (It does not implement the TranslatableModelInterface). Please check the section configuration (Attribute with reference '".$attribute->getsValueFromReference()."') or make the model implement the interface.");
/** @var AbstractTranslatableModel $model */
/** @var AbstractTranslationModel $translation */
$translation = $model->translations->where('language_id', $attribute->getAssociatedLanguage()->id)->first();
if(!$translation) return $attribute; //Skip filling the value if the model did not have a translation
if(array_key_exists($valueReference, $translation->attributesToArray()) === false) throw new \RuntimeException("The translation value reference field name ('".$valueReference."') does not exist on the translations from the model with class name: ".(get_class($model)).". Please check the attribute configuration in the section.");
$value = $translation->$valueReference;
if($value) $attribute->setValue((string) $value);
}
return $attribute;
});
}
/**
* Uses a model that must have a translations method, creates a new translation for it if it does not exists and associates it with the language given.
* Returns the translation
*
* @param AbstractTranslatableModel $model
* @param Language $language
* @return AbstractTranslationModel|null The translation
*/
public function getTranslationModelForModelByLanguage(AbstractTranslatableModel &$model, Language $language): ?AbstractTranslationModel{
$translation = $model->translations->filter(function(AbstractTranslationModel $translation) use ($language) {
return $translation->language_id == $language->id;
})->first();
if(!$translation) Log::warning('Could not find translation for language with an iso_2 value of "'.$language->iso_2.'" in "'.get_class($model).'" with id "'.$model->id);
return $translation;
}
/**
* Destroys the appropriate related models for a given model.
* Those related models must be the responsibility of this service
*
* @param Model|Collection $model
* @return Model|Collection
*/
public function destroyForModel(Model $model)
{
if(is_a($model, AbstractTranslatableModel::class)) {
/** @var AbstractTranslatableModel $model */
$model->translations()->delete();
}
return $model;
}
/**
* @param string $modelClassName
*/
public function setModelClassName(string $modelClassName): void
{
$this->modelClassName = $modelClassName;
}
/**
* @inheritDoc
*/
public function forceSaveWhenTrue(AbstractTranslatableModel $model, \Closure $closure)
{
$this->validateForceSaveClosure($closure);
$model->translations->each(function(AbstractTranslationModel $abstractTranslationModel) use($closure, $model) {
$result = $closure($abstractTranslationModel);
if($result === true) {
$abstractTranslationModel->translatable()->associate($model);
$abstractTranslationModel->save();
}
});
}
/**
* Validates the signature and types of the force save closure.
*
* @param \Closure $closure
*/
private function validateForceSaveClosure(\Closure $closure) {
$closureReflection = new \ReflectionFunction($closure);
$closureIsValid = true;
if($closureReflection->getNumberOfParameters() !== 1 && $closureReflection->getNumberOfRequiredParameters() !== 1)
throw new \InvalidArgumentException('The "forceSave" closure must have exactly 1 required argument. But got '.$closureReflection->getNumberOfRequiredParameters());
$parameterReflection = $closureReflection->getParameters()[0];
if(!$parameterReflection->hasType()) throw new \InvalidArgumentException('The "forceSave" closure must have exactly 1 required argument of type '.AbstractTranslationModel::class.'. But the parameter was untyped');
$typeReflection = $parameterReflection->getType();
if($typeReflection->getName() !== AbstractTranslationModel::class) throw new \InvalidArgumentException('The "forceSave" closure must have exactly 1 required argument of type '.AbstractTranslationModel::class.'. But the parameter of type: '.$typeReflection->getName());
}
/**
* Returns an array of form field names that are not up to date with the given model.
* Or an empty array if the model is up to date with form fields. The form fields must
* come from the outOfDateCheckFormFields method.
*
* @param Model $model
* @return array
*/
public function outOfDate(Model $model): array
{
//Create an array to keep track of the fields (attributes) which are out of date.
$outOfDateFields = [];
//Only process models which can have translations
if(!is_a($model, AbstractTranslatableModel::class)) return $outOfDateFields;
//Get the form fields and their values
$outOfDateCheckFormFields = $this->outOfDateCheckFormFields();
//Loop over them and check if they are out of date.
foreach($outOfDateCheckFormFields as $attributeKey => $value) {
// Create an attribute key if possible
if(!AttributeKey::couldBeAKeyString((string) $attributeKey)) continue;
$attributeKey = AttributeKey::createInstanceFromString($attributeKey);
// If the attribute is not for a translation, we ignore it here. It must be processed in the ModelService
if($attributeKey->getTranslationIso2() == '') continue;
//Get the translation model for the form field (also known as attribute)
$translationModel = $model->translations()->whereHas('language', function(EloquentBuilder $query) use($attributeKey) {
$query->where('iso_2', '=', $attributeKey->getTranslationIso2());
})->first();
//Check if the form field value does not equal the translation models value. If so, the form field is out of date.
if($translationModel && $value !== (string) $translationModel->{(string) $attributeKey->getValuePart()}) { // Notice that when the attribute key does not exist on the model, the value will be null.
//The value of the model is not the same as the one from the form field. So form field data was out of date.
$outOfDateFields[(string) $attributeKey] = $value;
}
}
return $outOfDateFields;
}
/**
* Return key value pairs of data (usually form fields)
* that will be used for the outOfDateCheck
*
* @return array
*/
public function outOfDateCheckFormFields(): array
{
return request()->all();
}
}