File: D:/HostingSpaces/SBogers10/shop.komma.nl/app/Properties/Kms/PropertyService.php
<?php
namespace App\Properties\Kms;
use App\Attributes\PropertyDataCollection;
use App\Products\Product\Product;
use App\Products\ProductComposite\ProductComposite;
use App\Products\ProductGroup\ProductGroup;
use App\Properties\Models\PropertizableInterface;
use App\Properties\Models\Property;
use App\Properties\Models\PropertyKey;
use App\Properties\Models\PropertyKeyTranslation;
use App\Properties\Models\PropertyKeyValueInterface;
use App\Properties\Models\PropertyKeyValueTranslationInterface;
use App\Properties\Models\PropertyValue;
use App\Properties\Models\PropertyValueTranslation;
use App\Properties\Resources\Key;
use App\Properties\Resources\KeyValueTranslation;
use App\Properties\Resources\Property as PropertyResource;
use App\Properties\Resources\Value;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Komma\KMS\Core\AbstractTranslatableModel;
use Komma\KMS\Core\Attributes\Attribute;
use Komma\KMS\Core\Attributes\Models\SelectOption;
use Komma\KMS\Core\Attributes\Models\Traits\HasThumbnailInterface;
use Komma\KMS\Core\ModelService;
use Komma\KMS\Core\Sections\SidebarListItem;
use Komma\KMS\Globalization\Languages\Models\Language;
/**
* Class PropertyService
* @package App\Komma\Shop\Properties
*/
class PropertyService extends ModelService
{
private $alreadySavedFixed = false;
function __construct()
{
$this->setModelClassName(Property::class);
parent::__construct();
}
/**
* @param Model $propertizable
* @param Collection $attributes
*/
public function saveForPropertizable(Model $propertizable, Collection $attributes): Model {
$attributes->each(function(Attribute $attribute) use ($propertizable) {
if (!is_a($attribute, PropertyDataCollection::class)) return;
$value = $attribute->getValue();
$data = json_decode($value, true);
if (!is_array($data)) return;
\DB::transaction(function() use($data, $attribute, $propertizable) {
foreach ($data as $propertyData) {
//Validate some of the property data first
if(
!array_key_exists('id', $propertyData) ||
!array_key_exists('state', $propertyData)
) continue;
//Get the property in question if possible
/** @var Property|null $property */
$property = Property::with('key.translations', 'values.translations')->where('id', '=', $propertyData['id'])->first();
//Determine what should happen to it and delegate to the methods that handle the states
switch ($propertyData['state']) {
case Property::NEW:
case Property::DIRTY:
$this->savePropertyFromJsonArray($propertyData, $propertizable);
break;
case Property::DELETED:
if(!$property) continue 2;
$this->destroyProperty($property);
break;
}
}
});
});
return $propertizable;
}
/**
* @param array $data
* @param PropertizableInterface $propertizable
*
* @return PropertyResource
*/
protected function savePropertyFromJsonArray(array $data, PropertizableInterface $propertizable): PropertyResource
{
if(!array_key_exists('key', $data)) throw new \InvalidArgumentException('The data must contain a key attribute, but it did not contain it.');
/** @var Property $property */
$property = Property::with('key.translations', 'values.translations')->where('id', '=', $data['id'])->first();
if(!$property) $property = new Property();
$property->fill($data);
$property->save();
$property->propertizable()->associate($propertizable);
if(array_key_exists('values', $data)) $this->savePropertyValuesFromJsonArray($property, $data['values']);
$this->savePropertyKeyFromJsonArray($property, $data['key']);
$property->save();
return new PropertyResource($property);
}
/**
* @param Property $property
* @param array $data
*
* @return Key
*/
protected function savePropertyKeyFromJsonArray(Property $property, array $data) {
if(!array_key_exists('state', $data)) {
throw new \InvalidArgumentException('The data must contain a state attribute, but it did not contain it.');
}
$propertyKey = null;
switch ($data['state']) {
case Property::NEW:
$propertyKey = new PropertyKey();
$propertyKey->save();
$property->key()->associate($propertyKey);
$property->save();
if (array_key_exists('translations', $data)) {
$this->savePropertyKeyValueTranslationsFromJsonArray($propertyKey, $data['translations']);
}
break;
case Property::PRISTINE:
case Property::DIRTY:
$propertyKey = PropertyKey::find($data['id']);
$property->key()->associate($propertyKey);
$property->save();
if (array_key_exists('translations', $data)) {
$this->savePropertyKeyValueTranslationsFromJsonArray($propertyKey, $data['translations']);
}
break;
case Property::DELETED:
$propertyKey = PropertyValue::find($data['id']);
//We don't delete property key / values this way. We ignore it.
break;
}
return new Key($propertyKey);
}
/**
* @param Property $property
* @param array $data
*
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
protected function savePropertyValuesFromJsonArray(Property $property, array $data) {
$values = new Collection();
foreach ($data as $valueData) {
switch ($valueData['state']) {
case Property::NEW:
$propertyValue = new PropertyValue();
$propertyValue->property()->associate($property);
$propertyValue->save();
if (array_key_exists('translations', $valueData)) {
$this->savePropertyKeyValueTranslationsFromJsonArray($propertyValue, $valueData['translations']);
}
break;
case Property::PRISTINE:
case Property::DIRTY:
$propertyValue = PropertyValue::find($valueData['id']);
$property->values()->save($propertyValue);
$propertyValue->fill($valueData);
if (array_key_exists('translations', $valueData)) {
// if(count($valueData['translations']) > 0) dd($valueData);
$this->savePropertyKeyValueTranslationsFromJsonArray($propertyValue, $valueData['translations']);
}
break;
case Property::DELETED:
$propertyValue = PropertyValue::find($valueData['id']);
//We don't delete property key / values this way. We ignore it.
break;
}
$values->push(new Value($propertyValue));
}
return Value::collection($values);
}
protected function savePropertyKeyValueTranslationsFromJsonArray(PropertyKeyValueInterface $propertyKeyValue, array $data): Collection {
$translationClass = $className = get_class($propertyKeyValue->translations()->getRelated());
// if(is_a($translationClass, PropertyValueTranslation::class)) dd($translationClass, $data);
return collect($data)->map(function(array $data) use($propertyKeyValue, $translationClass) {
if(!array_key_exists('state', $data)) throw new \InvalidArgumentException('The data must contain a state attribute, but it did not contain it.');
$translation = null;
switch ($data['state']) {
case Property::NEW:
$translation = new $translationClass;
$translation->fill($data);
if(array_key_exists('language', $data)) $this->savePropertyKeyValueTranslationLanguage($translation, $data['language']);
$propertyKeyValue->translations()->save($translation);
// dd($propertyKeyValue, $translation);
break;
case Property::PRISTINE:
case Property::DIRTY:
/** @var PropertyKeyValueTranslationInterface|null $translation */
$translation = $translationClass::find($data['id']);
if(!$translation) throw new \InvalidArgumentException('Data refers to a key that does not exist anymore. Cannot save a '.$translationClass);
$translation->fill($data);
if(array_key_exists('language', $data)) $this->savePropertyKeyValueTranslationLanguage($translation, $data['language']);
$translation->save();
break;
case Property::DELETED:
$translation = $translationClass::find($data['id']);
//We don't delete property keys this way. We ignore it.
break;
}
if($translation) {
return new KeyValueTranslation($translation);
}
return null;
})->filter(
fn($translationResource) => is_a($translationResource, KeyValueTranslation::class)
);
}
/**
* @param PropertyKeyValueTranslationInterface $translation
* @param $data
*
* @return PropertyValueTranslation
*/
protected function savePropertyKeyValueTranslationLanguage(PropertyKeyValueTranslationInterface $translation, $data): PropertyKeyValueTranslationInterface {
if(!array_key_exists('iso_2', $data)) throw new \InvalidArgumentException('The data must contain an iso_2 attribute. But did not contain it.');
$language = Language::where('iso_2', '=', $data['iso_2'])->first();
$translation->language()->associate($language);
return $translation;
}
/**
* Delete property keys and values that do not belong to a Property
*
* @throws \Exception
*/
public function deleteOrphaned() {
$this->deleteOrphanedPropertyKeys();
$this->deleteOrphanedPropertyValues();
}
/**
* Delete property keys that do not belong to a property or a given property
*/
public function deleteOrphanedPropertyKeys() {
\DB::transaction(function () {
$orphanedKeysQuery = PropertyKey::whereDoesntHave('properties')->whereDoesntHave('categoriesThatHaveThisPropertyKeyAsRequired')->with('translations');
$orphanedKeysQuery->get()->each(function (PropertyKey $propertyKey) {
$translationIds = $propertyKey->translations->pluck('id');
PropertyKeyTranslation::destroy($translationIds); //We can safely delete the translations because they belong only to 1 PropertyKey
});
$orphanedKeysQuery->delete();
});
}
/**
* Delete property values that do not belong to a Property or a given property
*/
public function deleteOrphanedPropertyValues() {
\DB::transaction(function () {
$orphanedValuesQuery = PropertyValue::whereDoesntHave('property')->with('translations');
$orphanedValuesQuery->get()->each(function (PropertyValue $propertyKey) {
$translationIds = $propertyKey->translations->pluck('id');
PropertyValueTranslation::destroy($translationIds); //We can safely delete the translations because they belong only to 1 PropertyValue
});
$orphanedValuesQuery->delete();
});
}
/**
* @throws \Exception
* @param Property $property
*/
protected function destroyProperty(Property $property) {
/** @var Property $property */
//Only delete the values if they do exist and dont not belong to another property
foreach ($property->values as $propertyValue) {
$propertyValue->delete();
$propertyValue->translations()->delete();
}
//Only delete the key if it does exist and does not belong to another property
if($property->key->properties()->count() == 1 && $property->key->categoriesThatHaveThisPropertyKeyAsRequired()->count() === 0) {
$property->key->delete();
$property->key->translations()->delete();
}
$property->delete();
$this->deleteOrphaned();
}
/**
* Links a propertyKey and value to a given property. If one of them does not exist, they will be saved.
*
* @param Property $property
* @param PropertyKey $propertyKey
* @param PropertyValue $propertyValue
*/
protected function linkPropertyParts(Property $property, PropertyKey $propertyKey, PropertyValue $propertyValue) {
if(!$propertyKey->exists) $propertyKey->save();
if(!$propertyValue->exists) $propertyValue->save();
$property->key()->associate($propertyKey);
$property->value()->associate($propertyValue);
$keyTranslation = $propertyKey->translation()->first();
if($keyTranslation) $property->code_name = Str::slug($keyTranslation->name);
$property->save();
}
/**
* 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) {
$this->debug('Skipping loading properties for model "'.get_class($model).'". Because we did not get attributes to load');
return $attributes;
}
$this->checkContainsAttributes($attributes);
/** @var \Illuminate\Database\Eloquent\Collection $properties */
if(is_a($model, PropertizableInterface::class)) {
$this->loadForPropertizable($model, $attributes);
} elseif (is_a($model, Property::class)) {
$this->loadForProperty($model, $attributes);
}
return $attributes;
}
/**
* Puts the values of attributes in an Eloquent model. And then saves that model.
*
* @param Model|PropertizableInterface $model
* @param Collection $attributes
*
* @return Model|PropertizableInterface
*/
public function save(Model $model, Collection $attributes = null): Model
{
if($attributes === null) {
$this->debug('Skipping loading properties for model "'.get_class($model).'". Because we did not get attributes to load');
return $attributes;
}
$this->checkContainsAttributes($attributes);
/** @var \Illuminate\Database\Eloquent\Collection $properties */
if(is_a($model, PropertizableInterface::class)) {
$this->saveForPropertizable($model, $attributes);
} else {
throw new \RuntimeException('Not implemented');
}
return $model;
}
protected function loadForProperty(Property $model, Collection $attributes) {
$model->load('key.translations', 'values.translations');
$attributes->each(function(Attribute $attribute) use($model) {
if($attribute->getsValueFromReference() !== 'key' ||
!$attribute->getAssociatedLanguage() ||
!$model->key)
return;
$propertyKeyTranslation = $model->key->translations->where('language_id', $attribute->getAssociatedLanguage()->id)->first();
if(!$propertyKeyTranslation) return;
$attribute->setValue($propertyKeyTranslation->value);
});
}
/**
* @param PropertizableInterface $model
* @param Collection $attributes
*/
protected function loadForPropertizable(PropertizableInterface $model, Collection $attributes) {
if(!is_a($model, AbstractTranslatableModel::class)) return;
$attributes->each(function (Attribute $attribute, $index) use ($model, $attributes) {
if (!is_a($attribute, PropertyDataCollection::class)) return;
$mappedProperties = $model->properties->map(function(Property $property) {
return new PropertyResource($property);
});
$attribute->setValue(json_encode($mappedProperties));
});
}
/**
* Get or create a PropertyKey with a given translation value for a specific language.
*
* @param string $value
* @param Language|int $language
* @param PropertyKey $propertyKey
*
* @return PropertyKey
*/
public function getOrCreatePropertyKeyWithTranslationValue(string $value, Language $language):PropertyKey
{
/** @var PropertyKeyTranslation $keyTranslation */
$keyTranslation = PropertyKeyTranslation::firstOrNew([
'language_id' => $language->id,
'name' => strtolower($value)
]);
/** @var PropertyKey $propertyKey */
if($keyTranslation->exists && $keyTranslation->translatable) {
$propertyKey = $keyTranslation->translatable;
} else {
$propertyKey = $keyTranslation->translatable;
if(!$propertyKey) {
$propertyKey = new PropertyKey();
$propertyKey->save();
}
$propertyKey->translations()->save($keyTranslation);
$propertyKey->load('translations');
}
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):PropertyValue
{
/** @var PropertyValueTranslation $valueTranslation */
$valueTranslation = PropertyValueTranslation::firstOrNew([
'language_id' => $language->id,
'name' => strtolower($value)
]);
/** @var PropertyValue $propertyValue */
if($valueTranslation->exists && $valueTranslation->translatable) {
$propertyValue = $valueTranslation->translatable;
} else {
$propertyValue = $valueTranslation->translatable;
if(!$propertyValue) {
$propertyValue = new PropertyValue();
$propertyValue->save();
}
$propertyValue->translations()->save($valueTranslation);
$propertyValue->load('translations');
}
return $propertyValue;
}
/**
* 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->properties()->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;
}
/**
* Destroys the appropriate related models for a given model.
* Those related models must be the responsibility of this service
*
* @param Model $model
* @return Model
*/
public function destroyForModel(Model $model): Model
{
if(is_a(!$model, PropertizableInterface::class)) {
$this->debug('Skipping destroying properties for model "'.get_class($model).'". Because we did not get a propertizable model.');
return $model;
}
/** @var PropertizableInterface $model */
\DB::transaction(function () use($model) {
$model->properties()->with('values.translations', 'key.translations')->get()->each(function(Property $property) {
$this->destroyProperty($property);
});
});
return $model;
}
/**
* Gets all properties for the specified property key translation value for the language the keyValue is in
*
* @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;
}
/**
* @param Language|null $language
*
* @return PropertyKey[]|Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
*/
public function getPropertyKeyOptionsForSelect(Language $language = null) {
$queryBuilder = PropertyKey::with('translations');
if($language) {
$queryBuilder->whereHas('translations', function(Builder $builder) use($language) {
$builder->where('language_id', '=', $language->id);
});
}
return $queryBuilder->get()->map(function(PropertyKey $propertyKey) {
$displayName = $propertyKey->translations->map(function(PropertyKeyTranslation $propertyKeyTranslation) {
return $propertyKeyTranslation->name;
})->join(' / ');
return (new SelectOption())->setValue($propertyKey->id)->setContent($displayName)->setHtmlContent($displayName);
});
}
/**
* @param Language $language
* @return Builder
*/
public function allKeyTranslationsForLanguage(Language $language = null) {
$query = PropertyKeyTranslation::query();
if($language) $query->where('language_id', '=', $language->id);
return $query;
}
/**
* @param Language $language
* @return Builder
*/
public function allValueTranslationsForLanguage(Language $language = null) {
$query = PropertyValueTranslation::query();
if($language) $query->where('language_id', '=', $language->id);
return $query;
}
public function getModelsForSideBar(): array
{
$properties = $this->modelClassName::with('key.translations', 'values.translations')->get();
$sidebarList = [];
foreach ($properties as $user) {
//New SidebarListItem
$sidebarListItem = new SidebarListItem();
$user->title = $user->email; //used in KmsRepository::setThumbnail
/** @var HasThumbnailInterface $user */
$user->generateThumbnail();
//Set the values for the sidebar
$sidebarListItem->setId($user->id);
$sidebarListItem->setStatus(!$user->is_admin);
$sidebarListItem->setName($user->getDisplayName());
$sidebarListItem->setThumbnail($user->getThumbnail());
$sidebarListItem->alsoSearchInAttributesOfModel($user);
$sidebarList[] = $sidebarListItem;
}
return $sidebarList;
}
/**
* Converts name to a productable FQCN or null if invalid
*
* @param $type
*
* @return string|null
*/
public function productableFQCN($type) {
$propertizableTypeClass = null;
switch ($type) {
case 'product':
$propertizableTypeClass = Product::class;
break;
case 'product_group':
$propertizableTypeClass = ProductGroup::class;
break;
case 'product_composite':
$propertizableTypeClass = ProductComposite::class;
break;
}
return $propertizableTypeClass;
}
/**
* @param Collection $collection
* @param string $className
* @param bool $mayTrowException
*
* @return bool
*/
protected function checkContainsInstancesOfClass(Collection $collection, string $className, bool $mayTrowException = true)
{
$containsInstancesOf = true;
$collection->each(function ($attribute) use(&$containsInstancesOf, $className, $mayTrowException) {
if(!is_a($attribute, $className)) {
if($mayTrowException) throw new \InvalidArgumentException('The collection did not exclusively contain instances of: .'.$className);
$containsInstancesOf = false;
return false;
}
});
return $containsInstancesOf;
}
}