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/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;
    }
}