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/farmfun.komma.pro/app/Komma/Shop/Properties/Kms/PropertyService.php
<?php

namespace App\Komma\Shop\Properties\Kms;

use App\Helpers\KommaHelpers;
use App\Komma\Globalization\Languages\Models\Language;
use App\Komma\Kms\Core\Attributes\Attribute;
use App\Komma\Kms\Core\Attributes\Currency;
use App\Komma\Kms\Core\Attributes\Models\SelectOptionInterface;
use App\Komma\Kms\Core\Attributes\TextField;
use App\Komma\Kms\Core\ModelService;
use App\Komma\Kms\Core\ModelServiceInterface;
use App\Komma\Kms\Core\Sections\SectionTabItem;
use App\Komma\Kms\Core\TranslationServiceInterface;
use App\Komma\Shop\Properties\Models\PropertizableInterface;
use App\Komma\Shop\Properties\Models\Property;
use App\Komma\Shop\Properties\Models\PropertyKey;
use App\Komma\Shop\Properties\Models\PropertyKeyTranslation;
use App\Komma\Shop\Properties\Models\PropertyValue;
use App\Komma\Shop\Properties\Models\PropertyValueTranslation;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

/**
 * Class PropertyService
 */
class PropertyService extends ModelService
{
    protected $sortable = false;

    /** @var ModelServiceInterface */
    protected $propertyKeyService;

    /** @var ModelServiceInterface */
    protected $propertyValueService;

    /** @var TranslationServiceInterface */
    private $keyTranslationService;

    /** @var TranslationServiceInterface */
    private $valueTranslationService;

    public function __construct()
    {
        $this->propertyKeyService = app(ModelServiceInterface::class);
        $this->propertyKeyService->setModelClassName(PropertyKey::class);
        $this->propertyValueService = app(ModelServiceInterface::class);
        $this->propertyValueService->setModelClassName(PropertyValue::class);
        $this->keyTranslationService = app(TranslationServiceInterface::class);
        $this->keyTranslationService->setModelClassName(PropertyKeyTranslation::class);
        $this->valueTranslationService = app(TranslationServiceInterface::class);
        $this->valueTranslationService->setModelClassName(PropertyValueTranslation::class);
        $this->setModelClassName(Property::class);
        parent::__construct();
    }

    /**
     * Puts the values of attributes in an Eloquent model. And then saves that model.
     *
     * @param Model|Property $property
     * @param Collection $attributes
     * @return Model|Property
     */
    public function save(Model $property, Collection $attributes = null): Model
    {
        if ($attributes === null) {
            return $property;
        }

        $this->checkContainsAttributes($attributes);

        $attributes->each(function (Attribute $attribute) use ($property) {
            $reference = $attribute->getsValueFromReference();
            $value = $attribute->getValue();
//            $valueFrom = $attribute->getsValueFrom();

            if ($reference == 'key') {
                /** @var Property $property */
                $propertyKey = $this->getOrCreatePropertyKeyWithTranslationValue(trim($value), $attribute->getAssociatedLanguage(), $property);

                //Check if there is a translation that we can update or check if we need to create a new translation
                /** @var PropertyKeyTranslation $keyTranslation */
                $keyTranslation = $this->keyTranslationService->getTranslationModelForModelByLanguage($propertyKey, $attribute->getAssociatedLanguage());
                $keyTranslation->value = $value;

                $keyTranslation->save();
            } elseif ($reference == 'value') {
                $propertyValue = $this->getOrCreatePropertyValueWithTranslationValue(trim($value), $attribute->getAssociatedLanguage(), $property);

                //Check if there is a translation that we can update or check if we need to create a new translation
                /** @var PropertyValueTranslation $valueTranslation */
                $valueTranslation = $this->valueTranslationService->getTranslationModelForModelByLanguage($propertyValue, $attribute->getAssociatedLanguage());
                $valueTranslation->value = $value;
                $valueTranslation->save();
            } elseif ($reference == 'code_name') {
                $property->code_name = $attribute->getValue();
            } elseif ($reference == 'property_id') {
                $this->linkToProperties($property, $value);
            }
        });

        $property->save();

        return $property;
    }

    /**
     * Gets the values of an Eloquent model and passes them to a collection of attributes
     *
     * @param Model $model
     * @param Collection $attributes
     * @return mixed
     */
    public function load(Model $model, Collection $attributes = null): Collection
    {
        if ($attributes === null) {
            return new Collection();
        }
        if (! is_a($model, Property::class)) {
            return $attributes;
        }
        /** @var Property $model */
        $attributes = parent::load($model, $attributes);
        $attributes->each(function (Attribute $attribute) use ($model) {
            $reference = $attribute->getsValueFromReference();

            if ($reference == 'key') {
                $key = $model->key()->first();
                if ($key) {
                    $translation = $this->keyTranslationService->getTranslationModelForModelByLanguage($key, $attribute->getAssociatedLanguage());
                    $attribute->setValue(($translation) ? $translation->value : '');
                }
            } elseif ($reference == 'value') {
                $propertyValue = $model->value()->with([
                    'translations' => function (HasMany $query) use ($attribute) {
                        $query->where('language_id', '=', $attribute->getAssociatedLanguage()->id)->get();
                    },
                ])->first();

                if ($propertyValue && $propertyValue->relationLoaded('translations') && $propertyValue->translations->first()) {
                    /** @var PropertyValue $propertyValue */
                    $attribute->setValue($propertyValue->translations->first()->value);
                }
            } elseif ($reference == 'property_id') {
                $idString = $this->getPropertyIdsForModel($model);
                if ($idString) {
                    $attribute->setValue($idString);
                }
            }
        });

        return $attributes;
    }

    /**
     * Fills non-product specific attributes. Product specific attributes are processed in the parent
     *
     * @param Collection $sectionTabItems A collection containing implementations AbstractSectionTabItem's
     * @param Model $model
     * @return Collection
     */
    public function fillAttributesWithData(Collection $sectionTabItems, Model $model)
    {
        /** @var $model Property */
        $filledAttributesCollection = parent::fillAttributesWithData($sectionTabItems, $model);

        /** @var TextField $quantityDiscountAttribute */
        $quantityDiscountAttribute = null;
        /** @var Currency $quantityPriceAttribute */
        $quantityPriceAttribute = null;

        $sectionTabItems->each(
            function ($sectionTabItem, $key) use ($model, $filledAttributesCollection, &$quantityDiscountAttribute, &$quantityPriceAttribute) {
                /** @var $sectionTabItem SectionTabItem */
                if (! is_a($sectionTabItem->getAttribute(), Attribute::class)) {
                    throw new \InvalidArgumentException('One of the attributes in a AbstractSectionTabItem instance is not but must be an child instance of Attribute.');
                }

                $attribute = $sectionTabItem->getAttribute();
                $valueReference = $sectionTabItem->getAttribute()->getsValueFromReference();

                switch ($valueReference) {
                    case 'key':
                        $key = $model->key()->first();
                        if (! $key) {
                            break;
                        }
                        $translation = $this->propertyKeyService->getTranslationModelForModelByLanguage($key, $attribute->getAssociatedLanguage());
                        $attribute->setValue(($translation) ? $translation->value : '');
                        break;
                    case 'value':
                        $propertyValue = $model->value()->with(['translations' => function (HasMany $query) use ($attribute) {
                            $query->where('language_id', '=', $attribute->getAssociatedLanguage()->id)->get();
                        }])->first();

                        if (! $propertyValue || ! $propertyValue->relationLoaded('translations') || ! $propertyValue->translations->first()) {
                            break;
                        }

                        /** @var PropertyValue $propertyValue */
                        $attribute->setValue($propertyValue->translations->first()->value);
                }
            }
        );

        return $filledAttributesCollection;
    }

    /**
     * Get or create a PropertyKey with a given translation value for a specific language and optionally associate it to a Property
     * Notice: When you want to associate it to a Property you need to save the property yourself.
     *
     * @param string $value
     * @param Language $language
     * @param Property $property
     * @return PropertyKey
     */
    public function getOrCreatePropertyKeyWithTranslationValue(string $value, Language $language, Property &$property):PropertyKey
    {
        /** @var PropertyKeyTranslation $keyTranslation */
        $keyTranslation = PropertyKeyTranslation::firstOrNew([
            'language_id' => $language->id,
            'value' => strtolower($value),
        ]);

        /** @var PropertyKey $propertyKey */
        if ($keyTranslation->exists) {
            $propertyKey = $keyTranslation->translatable;
        } else {
            $propertyKey = $property->key;
            if (! $propertyKey) {
                $propertyKey = $this->propertyKeyService->newModel();
                $propertyKey->save();
            }
            $propertyKey->translations()->save($keyTranslation);
        }

        $property->key()->associate($propertyKey);

        return $propertyKey;
    }

    /**
     * Get or create a PropertyValue with a given translation value for a specific language and optionally associate it to a Property
     * Notice: When you want to associate it to a Property you need to save the property yourself.
     *
     * @param string $value
     * @param Language $language
     * @param Property $property
     * @return PropertyValue
     */
    public function getOrCreatePropertyValueWithTranslationValue(string $value, Language $language, Property &$property = null):PropertyValue
    {
        /** @var PropertyValueTranslation $valueTranslation */
        $valueTranslation = PropertyValueTranslation::firstOrNew([
            'language_id' => $language->id,
            'value' => strtolower($value),
        ]);

        /** @var PropertyValue $propertyValue */
        if ($valueTranslation->exists) {
            $propertyValue = $valueTranslation->translatable;
        } else {
            $propertyValue = $property->value;
            if (! $propertyValue) {
                $propertyValue = $this->propertyValueService->newModel();
                $propertyValue->save();
            }
            $propertyValue->translations()->save($valueTranslation);
        }

        $property->value()->associate($propertyValue);

        return $propertyValue;
    }

    /**
     * Links a propertizable to properties
     * The properties id is a comma separated string
     *
     * @param PropertizableInterface $model
     * @param string $propertyIds
     */
    public function linkToProperties(PropertizableInterface $model, string $propertyIds):void
    {
        if (! $model->id) {
            return;
        }

        if ($propertyIds !== '') {
            $propertyIds = array_unique(explode(',', $propertyIds));
        } else {
            $propertyIds = [];
        }

        $properties = $this->modelClassName::whereIn('id', $propertyIds)->get();

        if (! $properties) {
            return;
        }

        $modelShortClassName = KommaHelpers::getShortNameFromClass($model, true);
        $relationName = Str::plural($modelShortClassName);

        if (! method_exists((new $this->modelClassName), $relationName)) {
            throw new \RuntimeException('The class "'.$this->modelClassName.'" must have, but did not have a MorphToMany relation method called "'.$relationName.'". Without it we cannot make the "'.get_class($model).'" a child of "'.$this->modelClassName.'"');
        }

        $model->properties()->sync($propertyIds);
    }

    /**
     * Create an array of Select options for a PropertyValueSelect
     *
     * @param Property $property
     * @param Language $language
     * @return array
     */
    public function getOptionModelsForPropertyValueSelect(Property $property, Language $language):array
    {
        //Get all the PropertyValues in a collection where it has a translation with associated with the language. And include the translations
        $values = $property->value()->whereHas('translations', function (Builder $query) use ($language) {
            $query->where('language_id', '=', $language->id);
        })->with(['translations' => function (HasMany $query) use ($language) {
            $query->where('language_id', '=', $language->id);
        }])->get();

        $optionsCollection = $values->map(function (PropertyValue $propertyValue, $index) {
            if ($propertyValue->translations->count() == 0) {
                return null;
            }
            $translation = $propertyValue->translations->first();

            /** @var SelectOptionInterface $option */
            $option = app(SelectOptionInterface::class);
            $option->setValue((string) $translation->value);
            $option->setHtmlContent($translation->value);
            $option->setContent($translation->value);

            return $option;
        });

        /** @var SelectOptionInterface $empty */
        $empty = app(SelectOptionInterface::class);
        $empty->setValue(null);
        $empty->setHtmlContent('&nbsp;');
        $empty->setContent('');

        $options = $optionsCollection->toArray();
        array_unshift($options, $empty); //add element to beginning

        return $options;
    }

    /**
     * Returns the category ids as a comma separated string for the CategorizableInterface implementation
     *
     * @param PropertizableInterface $model
     * @return string Category ids, comma separated
     */
    public function getPropertyIdsForModel(PropertizableInterface $model): ?string
    {
        if (! $model->id) {
            return null;
        }

        $idsCollection = $model->properties()->get(['property_id'])->map(function (Property $property) {
            return $property->property_id;
        });
        $idString = implode(',', $idsCollection->toArray());

        if ($idString == '') {
            return null;
        }

        return $idString;
    }

    /**
     * Creates or deletes a property depending on if the PropertyValueSelect has a numeric value or not.
     * Links a valueTranslation with the id that is the same as the attributes value to the property and
     * links it to the given model. The property will also get a predefined key name suffixed with the given nameSuffix
     *
     * @param PropertizableInterface $model
     * @param PropertyValueSelect $attribute
     * @param string $nameSuffix
     * @throws \Exception
     * @throws \Throwable
     */
    public function createOrDeletePropertyUsingPropertyValueSelectAttributeAndLinkItToModel(PropertizableInterface $model, PropertyValueSelect $attribute, string $nameSuffix)
    {
        $keyTranslationValue = ($this->getPropertyKeyTranslationValueForModel($model, $nameSuffix));

        //Get the value translation to link to the property
        /** @var PropertyValueTranslation $valueTranslation */
        $valueTranslation = PropertyValueTranslation::where('value', '=', $attribute->getValue())->first();
        /** @var Language $language */
        $language = $attribute->getAssociatedLanguage();

        if ($valueTranslation) {
            if ($this->propertyWithKeyAndValueTranslationValuesExists($keyTranslationValue, $valueTranslation->value, $language)) {
                return;
            }

            $alreadyExists = $this->createPropertyForPropertizableModelIfItNotExists($language, $keyTranslationValue, $valueTranslation->value, $model);
            if ($alreadyExists) {
                $this->deletePropertyForPropertizableModel($keyTranslationValue, $language);
            } //Already exists but with a new value. That's why we delete the old one.
            $this->createPropertyForPropertizableModelIfItNotExists($language, $keyTranslationValue, $valueTranslation->value, $model);
        } else {
            $this->deletePropertyForPropertizableModel($keyTranslationValue, $language);
        }
    }

    /**
     * Returns the id of a property if there is a property with the specified key and value translation values. false if not.
     *
     * @param string $propertyKeyTranslationValue
     * @param string $properyValueTranslationValue
     * @param Language $language
     * @return bool
     */
    public function propertyWithKeyAndValueTranslationValuesExists(string $propertyKeyTranslationValue, string $properyValueTranslationValue, Language $language = null)
    {
        /** @var PropertyKeyTranslation $keyTranslation */
        $keyTranslationQueryBuilder = PropertyKeyTranslation::where('value', '=', $propertyKeyTranslationValue);
        $keyTranslation = ($language) ? $keyTranslationQueryBuilder->where('language_id', '=', $language->id)->first() : $keyTranslationQueryBuilder->first();
        if (! $keyTranslation) {
            return false;
        }

        /** @var Collection $propertyValueTranslations */
        $valueTranslationQueryBuilder = PropertyValueTranslation::where('value', '=', $properyValueTranslationValue);
        $propertyValueTranslations = ($language) ? $valueTranslationQueryBuilder->where('language_id', '=', $language->id)->get() : $valueTranslationQueryBuilder->get();
        if (! $propertyValueTranslations) {
            return false;
        }

        /** @var PropertyKey $propertyKey */
        $propertyKey = $keyTranslation->translatable()->first();
        if (! $propertyKey) {
            return false;
        }

        /** @var Property $propertyFromKey */
        $propertyFromKey = $propertyKey->property()->first();
        if (! $propertyFromKey) {
            return false;
        }

        $keyPropertyId = $propertyFromKey->id;

        $existingPropertyIdOrFalse = false;
        $propertyValueTranslations->each(function (PropertyValueTranslation $propertyValueTranslation) use ($keyPropertyId, &$existingPropertyIdOrFalse) {
            /** @var PropertyValue $propertyValue */
            $propertyValue = $propertyValueTranslation->translatable()->first();
            if (! $propertyValue) {
                return null;
            } //Continue

            $valuePropertyId = $propertyValue->property_id;
            if ($keyPropertyId == $valuePropertyId) {
                $existingPropertyIdOrFalse = $keyPropertyId;

                return false; //Break
            }
        });

        return $existingPropertyIdOrFalse;
    }

    /**
     * Creates a new property with a key and a value (both with translations), sets the key and value translation values to the given ones, and links it to the given model.
     * All on one condition: A property must not exist with the given key translation value. Returns false if the property was created. true if it was not created because it already exists
     *
     * @param Language $language
     * @param string $propertyKeyTranslationValue
     * @param string $propertyValueTranslationValue
     * @param PropertizableInterface $model
     * @return bool alreadyExists
     * @throws \Throwable
     */
    public function createPropertyForPropertizableModelIfItNotExists(Language $language, string $propertyKeyTranslationValue, string $propertyValueTranslationValue, PropertizableInterface $model):bool
    {
        if (PropertyKeyTranslation::where('value', '=', $propertyKeyTranslationValue)->first()) {
            return true;
        }

        \DB::transaction(function () use ($propertyKeyTranslationValue, $language, $propertyValueTranslationValue, $model) {
            //Create the key
            $key = new PropertyKey();
            $key->save();
            $keyTranslation = new PropertyKeyTranslation();
            $keyTranslation->language()->associate($language);
            $keyTranslation->translatable()->associate($key);
            $keyTranslation->value = $propertyKeyTranslationValue;
            $keyTranslation->save();

            //Create the property
            $property = new Property();
            $property->key()->associate($key);
            $property->code_name = Str::slug($keyTranslation->value);
            $property->save();

            //Link the value translation to the property by linking its value to the property //TODO this could be a improvement. A value CAN belong to multiple properties (key_values). The only problem it gives is when you delete a property linked to that value you cant do a cascade delete because it can break other properties
            $value = new PropertyValue();
            $value->property()->associate($property);
            $value->save();

            $newValueTranslation = new PropertyValueTranslation();
            $newValueTranslation->value = $propertyValueTranslationValue;
            $newValueTranslation->language()->associate($language);
            $newValueTranslation->translatable()->associate($value);
            $newValueTranslation->save();

            $model->properties()->save($property);
        });

        return false;
    }

    /**
     * Deletes the property and its key and value by a keys translation value for a certain language
     *
     * @param string $keyTranslationValue
     * @param Language $keysTranslationLanguage
     * @throws \Throwable
     */
    public function deletePropertyForPropertizableModel(string $keyTranslationValue, Language $keysTranslationLanguage)
    {
        \DB::transaction(function () use ($keyTranslationValue, $keysTranslationLanguage) {
            //The value does not exist, so that means we need to delete the current property if it exists.
            /** @var PropertyKeyTranslation|null $keyTranslation */
            $keyTranslation = PropertyKeyTranslation::where([
                ['value', '=', $keyTranslationValue],
                ['language_id', '=', $keysTranslationLanguage->id],
            ])->first();
            if (! $keyTranslation) {
                return;
            }

            /** @var PropertyKey|null $key */
            $key = $keyTranslation->translatable()->first();
            if (! $key) {
                return;
            }

            /** @var Property $property */
            $property = $key->property()->first();
            if (! $property) {
                throw new \RuntimeException('PropertyKey with id: "'.$key->id.'"" did not have a property while it should');
            }

            /** @var PropertyValue $value */
            $value = $property->value()->first();
            if ($value) {
                $value->translations()->get()->each(function (PropertyValueTranslation $propertyValueTranslation) {
                    $propertyValueTranslation->delete();
                });
            }
            $value->delete();

            $key->delete();

            $property->delete();
        });
    }

    /**
     * Destroys the appropriate related models for a given model.
     * Those related models must be the responsibility of this service
     *
     * @param Model $property
     * @return Model
     */
    public function destroyForModel(Model $property): Model
    {
        \DB::transaction(function () use ($property) {
            /** @var Property $property */
            /** @var PropertyValue $value */
            $value = $property->value()->with(['translations'])->first();
            /** @var PropertyKey $key */
            $key = $property->key()->with(['translations'])->first();

            //Only delete the value if it does exist and does not belong to another property
            if ($value->properties()->count() == 1) {
                $translationIds = $value->translations()->get(['id', 'property_value_id'])->map(function (PropertyValueTranslation $propertyValueTranslation) {
                    return $propertyValueTranslation->id;
                });
                PropertyValueTranslation::destroy($translationIds);
                $value->delete();
            }

            //Only delete the key if it does exist and does not belong to another property
            if ($key->properties()->count() == 1) {
                $translationIds = $key->translations()->get(['id', 'property_key_id'])->map(function (PropertyKeyTranslation $propertyKeyTranslation) {
                    return $propertyKeyTranslation->id;
                });
                PropertyKeyTranslation::destroy($translationIds);
                $key->delete();
            }
        });

        return $property;
    }

    /**
     * Gets all properties for the specified property key translation value for the language the keyValue is in OR
     * for all languages if you specify the boolean as false
     *
     * @param string $keyTranslationValue
     * @return Builder|null
     */
    public function getPropertiesForPropertyKeyValue(string $keyTranslationValue):Builder
    {
        /** @var PropertyKeyTranslation|null $keyTranslation */
        $keyTranslation = PropertyKeyTranslation::where('value', '=', strtolower($keyTranslationValue))->first();
        if (! $keyTranslation) {
            return null;
        }

        /** @var PropertyKey|null $key */
        $key = $keyTranslation->translatable()->first();
        if (! $key) {
            return null;
        }

        /** @var $property */
        $properties = Property::where('property_key_id', '=', $key->id)->with(['value' => function (BelongsTo $query) {
            $query->with(['translations']);
        }]);

        return $properties;
    }

//    /**
//     * Set the attributes value with the id of the properties translation of which it has a key with a translation value like the getPropertyNameForModel method returns
//     *
//     * @param PropertizableInterface $model
//     * @param PropertyValueSelect $attribute
//     * @param string $nameSuffix
//     */
//    public function setPropertyValueSelectValueForModelIfPossible(PropertizableInterface $model, PropertyValueSelect $attribute, string $nameSuffix)
//    {
//        $name = $this->getPropertyKeyTranslationValueForModel($model, $nameSuffix);
//
//        /** @var PropertyKeyTranslation $keyTranslation */
//        $keyTranslation = PropertyKeyTranslation::where('value', '=', $name)->first();
//        if(!$keyTranslation) return;
//
//        /** @var PropertyKey $key */
//        $key = $keyTranslation->translatable()->first();
//        if(!$key) return;
//
//        /** @var Property $property */
//        $property = $key->property()->first();
//        if(!$property) return;
//
//        /** @var PropertyValue $value */
//        $value = $property->value()->first();
//        if(!$value) return;
//
//        /** @var PropertyValueTranslation $propertyValueTranslation */
//        $propertyValueTranslation = $value->translations()->where('language_id', '=', $attribute->getAssociatedLanguage()->id)->first();
//        if(!$propertyValueTranslation) return;
//
//        $attribute->setValue((string) $propertyValueTranslation->value);
//    }

    /**
     * Creates a property name for a model using a model and a suffix
     *
     * @param Model $model
     * @param string $nameSuffix
     * @return string
     */
    private function getPropertyKeyTranslationValueForModel(Model $model, string $nameSuffix)
    {
        return KommaHelpers::getShortNameFromClass($model).' '.$model->id.' '.$nameSuffix;
    }
}