File: D:/HostingSpaces/blijegasten/blijegasten.be/app/Komma/Shop/Properties/Kms/PropertyService.php
<?php
namespace App\Komma\Shop\Properties\Kms;
use App\Helpers\KommaHelpers;
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\Models\Traits\HasThumbnailInterface;
use App\Komma\Kms\Core\Attributes\TextField;
use App\Komma\Kms\Core\Entities\DisplayNameInterface;
use App\Komma\Kms\Core\ModelService;
use App\Komma\Kms\Core\ModelServiceInterface;
use App\Komma\Kms\Core\Sections\SectionTabItem;
use App\Komma\Kms\Core\Sections\SideBarListItem;
use App\Komma\Globalization\Languages\Models\Language;
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\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
/**
* Class PropertyService
* @package App\Komma\Shop\Properties
*/
class PropertyService extends ModelService
{
protected $sortable = false;
/** @var ModelServiceInterface $propertyKeyService */
protected $propertyKeyService;
/** @var ModelServiceInterface $propertyValueService */
protected $propertyValueService;
/** @var TranslationServiceInterface */
private $keyTranslationService;
/** @var TranslationServiceInterface */
private $valueTranslationService;
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();
}
else if($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();
} else if($reference == 'code_name') {
$property->code_name = $attribute->getValue();
} else if($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);
};
} else if($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(' ');
$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;
}
}