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