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/Neopoints/momsecurity.be/app/Komma/Dynamic/ComponentArea/ComponentAreaService.php
<?php


namespace App\Komma\Dynamic\ComponentArea;

use App\Komma\Dynamic\Component\ComponentAttributeKey;
use App\Komma\Documents\Kms\DocumentServiceInterface;
use App\Komma\Documents\Models\Document;
use App\Komma\Dynamic\Component\Component;
use App\Komma\Dynamic\Component\ComponentSaveState;
use App\Komma\Dynamic\Componentables\ComponentableService;
use App\Komma\Dynamic\Componentables\ComponentableServiceInterface;
use App\Komma\Dynamic\ComponentType\ComponentTypeInterface;
use App\Komma\Dynamic\ComponentType\ComponentTypeResolver;
use App\Komma\Dynamic\ComponentType\SaveState\SaveStateInterface;
use App\Komma\Kms\Core\AbstractTranslatableModel;
use App\Komma\Kms\Core\Attributes\Attribute;
use App\Komma\Kms\Core\Attributes\ComponentArea as ComponentAreaAttribute;
use App\Komma\Dynamic\ComponentArea\ComponentArea as ComponentAreaModel;
use App\Komma\Kms\Core\Attributes\ComponentArea;
use App\Komma\Kms\Core\Attributes\Documents;
use App\Komma\Kms\Core\Sections\AttributeKey;
use Illuminate\Database\Eloquent\Model;

/**
 * Class ComponentAreasService
 *
 * To save and load Components from the database via a ComponentAreas attribute.
 *
 */
class ComponentAreaService implements ComponentAreaServiceInterface
{
    const SAVE_VERSION = '0.9.1';

    /** @var DocumentServiceInterface $documentService */
    private $documentService;

    /** @var ComponentableService $componentableService */
    private $componentableService;

    /**
     * ComponentAreaService constructor.
     */
    public function __construct()
    {
        $this->documentService = \App::make(DocumentServiceInterface::class);
        $this->componentableService = \App::make(ComponentableServiceInterface::class);
    }

    /**
     * Convert the json in the ComponentAreasAttribute into a componentAreas with Components and save them
     * The json comes from componentAreaManager.js
     *
     * @param Model $model
     * @param ComponentAreaAttribute $componentAreaAttribute
     * @return HasComponentAreasInterface
     * @throws \ReflectionException
     */
    public function saveOrUpdateComponentAreaForAttribute(Model $model, ComponentAreaAttribute $componentAreaAttribute): Model
    {
        $modelToUse = $this->getModelToUse($model, $componentAreaAttribute);
        if(!$modelToUse) return $model;

        //Split value into ComponentAreas with ComponentType models
        /** @var ComponentAreaModel $componentArea */
        $componentArea = $modelToUse->componentAreas()->firstOrNew([
            'dynamicable_id' => $modelToUse->id,
            'dynamicable_type' => get_class($modelToUse),
            'code_name' => $componentAreaAttribute->getsValueFromReference()
        ]);

        //Get all possible ComponentTypes
        $componentTypes = ComponentTypeResolver::resolveAll();
        if($componentTypes->count() == 0) throw new \RuntimeException('There are no group types present in the database. Make sure they are');

        if($componentAreaAttribute->getValue() == '') return $modelToUse;
        $componentAreasSaveState = ComponentAreaSaveState::fromJsonString($componentAreaAttribute->getValue());

        //Delete components that where deleted in the frontend
        $this->deleteOrphanedComponents($componentAreasSaveState, $componentArea);


        //If there are components, we are going to save the componentAreas because they need the id
        if($componentAreasSaveState->getComponentsCount() > 0) $componentArea->save();

        //Update or create each component from the savestate.
        $componentSaveStates = $componentAreasSaveState->getComponentSaveStates();
        foreach($componentSaveStates as $index => $componentSaveState) {
            //Verify that the savestate version is the one we expect
            if ($componentSaveState->getVersion() !== self::SAVE_VERSION) {
                throw new \RuntimeException('The save version of attribute at position ' . $componentSaveState->getSortOrder() . ' is expected to be ' . self::SAVE_VERSION . '. But was ' . $componentSaveState->getVersion());
            }

            //Get the ComponentType or throw an error
            $resultsCollection = $componentTypes->where('id', $componentSaveState->getComponentTypeId());
            if ($resultsCollection->count() == 0) {
                throw new \RuntimeException('The ComponentType with id: "' . $componentSaveState->getComponentTypeId() . '" from savestate does not exist.');
            }
            $componentType = $resultsCollection->first();

            //Get the existing component or a new one if id is 0 or lower
            if ($componentSaveState->getId() <= 0) {
                $component = new Component();
                $component->component_type_id = $componentSaveState->getComponentTypeId();
                $component->componentArea()->associate($componentArea);
                $component->save();
                $componentSaveStateBeforeSave = clone $componentSaveState;
                $componentSaveState = $this->updateComponentSaveStateAttributeKeys($componentSaveState, $component);
                $componentSaveState->setId($component->id);
//                    dd($componentSaveStateBeforeSave, $componentSaveState); //debug line
                $componentSaveState = $this->processUploadedDocumentsForComponent($componentSaveState->getId(), $component, $componentAreaAttribute, $componentSaveState, $componentSaveStateBeforeSave, $modelToUse);
            } else {
                $component = Component::where('id', '=', $componentSaveState->getId())->first();
                if (!$component) {
                    return $model; //Somebody deleted the component and we need to skip it.
                }

                $componentSaveState = $this->processUploadedDocumentsForComponent($component->id, $component, $componentAreaAttribute, $componentSaveState, $componentSaveState, $modelToUse);
            }



            //Fill / update the component
            $component->fill([
                'version' => self::SAVE_VERSION,
                'sort_order' => $componentSaveState->getSortOrder(),
                'data' => json_encode($componentSaveState->getData())
            ]);
            $component->save();
        }

        return $modelToUse;
    }


     /**
     * Instructs the document service to upload documents for the component area
     *
     * @param int $componentId
     * @param Component $component
     * @param ComponentAreaInterface $componentArea
     * @param ComponentSaveState $componentSaveState
     * @param ComponentSaveState $componentSaveStateBeforeSave The component save state before the component was saved and the savestate was update with the definitve component attributes
     * @param Model $model
     * @return ComponentSaveState
     * @throws \ReflectionException
     */
    private function processUploadedDocumentsForComponent(int $componentId, Component $component, ComponentAreaInterface $componentArea, ComponentSaveState $componentSaveState, ComponentSaveState $componentSaveStateBeforeSave, Model $model) {

        $componentType = ComponentTypeResolver::resolve($componentSaveState->getComponentTypeId());
        if (!$componentType) {
            return $componentSaveState;
        }

        /** @var SaveStateInterface $saveState */
        foreach ($componentType->save_state_instance->getAttributeInstances() as $attributeNumber => $attributeInstance) {
            /** @var Attribute $attributeInstance */
            if (is_a($attributeInstance, Documents::class)) {
                $beforeSaveKey = ComponentAttributeKey::createInstance(
                    $componentSaveStateBeforeSave->getId(),
                    $attributeInstance->getsValueFromReference(),
                    AttributeKey::getAttributeShortClassNameFromString($componentArea->getKey()),
                    AttributeKey::getValuePartFromString($componentArea->getKey()),
                    AttributeKey::getTranslationISO2FromString($componentArea->getKey())
                );

                /** @var Documents $attributeInstance */ //$componentArea->getKey()
                $afterSaveKey = ComponentAttributeKey::createInstance(
                    $componentId,
                    $attributeInstance->getsValueFromReference(),
                    AttributeKey::getAttributeShortClassNameFromString($componentArea->getKey()),
                    AttributeKey::getValuePartFromString($componentArea->getKey()),
                    AttributeKey::getTranslationISO2FromString($componentArea->getKey())
                );
                $attributeInstance->setKey($afterSaveKey);

                if ($componentArea->getSubFolder() != '') {
                    $attributeInstance->setSubFolder($componentArea->getSubFolder());
                }

                $this->documentService->processUploadedDocumentsForModel($component, $attributeInstance, $beforeSaveKey, $afterSaveKey);
            }
        }

        return $componentSaveState;
    }

    /**
     * Updates the component save states attribute keys so that they contain a reference to the component id.
     * For example. Consider that a new components attribute has a key like this: ComponentArea-dynamic_group_sections-C0-A1-nl.
     * Notice the C0. It means that the component has an id of 0, which means it is new. After you've saved the component, it does have
     * an id of for example 37. You can then use this method to update the attributes key in the savestate so that it is:
     * ComponentArea-dynamic_group_sections-C37-A1-nl.
     *
     * @param ComponentSaveState $componentSaveState
     * @param Component $component
     * @return ComponentSaveState
     */
    private function updateComponentSaveStateAttributeKeys(ComponentSaveState $componentSaveState, Component $component): ComponentSaveState
    {
        $attributesData = $componentSaveState->getData();
        foreach($attributesData as $oldAttributeKey => $attributeValue) {
            if(ComponentAttributeKey::couldBeAKeyString($oldAttributeKey)) {
                $newAttributeKey = ComponentAttributeKey::createInstanceFromString($oldAttributeKey);
                $newAttributeKey->setComponentId($component->id);
                $attributesData[(string) $newAttributeKey] = $attributeValue;
                unset($attributesData[$oldAttributeKey]);
            }
        }
        $componentSaveState->setData($attributesData);

        return $componentSaveState;
    }

    /**
     * Determines which groups aren't in the savestate anymore and delete them
     *
     * @param ComponentAreaSaveState $componentAreasSaveState
     * @param ComponentAreaModel $componentAreas
     */
    private function deleteOrphanedComponents(ComponentAreaSaveState $componentAreasSaveState, ComponentAreaModel $componentAreas)
    {
        if(!$componentAreas->exists) return;

        //Get the existing group ids:
        $existingGroupIds = [];
        $componentSaveStates = $componentAreasSaveState->getComponentSaveStates();
        foreach($componentSaveStates as $componentSaveState) {
            $existingGroupIds[] = (integer) $componentSaveState->getId();
        }

        $componentAreas->components()->get(['id'])->each(function(Component $component) use ($existingGroupIds) {
           if(!in_array($component->id, $existingGroupIds, true)) {
               $component->documents()->delete();
               $component->delete();
           }
        });
    }

    /**
     * Get the model to save or load to and from, depending on the attributes configuration
     *
     * @param Model $model
     * @param ComponentAreaAttribute $attribute
     * @return HasComponentAreasInterface
     */
    private function getModelToUse(Model $model, ComponentAreaAttribute $attribute): ?HasComponentAreasInterface
    {
        if(is_a($model, AbstractTranslatableModel::class) && $attribute->hasAssociatedLanguage()) {
            /** @var $model AbstractTranslatableModel */
            $translationModel = $model->translations->where('language_id', $attribute->getAssociatedLanguage()->id)->first();
            if(!$translationModel || $translationModel->exists == false) return null;
            if(!is_a($translationModel, HasComponentAreasInterface::class)) throw new \RuntimeException('Expected class "'.get_class($translationModel).'" to be an "'.HasComponentAreasInterface::class.'"');
            /** @var $translationModel HasComponentAreasInterface */
            return $translationModel;
        }

        if(!is_a($model, HasComponentAreasInterface::class)) throw new \RuntimeException('Expected class "'.get_class($model).'" to be an "'.HasComponentAreasInterface::class.'"');
        /** @var $model HasComponentAreasInterface */
        if($model->exists == false) return null;
        return $model;
    }

    /**
     * Convert a componentArea with its Components to a json string which componentAreaManager.js can handle
     *
     * @param Model $model
     * @param ComponentAreaAttribute $attribute
     * @return string
     */
    public function getComponentsSaveStateDataAsJsonFromModel(Model $model, ComponentAreaAttribute $attribute): string
    {
        if(!$model->exists) return '';
        $model = $this->getModelToUse($model, $attribute);
        if(!$model) return '';

        /** @var ComponentAreaModel $componentArea */
        $componentArea = $model->componentAreas()->where('code_name', '=', $attribute->getsValueFromReference())->first();
        if(!$componentArea) return '';

        $componentAreaSaveState = new ComponentAreaSaveState();
        $componentArea->components()->orderBy('sort_order', 'ASC')->get()->each(function(Component $component) use(&$componentAreaSaveState, $attribute) {
            if($component->version !== self::SAVE_VERSION) throw new \RuntimeException('The save version of a saved component with id '.$component->id.' is expected to be '.self::SAVE_VERSION.'. But was '.$component->version);

            $componentSaveState = new ComponentSaveState();
            $componentSaveState->setId($component->id);
            $componentSaveState->setComponentTypeId($component->component_type_id);
            $componentSaveState->setVersion($component->version);
            $componentSaveState->setSortOrder($component->sort_order);
            $componentSaveState->setData(json_decode($component->data, true));

            $componentablesData = $this->componentableService->loadComponentablesDataForComponent($component, $componentSaveState, $attribute->getKey());
            $componentSaveState->setComponentableData($componentablesData);

            $componentType = ComponentTypeResolver::resolve($component->component_type_id);

            /** @var SaveStateInterface $saveStateInstance */
            $saveStateInstance = $componentType->save_state_instance;
            foreach($saveStateInstance->getAttributeInstances() as $attributeNumber => $attributeInstance) {
                if (is_a($attributeInstance, Documents::class)) {
                    $key = array_keys($componentSaveState->getData())[$attributeNumber];
                    $attributeKey = explode('-data', $key)[0];
                    $data = $componentSaveState->getData();
                    $data[$key] = json_encode($component->documents()->where('key', '=', $attributeKey)->get());
                    $componentSaveState->setData($data);
                }
            }
            $componentAreaSaveState->addComponentSaveState($componentSaveState);
        });

        $value = json_encode($componentAreaSaveState);

        return $value;
    }

    /**
     * Loops over all attributes in a component and modifies their keys so that they become ComponentAttribute keys.
     * In those keys it is made clear for which component and attribute the key is for.
     *
     * @param ComponentTypeInterface $componentType
     * @param string $componentAreaAttributeKeyAsString
     * @param int $componentId
     * @return ComponentTypeInterface
     */
    public static function generateComponentAttributeKeysForComponent(ComponentTypeInterface $componentType, string $componentAreaAttributeKeyAsString, int $componentId)
    {
        /** @var SaveStateInterface $saveStateInstance */
        $saveStateInstance = $componentType->save_state_instance;
        $saveStateInstance = clone $saveStateInstance; //Make sure we leave the original save state intact

        $attributes = $saveStateInstance->getAttributeInstances();

        foreach($attributes as $attributeNumber => $attribute)
        {
            /** @var Attribute $attribute */
            if(!AttributeKey::couldBeAKeyString($componentAreaAttributeKeyAsString)) throw new \InvalidArgumentException('The component area attribute key must be the string version of a "'.AttributeKey::class.'" instance. But was "'.$componentAreaAttributeKeyAsString.'"');
            /** @var AttributeKey $attributeKey */
            $attributeKey = AttributeKey::createInstanceFromString($componentAreaAttributeKeyAsString);

//            $componentAttributeKey = ComponentAttributeKey::createInstanceFromAttributeKey($attributeKey, $componentId, $attributeNumber + 1);
//            $componentAttributeKey = ComponentAttributeKey::createInstanceFromAttributeKey($attributeKey, $componentId, $attribute->getKey()->getValuePart());
            $componentAttributeKey = ComponentAttributeKey::createInstance(
                $componentId,
                $attribute->getsValueFromReference(),
                AttributeKey::getAttributeShortClassNameFromString($componentAreaAttributeKeyAsString),
                AttributeKey::getValuePartFromString($componentAreaAttributeKeyAsString),
                AttributeKey::getTranslationISO2FromString($componentAreaAttributeKeyAsString)
            );

            $attribute->swapKeyTo($componentAttributeKey);
        }

        return $componentType;
    }

    /**
     * Delete document models for components that don't exist anymore
     */
    private function deleteOrphanedDocuments()
    {
        $componentIds = Component::get(['id'])->pluck(['id'])->toArray();

        $documentsToDelete = Document::get(['id', 'documentable_id'])->filter(function (Document $document) use($componentIds) {
            return !in_array($document->documentable_id, $componentIds, true); //When the document component id is not in the array of existing components, return true. Resulting the document to be in $documentsToDelete
        });

        DocumentService::deleteDocuments($documentsToDelete);
    }
}