File: D:/HostingSpaces/SBogers95/rentman.io/app/Komma/Dynamic/ComponentArea/ComponentAreaService.php
<?php
namespace App\Komma\Dynamic\ComponentArea;
use App\Komma\Documents\Kms\DocumentService;
use App\Komma\Documents\Kms\DocumentServiceInterface;
use App\Komma\Documents\Models\Document;
use App\Komma\Dynamic\Attributables\AttributableService;
use App\Komma\Dynamic\Attributables\AttributableServiceInterface;
use App\Komma\Dynamic\Component\Component;
use App\Komma\Dynamic\Component\ComponentAreaInterface;
use App\Komma\Dynamic\Component\ComponentAttributeKey;
use App\Komma\Dynamic\Component\ComponentSaveState;
use App\Komma\Dynamic\ComponentArea\ComponentArea as ComponentAreaModel;
use App\Komma\Dynamic\ComponentType\ComponentTypeFactory;
use App\Komma\Dynamic\ComponentType\Types\AbstractComponentType;
use App\Komma\Dynamic\SaveState\SaveStateInterface;
use App\Komma\Kms\Core\AbstractTranslatableModel;
use App\Komma\Kms\Core\Attributes\Attribute;
use App\Komma\Kms\Core\Attributes\ComponentArea;
use App\Komma\Kms\Core\Attributes\ComponentArea as ComponentAreaAttribute;
use App\Komma\Kms\Core\Attributes\Documents;
use App\Komma\Kms\Core\HouseKeeping\CanDoHousekeepingInterface;
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, CanDoHousekeepingInterface
{
const SAVE_VERSION = '0.9.1';
/** @var DocumentServiceInterface */
private $documentService;
/** @var AttributableServiceInterface */
private $attributeableService;
/**
* ComponentAreaService constructor.
*/
public function __construct()
{
$this->documentService = \App::make(DocumentServiceInterface::class);
$this->attributeableService = \App::make(AttributableServiceInterface::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([
'componentable_id' => $modelToUse->id,
'componentable_type' => get_class($modelToUse),
'code_name' => $componentAreaAttribute->getsValueFromReference(),
]);
if ($componentArea->exists) {
$componentArea->load('components');
$backUpString = '';
foreach ($componentArea->components as $component) {
$backUpString .= $component->data."\n\n";
}
\DB::table('component_area_backups')->insert([
'componentable_id' => $modelToUse->id,
'componentable_type' => get_class($modelToUse),
'language_id' => isset($modelToUse->language_id) ? $modelToUse->language_id : null,
'code_name' => $componentAreaAttribute->getsValueFromReference(),
'json' => $backUpString,
]);
}
if (empty($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 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, $modelToUse, $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
$beforeSavedKey = $componentSaveState->getId();
if ($beforeSavedKey <= 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($componentAreaAttribute, $componentType, $componentSaveState->getId(), $component->id);
$this->attributeableService->removeAttributableAttributesFromComponentSaveState($componentSaveState, $componentAreaAttribute, $componentType, $componentSaveState->getId());
$componentSaveState->setId($component->id);
// Disable for imports
if (config('runImport') !== true) {
$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);
$this->attributeableService->saveAttributablesForComponentAttributes($componentAreaAttribute, $componentType, $componentSaveState->getId(), $component->id);
$this->attributeableService->removeAttributableAttributesFromComponentSaveState($componentSaveState, $componentAreaAttribute, $componentType, $componentSaveState->getId());
}
// Uncommit for running update import
// $this->bindDocumentsToImportedUpdates($beforeSavedKey, $componentSaveState->getId(), $modelToUse);
//Fill / update the component
$component->fill([
'version' => self::SAVE_VERSION,
'sort_order' => $componentSaveState->getSortOrder(),
'data' => json_encode($componentSaveState->getData()),
]);
$component->save();
// });
}
$componentAreaUpdatedAt = $componentArea->updated_at;
$savedComponent = $componentArea->components;
$componentAreaNewUpdatedAt = null;
foreach ($savedComponent as $component) {
if ($component->updated_at >= $componentAreaUpdatedAt) {
if (! isset($componentAreaNewUpdatedAt)) {
$componentAreaNewUpdatedAt = $component->updated_at;
} elseif ($component->updated_at >= $componentAreaNewUpdatedAt) {
$componentAreaNewUpdatedAt = $component->updated_at;
}
}
}
// Update related updated_at columns
if (isset($componentAreaNewUpdatedAt)) {
// Check if we also need to update te belong model
if ($componentAreaNewUpdatedAt >= $modelToUse->updated_at) {
$modelToUse->updated_at = $componentAreaNewUpdatedAt;
$modelToUse->save();
}
// Update the component area updated_at
$componentArea->updated_at = $componentAreaNewUpdatedAt;
$componentArea->save();
}
return $modelToUse;
}
/**
* QaD hack to overwrite the document key to the desired models
*
* @param $beforeSavedKey
* @param $afterSavedKey
* @param $model
*/
private function bindDocumentsToImportedUpdates($beforeSavedKey, $afterSavedKey, $model)
{
$documentKeyToOverwrite = 'ComponentArea|update_components|C'.$beforeSavedKey.'|image|'.$model->language->iso_2;
$desiredDocumentKey = 'ComponentArea|update_components|C'.$afterSavedKey.'|image|'.$model->language->iso_2;
Document::where('key', $documentKeyToOverwrite)->update([
'key' => $desiredDocumentKey,
'documentable_id' => $afterSavedKey,
'documentable_type' => Component::class,
]);
}
/**
* 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, ComponentArea $componentArea, ComponentSaveState $componentSaveState, ComponentSaveState $componentSaveStateBeforeSave, Model $model)
{
$componentType = ComponentTypeFactory::make($componentSaveState->getComponentTypeId());
if (! $componentType) {
return $componentSaveState;
}
/** @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->swapKeyTo($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 $componentArea
*/
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[] = (int) $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();
}
});
});
}
/**
* 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 = $this->getComponentSaveStateForComponent($component);
$componentType = ComponentTypeFactory::make($component->component_type_id);
/** @var SaveStateInterface $saveStateInstance */
foreach ($componentType->getAttributes() as $attributeNumber => $attributeInstance) {
if (is_a($attributeInstance, Documents::class)) {
$componentSaveState = $this->loadDocumentsIntoComponentSaveState($componentSaveState, $attributeNumber);
}
}
$componentAreaSaveState->addComponentSaveState($componentSaveState);
});
$value = json_encode($componentAreaSaveState);
return $value;
}
/**
* @param ComponentSaveState $componentSaveState
* @param int $attributeNumber
* @return ComponentSaveState
*/
private function loadDocumentsIntoComponentSaveState(ComponentSaveState $componentSaveState, int $attributeNumber)
{
$keys = array_keys($componentSaveState->getData());
if (isset($keys[$attributeNumber])) {
$key = $keys[$attributeNumber];
$attributeKey = explode('-data', $key)[0];
$data = $componentSaveState->getData();
$data[$key] = 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);
$decodeData = json_decode($component->data, true);
if (isset($decodeData)) {
$componentSaveState->setData($decodeData);
}
$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)
);
$attribute->swapKeyTo($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();
}
}