File: D:/HostingSpaces/bekkers/bekkersengineering.nl/app/KommaApp/Shop/Properties/Kms/PropertyService.php
<?php
namespace App\KommaApp\Shop\Properties\Kms;
use App\Helpers\KommaHelpers;
use App\KommaApp\Kms\Core\Attributes\Attribute;
use App\KommaApp\Kms\Core\Attributes\Currency;
use App\KommaApp\Kms\Core\Attributes\Models\SelectOption;
use App\KommaApp\Kms\Core\Attributes\Models\SelectOptionInterface;
use App\KommaApp\Kms\Core\Attributes\TextField;
use App\KommaApp\Kms\Core\KmsInterface;
use App\KommaApp\Kms\Core\Sections\SectionService;
use App\KommaApp\Kms\Core\Sections\SectionTabItem;
use App\KommaApp\Kms\SidebarListItem;
use App\KommaApp\Languages\Models\Language;
use App\KommaApp\Shop\Properties\Models\PropertizableInterface;
use App\KommaApp\Shop\Properties\Models\Property;
use App\KommaApp\Shop\Properties\Models\PropertyKey;
use App\KommaApp\Shop\Properties\Models\PropertyKeyTranslation;
use App\KommaApp\Shop\Properties\Models\PropertyValue;
use App\KommaApp\Shop\Properties\Models\PropertyValueTranslation;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Builder;
/**
* Class PropertyService
* @package App\KommaApp\Shop\Properties
*/
class PropertyService extends SectionService
{
protected $sortable = false;
/** @var PropertyKeyService $propertyKeyService */
protected $propertyKeyService;
/** @var PropertyValueService $propertyValueService */
protected $propertyValueService;
function __construct()
{
$this->forModelName = Property::class;
$this->propertyKeyService = \App::make(PropertyKeyService::class);
$this->propertyValueService = \App::make(PropertyValueService::class);
parent::__construct();
}
/**
* This method will save an model
*
* @param $model Model or null
* @param Collection $sectionTabItems These must be filled with data. This is something you need to do yourself.
*
* @return mixed
*/
public function saveModel(Model $model = null, Collection $sectionTabItems): Model
{
$sectionTabItems->each(function($sectionTabItem) use($model, $sectionTabItems) {
/** @var SectionTabItem $sectionTabItem */
$attribute = $sectionTabItem->getAttribute();
$reference = $attribute->getsValueFromReference();
$value = $attribute->getValue();
switch ($attribute->getsValueFrom())
{
case Attribute::ValueFromItself:
if($reference == 'key') {
/** @var Property $model */
$propertyKey = $this->getOrCreatePropertyKeyWithTranslationValue(trim($value), $attribute->getAssociatedLanguage(), $model);
//Check if there is a translation that we can update or check if we need to create a new translation
$keyTranslation = $this->propertyKeyService->getOrCreateTranslationModelForModel($propertyKey, $attribute->getAssociatedLanguage());
$keyTranslation->value = $value;
$keyTranslation->save();
}
else if($reference == 'value') {
$propertyValue = $this->getOrCreatePropertyValueWithTranslationValue(trim($value), $attribute->getAssociatedLanguage(), $model);
//Check if there is a translation that we can update or check if we need to create a new translation
$valueTranslation = $this->propertyValueService->getOrCreateTranslationModelForModel($propertyValue, $attribute->getAssociatedLanguage());
$valueTranslation->value = $value;
$valueTranslation->save();
}
break;
}
});
$model->save();
$model = parent::saveModel($model, $sectionTabItems); //First make sure we have a model and save the attributes in them from the SectionTabItem attributes
//Return the page
return $model;
}
/**
* 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 = null):PropertyKey
{
/** @var PropertyKeyTranslation $keyTranslation */
$keyTranslation = PropertyKeyTranslation::where([
['language_id', '=', $language->id],
['value', '=', strtolower($value)]
])->first();
if($keyTranslation) {
$propertyKey = $keyTranslation->translatable()->first();
} else {
$propertyKey = $this->propertyKeyService->newModel();
$propertyKey->save();
}
if($property) $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::where([
['language_id', '=', $language->id],
['value', '=', strtolower($value)]
])->first();
if($valueTranslation) {
$propertyValue = $valueTranslation->translatable()->first();
} else {
$propertyValue = $this->propertyValueService->newModel();
$propertyValue->save();
}
if($propertyValue) $property->value()->associate($propertyValue);
return $propertyValue;
}
/**
* 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)
{
$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':
if(!$model->key()->first()) break;
$translation = $this->propertyKeyService->getOrCreateTranslationModelForModel($model->key()->first(), $attribute->getAssociatedLanguage());
$attribute->setValue($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;
}
/**
* 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->forModelName::whereIn('id', $propertyIds)->get();
if(!$properties) return;
$modelShortClassName = $this->kms->getShortNameFromClass($model, true);
$relationName = str_plural($modelShortClassName);
if(!method_exists((new $this->forModelName), $relationName)) {
throw new \RuntimeException('The class "'.$this->forModelName.'" 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->forModelName.'"');
}
$model->properties()->sync($propertyIds);
}
/**
* Returns the option models for a select attribute
*
* @param null $languageId
* @param int|null $excludeId
* @return array
*/
public function getOptionsForSelect($languageId = null, int $excludeId = null): array
{
$entities = [];
// Check if the getModelsAsTree method exist
// if ( ! method_exists($this, 'getModelsAsTree')) {
if ( $this->sortable == false) {
/** @var $sidebarListItems SidebarListItem[] */
$sidebarListItems = $this->getModelsForSideBar();
if ($excludeId == -1) $entities = [];
foreach ($sidebarListItems as $sidebarListItem) {
$model = $this->forModelName::find($sidebarListItem->getId());
if(!$model) continue;
$entities[] = (new SelectOption())
->setValue($model->id)
->setContent($sidebarListItem->getName())
->setHtmlContent($sidebarListItem->getName());
}
return $entities;
}
return [];
}
/**
* 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::make(SelectOptionInterface::class);
$option->setValue((string) $translation->value);
$option->setHtmlContent($translation->value);
$option->setContent($translation->value);
return $option;
});
/** @var SelectOptionInterface $empty */
$empty = \App::make(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
*/
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
*/
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
*/
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();
});
}
/**
* This method will remove an TranslatableModelInterface instance
*
* @param $model
* @throws \Exception
*/
public function destroyModel(Model $model)
{
\DB::transaction(function () use($model) {
/** @var Property $model */
/** @var Collection $value */
$value = $model->value()->with(['translations'])->first();
$key = $model->key()->with(['translations'])->first();
$valueTranslationIdsToDelete = [];
$keyTranslationIdsToDelete = [];
$model->delete();
//Only delete the value if it does exist and does not belong to another property
if($value && Property::where('property_value_id', '=', $value->id)->count() == 0) {
$value->translations->each(function (PropertyValueTranslation $propertyValueTranslation) use (&$valueTranslationIdsToDelete) {
$valueTranslationIdsToDelete[] = $propertyValueTranslation->id;
});
PropertyValueTranslation::destroy($valueTranslationIdsToDelete);
$value->delete();
}
//Only delete the key if it does exist and does not belong to another property
if($key && Property::where('property_key_id', '=', $key->id)->count() == 0) {
$key->translations->each(function (PropertyKeyTranslation $propertyValueTranslation) use (&$keyTranslationIdsToDelete) {
$keyTranslationIdsToDelete[] = $propertyValueTranslation->id;
});
PropertyKeyTranslation::destroy($keyTranslationIdsToDelete);
$key->delete();
}
});
}
/**
* 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 $this->kms->getShortNameFromClass($model).' '.$model->id.' '.$nameSuffix;
}
/**
* Returns all models for the sidebar menu in the backend
*
* @return array
*/
public function getModelsForSideBar(): array
{
$models = $this->forModelName::with([
'key' => function(BelongsTo $query) {
$query->with('translations');
},
'value' => function(BelongsTo $query) {
$query->with('translations');
}
])->get();
$sidebarList = [];
foreach ($models as $model) {
$sidebarListItem = new SidebarListItem();
$this->setThumbnail($model);
$this->generateThumbnail($model);
//Set the values for the sidebar
$sidebarListItem->setId($model->id);
$sidebarListItem->setStatus(true);
$title = KommaHelpers::str_limit_full_word($model->title, 75);
$sidebarListItem->setName($title);
$sidebarListItem->setThumbnail($model->thumbnail);
$sidebarList[] = $sidebarListItem;
}
return $sidebarList;
}
}