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/ste.komma.pro/app/Components/ComponentArea/ComponentAreaService.php
<?php


namespace App\Components\ComponentArea;

use App\Attributes\ComponentArea as ComponentAreaAttribute;
use App\Components\Attributables\AttributableServiceInterface;
use App\Components\ComponentType\ComponentTypeFactory;
use App\Components\ComponentType\SaveState\SaveStateInterface;
use App\Components\Attributables\AttributableService;
use App\Components\Component\Component;
use App\Components\Component\ComponentAttributeKey;
use App\Components\Component\ComponentSaveState;
use App\Components\ComponentType\Types\AbstractComponentType;
use Komma\KMS\Documents\Kms\DocumentServiceInterface;
use Komma\KMS\Documents\Models\Document;
use Komma\KMS\Core\AbstractModelHandler;
use Komma\KMS\Core\Attributes\Attribute;
use App\Components\ComponentArea\ComponentArea as ComponentAreaModel;
use Komma\KMS\Core\Attributes\Documents;
use Komma\KMS\Core\HouseKeeping\CanDoHousekeepingInterface;
use Komma\KMS\Core\Sections\AttributeKey;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

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

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

    /** @var AttributableServiceInterface $attributeableService */
    private $attributeableService;

    /**
     * ComponentAreaService constructor.
     */
    public function __construct()
    {
        $this->documentService = app(DocumentServiceInterface::class);
        $this->attributeableService = app(AttributableServiceInterface::class);
    }

    /**
     * Puts the values of attributes in an Eloquent model. And then saves that model.
     *
     * @param Model $model
     * @param Collection $attributes
     * @return Model
     */
    public function save(Model $model, Collection $attributes = null): Model
    {
        if($attributes === null) return $model;

        $this->checkContainsAttributes($attributes);
        $attributes->each((function(Attribute $attribute) use($model) {
            $this->saveAttribute($model, $attribute);
        })->bindTo($this));

        return $model;
    }

    /**
     * Convert the json in the ComponentAreasAttribute into a componentAreas with Components and save them
     * The json comes from componentAreaManager.js
     *
     * @param Model $model
     * @param Attribute $attribute
     * @return HasComponentAreasInterface
     * @throws \ReflectionException
     * @throws \Throwable
     */
    public function saveAttribute(Model $model, Attribute $attribute): Model {
        if(!is_a($model,HasComponentAreasInterface::class)) return $model;
            if(!is_a($attribute, ComponentAreaAttribute::class)) return $model;

            //Split value into ComponentAreas with ComponentType models
            /** @var ComponentAreaAttribute $attribute */
            /** @var HasComponentAreasInterface $model */
            /** @var ComponentAreaModel $componentArea */
            $componentArea = $model->componentAreas()->firstOrNew([
                'componentable_id' => $model->id,
                'componentable_type' => get_class($model),
                'code_name' => $attribute->getsValueFromReference()
            ]);

            $componentAreasSaveState = ComponentAreaSaveState::fromJsonString($attribute->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 componentType still exists
                $componentType = ComponentTypeFactory::make($componentSaveState->getComponentTypeId());

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

                //            DB::transaction(function () use ($componentSaveState, $componentAreaAttribute, $componentArea, $model, $componentType) { //NOTE: It should be safer to use database transactions. But if we do, strange stuff starts to happen. Like deleted documents to show up in tabbed components
                //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);
                    $this->attributeableService->saveAttributablesForComponentAttributes($attribute, $componentType,
                        $componentSaveState->getId(), $component->id);
                    $this->attributeableService->removeAttributableAttributesFromComponentSaveState($componentSaveState,
                        $attribute, $componentType, $componentSaveState->getId());
                    $componentSaveState->setId($component->id);
                    //                    dd($componentSaveStateBeforeSave, $componentSaveState); //debug line
                    $componentSaveState = $this->processUploadedDocumentsForComponent($componentSaveState->getId(),
                        $component, $attribute, $componentSaveState, $componentSaveStateBeforeSave, $model);
                } 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,
                        $attribute, $componentSaveState, $componentSaveState, $model);
                    $this->attributeableService->saveAttributablesForComponentAttributes($attribute, $componentType,
                        $componentSaveState->getId(), $component->id);
                    $this->attributeableService->removeAttributableAttributesFromComponentSaveState($componentSaveState,
                        $attribute, $componentType, $componentSaveState->getId());
                }


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

                $component->save();
            }

        return $model;
    }

    /**
     * Instructs the document service to store / update uploaded 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): ComponentSaveState {
        $componentType = ComponentTypeFactory::make($componentSaveState->getComponentTypeId());
        if (!$componentType) return $componentSaveState;

        //Save, delete update documents
        /** @var SaveStateInterface $saveState */
        foreach ($componentType->getAttributes() 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);
                $componentSaveState = $this->reloadDocumentsIntoComponentSaveState($componentSaveState, $attributeInstance); //We do this since some documents may be deleted for example. And they should not be in the save state in that case.
            }
        }

        return $componentSaveState;
    }

    /**
     * Reloads documents into the component save state using the document key.
     *
     * @param ComponentSaveState $componentSaveState
     * @param Documents $attribute
     * @return ComponentSaveState
     */
    public function reloadDocumentsIntoComponentSaveState(ComponentSaveState $componentSaveState, Documents $attribute): ComponentSaveState
    {
        $attributeKey = $attribute->getKey();
        $documents = Document::where('key', '=', $attributeKey)->get();
        $data = $componentSaveState->getData();
        if(!array_key_exists((string) $attributeKey, $data)) throw new \RuntimeException('ComponentAreaService: Could not reload documents for attribute with key '.$attributeKey.' Data: '.json_encode($data));
        $data[(string) $attributeKey] = json_encode($documents);
        $componentSaveState->setData($data);
        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 $componentArea
     * @throws \Throwable
     */
    private function deleteOrphanedComponents(ComponentAreaSaveState $componentAreasSaveState, ComponentAreaModel $componentArea) {
        if (!$componentArea->exists) {
            return;
        }

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

        \DB::transaction(function () use ($componentArea, $componentAreasSaveState, $existingComponentIds) { //Use a transaction to make sure both component and attributables are deleted.
            //Delete the components that where deleted in the frontend.
            $componentArea->components()->get(['id'])->each(function (Component $component) use ($existingComponentIds) {
                if (!in_array($component->id, $existingComponentIds, true)) {
                    $component->documents()->delete();
                    $component->delete();
                }
            });
        });
    }

    /**
     * Gets the values of an Eloquent model and passes them to a collection of attributes
     *
     * @param Model $model
     * @param Collection $attributes
     * @return Collection
     */
    public function load(Model $model, Collection $attributes = null): Collection
    {
        if($attributes === null) return new Collection();

        if(!is_a($model, HasComponentAreasInterface::class) || !$model->exists) return $attributes;
        return $attributes->map((function(Attribute $attribute) use($model) {
            return $this->loadAttribute($model, $attribute);
        })->bindTo($this));
    }

    /**
     * Convert a componentArea with its Components to a json string which componentAreaManager.js can handle
     *
     * @param Model $model
     * @param Attribute $attribute
     * @return Attribute
     */
    public function loadAttribute(Model $model, Attribute $attribute): Attribute
    {
        if(!is_a($model, HasComponentAreasInterface::class) || !$model->exists) return $attribute;

        if(!is_a($model,HasComponentAreasInterface::class)) return $attribute;

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


        $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 = $this->getComponentSaveStateForComponent($component);
            $componentType = ComponentTypeFactory::make($component->component_type_id);

            /** @var SaveStateInterface $saveStateInstance */
            foreach ($componentType->getAttributes() as $attributeNumber => $componentAttribute) {
                /** @var Attribute $componentAttribute*/
                if (is_a($componentAttribute, Documents::class)) {

                    $key = new ComponentAttributeKey();
                    $key->setAttributeShortClassName(AttributeKey::getAttributeShortClassNameFromString($attribute->getKey()));
                    $key->setValuePart($attribute->getsValueFromReference());
                    $key->setAttributeReference($componentAttribute->getsValueFromReference());
                    $key->setComponentId($component->id);
                    $key->setTranslationIso2($attribute->getKey()->getTranslationIso2());

                    $componentSaveState = $this->loadDocumentsIntoComponentSaveState($componentSaveState, $key);
                }
            }
            $componentAreaSaveState->addComponentSaveState($componentSaveState);
        });

        $value = json_encode($componentAreaSaveState);
        $attribute->setValue($value);
        return $attribute;
    }

    /**
     * @param ComponentSaveState $componentSaveState
     * @param ComponentAttributeKey $attributeKey
     * @return ComponentSaveState
     */
    private function loadDocumentsIntoComponentSaveState(ComponentSaveState $componentSaveState, ComponentAttributeKey $attributeKey)
    {
        $data = $componentSaveState->getData();
        $data[(string) $attributeKey] = json_encode(Document::where('key', '=', $attributeKey)->get());
        $componentSaveState->setData($data);
        return $componentSaveState;
    }

    /**
     * Gets the components save state.
     * Notice that this is not only the components data attribute.
     * Some of the data also comes from Attributable models, stored elsewhere in the database.
     *
     * @param Component $component
     * @return ComponentSaveState
     */
    public function getComponentSaveStateForComponent(Component $component): ComponentSaveState {
        $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));

        $componentSaveState = $this->attributeableService->loadAttributablesForComponentAttributes($componentSaveState, $component->id);


        return $componentSaveState;
    }

    /**
     * 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 AbstractComponentType $componentType
     * @param string $componentAreaAttributeKeyAsString
     * @param int $componentId
     * @return AbstractComponentType
     */
    public static function generateComponentAttributeKeysForComponent(AbstractComponentType $componentType, string $componentAreaAttributeKeyAsString, int $componentId)
    {
        /** @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.'"');

        foreach($componentType->getAttributes() as $attributeNumber => $attribute)
        {
            $componentAttributeKey = ComponentAttributeKey::createInstance(
                $componentId,
                $attribute->getsValueFromReference(),
                AttributeKey::getAttributeShortClassNameFromString($componentAreaAttributeKeyAsString),
                AttributeKey::getValuePartFromString($componentAreaAttributeKeyAsString),
                AttributeKey::getTranslationISO2FromString($componentAreaAttributeKeyAsString)
            );

            \Log::debug("ComponentAreaService:399 ".$componentAttributeKey);
            $attribute->setKey($componentAttributeKey);
        }

        return $componentType;
    }

    /**
     * Delete document models for components that don't exist anymore
     */
    private static 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);
    }

    /**
     * Clean up old stuff
     */
    public static function doHouseKeeping()
    {
        self::deleteOrphanedDocuments();
        AttributableService::deleteOrphanedComponentsAttributables();
    }

    /**
     * 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)
    {
        if(!is_a($model, HasComponentAreasInterface::class)) return;

        DB::transaction(function () use($model) {
            /** @var HasComponentAreasInterface $model */
            $model->componentAreas()->with('components')->get()->each(function(ComponentAreaModel $componentArea) {
                $componentArea->components->each(function(Component $component) {
                    $component->documents()->delete();
                    $component->delete();
                });
                $componentArea->delete();
            });
        });
        return $model;
    }
}