HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/SBogers10/kommabasic.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();
    }
}