File: D:/HostingSpaces/SBogers10/otium.komma.nl/vendor/komma/kms/src/Core/SectionController.php
<?php
namespace Komma\KMS\Core;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\MessageBag;
use Illuminate\View\View;
use Komma\KMS\Components\ComponentArea\ComponentAreaService;
use Komma\KMS\Components\ComponentArea\ComponentAreaServiceInterface;
use Komma\KMS\Core\Attributes\ComponentArea;
use Komma\KMS\Core\Attributes\Documents;
use Komma\KMS\Core\Sections\SectionService;
use Komma\KMS\Core\Tree\NestedSets\Nodes\TreeModelInterface;
use Komma\KMS\Helpers\KommaHelpers;
use Komma\KMS\Documents\Kms\DocumentService;
use Komma\KMS\Core\Attributes\Attribute;
use Komma\KMS\Core\Attributes\Models\Traits\HasThumbnailInterface;
use Komma\KMS\Core\Entities\DisplayNameInterface;
use Komma\KMS\Core\Sections\Section;
use Komma\KMS\Core\Tree\TreeServiceInterface;
use Komma\KMS\Sites\SiteServiceInterface;
abstract class SectionController extends Controller
{
use AuthorizesRequests;
/** @var $forModelInstance Model|HasThumbnailInterface|DisplayNameInterface */
protected $forModelInstance;
protected string $slug;
protected string $classModelName;
protected bool $showEntity = true;
protected ?string $forTranslationModelName = null;
protected Section $section;
protected DocumentService $documentService;
protected SiteServiceInterface $siteService;
protected TreeServiceInterface $treeService;
protected ComponentAreaService $componentAreaService;
protected ModelServiceInterface $modelService;
protected TranslationServiceInterface $translationService;
protected AttributeDataServiceInterface $dataService;
private RelatedModelServiceInterface $relatedModelService;
private SectionService $sectionService;
public function __construct(Section $section)
{
$this->validateSetup();
$this->section = $section;
$this->dataService = app(AttributeDataServiceInterface::class);
$this->modelService = app(ModelServiceInterface::class);
$this->modelService->setModelClassName($this->classModelName);
$this->forModelInstance = $this->modelService->getForModelFromRoute($this->slug);
$this->treeService = app(TreeServiceInterface::class);
$this->treeService->setClassModelName($this->classModelName);
if($this->forTranslationModelName) {
$this->translationService = app(TranslationServiceInterface::class);
$this->translationService->setModelClassName($this->forTranslationModelName);
}
$this->documentService = new DocumentService();
$this->siteService = app(SiteServiceInterface::class);
$this->sectionService = new SectionService($section, $this->siteService);
$this->sectionService->setModelClassName($this->classModelName);
$this->relatedModelService = app(RelatedModelServiceInterface::class);
$this->relatedModelService->setModelClassName($this->classModelName);
$this->componentAreaService = app(ComponentAreaServiceInterface::class);
}
/**
* This method is called on the overview page.
* It will render the section and view.
*
* @return mixed
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index()
{
$this->authorize('index', $this->classModelName);
//Create a root model if the modelClassName instance implements the treeInterface
$this->modelService->createRootTreeModelIfNeeded();
//Disable the right pane
$this->showEntity = false;
return $this->render(null);
}
/**
* This method is called when a item is selected.
* By default it will generate an edit form.
*
* @param Model $model
* @return mixed
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show($model)
{
$this->authorize('show', $model);
//Create a root model if the model
//ClassName instance implements the treeInterface
$this->modelService->createRootTreeModelIfNeeded();
//Call the edit method
// $this->section->loadModels(); //we need to do this here because this won't work in section constructors since there the authenticated user is not available in laravel 5.3
return $this->edit($model);
}
/**
* This method is called when we want to edit an item.
* It is called from the $this->show method.
*
* @param Model $model
* @return mixed
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function edit($model)
{
$this->authorize('edit', $model);
//Create a root model if the modelClassName instance implements the treeInterface
$this->modelService->createRootTreeModelIfNeeded();
return $this->render($model);
}
/**
* This method will generate a new item form
*
* @return mixed
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create()
{
$this->authorize('create', $this->classModelName);
//Create a root model if the modelClassName instance implements the treeInterface
$this->modelService->createRootTreeModelIfNeeded();
//Render the form
return $this->render();
}
/**
* This method will validate and save the model
* It is called on create form submit,
* and it is called on the form edit.
*
* @return mixed
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Throwable
*/
public function store()
{
$this->authorize('store', $this->classModelName);
$this->modelService->createRootTreeModelIfNeeded();
//Prepare the new model, and attributes
$model = $this->modelService->newModel();
if($this->forTranslationModelName) $model = $this->translationService->makeAndInjectEmptyTranslationsIntoTranslatableIfNeeded($model);
$this->sectionService->generateAttributesAndAddThemToTabs($model);
$attributes = $this->section->getAttributes();
//Validate the form data and fill the attributes from input
$validator = $this->dataService->validateInputAndReturnValidator($attributes);
if ($validator->fails()) return redirect()->back()->withInput()->withErrors($validator);
$attributes = $this->dataService->fillAttributesFromInput($attributes);
$byValueFrom = $attributes->mapToGroups(function($attribute, $index) {
/** @var Attribute $attribute */
return [$attribute->getsValueFrom() => $attribute];
});
$model = $this->save($model, $byValueFrom);
//Set the siteSlug variable for routes that need the siteSlug variable in their route
$routeParameters = [];
$site = $this->siteService->getCurrentSite();
if($site->exists)
$routeParameters['siteSlug'] = $site->slug;
$routeParameters[strtolower(KommaHelpers::getShortNameFromClass($model, true))] = $model;
$sessionData = [
'tabslug' => request()->get('tabslug'),
'success' => __('KMS::global.saved')
];
return Redirect::action('\\'.get_class($this) . '@show', $routeParameters)->with($sessionData);
}
/**
* This method handles the update functionality.
* And is called by the edit form.
*
* @param $model Model
* @return mixed
* @throws \Exception
* @throws \Throwable
*/
public function update($model)
{
$this->authorize('update', $model);
$this->modelService->createRootTreeModelIfNeeded();
//Prepare the model and the attributes
if($this->forTranslationModelName) $model = $this->translationService->makeAndInjectEmptyTranslationsIntoTranslatableIfNeeded($model);
$this->sectionService->generateAttributesAndAddThemToTabs($model);
$attributes = $this->section->getAttributes();
//Validate the form data and fill the attributes from input
$validator = $this->dataService->validateInputAndReturnValidator($attributes);
if ($validator->fails()) return redirect()->back()->withInput()->withErrors($validator);
$attributes = $this->dataService->fillAttributesFromInput($attributes);
$byValueFrom = $attributes->mapToGroups(function($attribute, $index) {
/** @var Attribute $attribute */
return [$attribute->getsValueFrom() => $attribute];
});
$model = $this->save($model, $byValueFrom);
//Set the siteSlug variable for routes that need the siteSlug variable in their route
$routeParameters = [];
$site = $this->siteService->getCurrentSite();
if($site->exists)
$routeParameters['siteSlug'] = $site->slug;
$routeParameters[strtolower(KommaHelpers::getShortNameFromClass($model, true))] = $model;
$sessionData = [
'tabslug' => request()->get('tabslug'),
'success' => __('KMS::global.saved')
];
//Redirect the users to the "show" page.
// return \Redirect::action('\\'.get_class($this) . '@show', $routeParameters)->with($sessionData);
return Redirect::action('\\'.get_class($this) . '@show', $routeParameters)->with($sessionData);
}
/**
* This method is called when a item will be deleted
*
* @param Model $model
* @return mixed
* @throws \Exception
*/
public function destroy(Model $model)
{
$this->authorize('destroy', $model);
$this->componentAreaService->destroyForModel($model);
if($this->forTranslationModelName) {
$model->translations()->get()->each(function (AbstractTranslationModel $translationModel) {
$this->componentAreaService->destroyForModel($translationModel);
});
}
$this->modelService->createRootTreeModelIfNeeded();
//Set the siteSlug variable for routes that need the siteSlug variable in their route
$routeParameters = [];
$site = $this->siteService->getCurrentSite();
if($site->exists)
$routeParameters['siteSlug'] = $site->slug;
/** @var Model $model */
$model = $this->destroyForModel($model);
if(is_a($model, TreeModelInterface::class)) {
/** @var TreeModelInterface $model */
$model = $this->treeService->destroyForModelAndExecuteCallbackForeach($model, function(TreeModelInterface $childModel) {
$childModel = $this->destroyForModel($childModel);
});
$model->deleteWithChildren();
} else {
$model->delete();
}
//Return to the item index
return Redirect::action('\\' . get_class($this) . '@index', $routeParameters)->with('message', __('KMS::global.removed'));
}
/**
* Deletes stuff attached or related to the given model.
* So that afterwards you can dispose that model.
*
* @param Model $model
* @return Model
*/
protected function destroyForModel(Model $model) {
$this->documentService->destroyForModel($model);
if($this->forTranslationModelName) {
$model->translations()->get()->each(function(AbstractTranslationModel $translationModel) {
$this->documentService->destroyForModel($translationModel);
});
$model = $this->translationService->destroyForModel($model);
}
$model = $this->modelService->destroyForModel($model);
return $model;
}
/**
* Returns a rendered view
*
* @param Model $model
* @return View
*/
protected function render(Model $model = null)
{
if ($this->showEntity) $this->sectionService->generateAttributesAndAddThemToTabs($model);
if (!$model) return $this->makeView(); //Return a view since we can't fill it with data because the model is not set
if($this->showEntity) {
$byValueFrom = $this->section->getAttributes()->mapToGroups(function($attribute, $index) {
/** @var Attribute $attribute */
return [$attribute->getsValueFrom() => $attribute];
});
$this->load($model, $byValueFrom);
}
return $this->makeView();
}
/**
* Makes the view and returns it
*
* @return View
*/
protected function makeView(): View
{
$tabslug = null !== request()->get('tabslug') ? request()->get('tabslug') : Session::get('tabslug', '');
$sessionData = [
'tabslug' => $tabslug
];
$modelId = ($this->forModelInstance) ? $this->forModelInstance->id : null;
$successes = (Session::has('successes')) ? Session::get('successes') : new MessageBag();
$thumbnail = '';
if(is_a($this->forModelInstance, HasThumbnailInterface::class)) {
$thumbnail = $this->forModelInstance->getThumbnail();
};
if(is_a($this->forModelInstance, DisplayNameInterface::class)) {
$displayName = $this->forModelInstance->getDisplayName();
} else {
$displayName = $this->section->getSectionNewModel();
}
$saveRoute = $this->modelService->getSaveRoute($this->slug, $modelId);
// $sideBarModels = $this->modelService->getModelsForSideBar();p
$siteSlug = !$this->siteService->getCurrentSite()->exists ? null : $this->siteService->getCurrentSite()->slug;
// If model has TreeModelInterface enable the sortable
$sortable = is_a(new $this->classModelName, TreeModelInterface::class);
return \View::make('KMS::section.index', [
'sectionTitle' => $this->section->getSectionTitle(),
'sectionTabs' => $this->section->getTabs(),
'slug' => $this->slug,
'siteSlug' => $siteSlug,
'successes' => $successes,
'models' => $this->modelService->getModelsForSideBar(),
'maxUploadSize' => KommaHelpers::fileUploadMaxSize(),
'maxPostSize' => KommaHelpers::maxPostSize(),
'modelClassName' => $this->classModelName,
'saveRoute' => $saveRoute,
'sortable' => $sortable,
'showEntity' => $this->showEntity,
'thumbnail' => $thumbnail,
'displayName' => $displayName,
'currentModel' => $this->forModelInstance,
'submitButtonLabel' => $this->section->getSubmitButtonLabel(),
'preventNavigationTranslation' => json_encode(__('KMS::prevent_navigation'))
])->with($sessionData);
}
/**
* Get the models a a tree for api calls
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function getStructureAsJson()
{
$this->authorize('index', $this->classModelName);
$rootModel = $this->modelService->getRootModelForTree();
$structure = $this->treeService->getStructure($rootModel);
return json_encode($structure);
}
/**
* Change the model tree
*
* @return \Illuminate\Http\JsonResponse
*/
public function setStructureAsJson()
{
// Special way to get POST data send by ajax on IIS servers
$data = json_decode(request()->getContent());
$tree = json_decode($data->tree, true);
$this->treeService->setStructure($tree);
return response()->json($tree);
}
private function validateSetup()
{
if(!$this->classModelName) throw new \RuntimeException('Make sure you set (override) the $classModelName property in the controller ('.get_class($this).') to the eloquent model the controller does it\'s job for.');
if(!class_exists($this->classModelName)) throw new \RuntimeException('The modelClassName must be a class but was not ('.$this->classModelName.').');
if(!$this->slug) throw new \RuntimeException('Make sure you set (override) the $slug property in the controller ('.get_class($this).') to appropriate slug.');
}
/**
* A function that MUST delegate to services to accomplish saving.
* It MUST NOT act like a service.
*
* @param Model $model
* @param Collection $attributesByValueFrom Keys must be ValueFrom integers. Values must be Attributes that have the ValueFrom integers
* @return Model
*/
protected function save(Model $model, Collection $attributesByValueFrom = null): Model
{
if($attributesByValueFrom == null) return $model;
/** @var Attribute $attribute */
$this->modelService->save($model, $attributesByValueFrom->collapse());
if(is_a($model, AbstractTranslatableModel::class)) $this->translationService->save($model, $attributesByValueFrom->get(Attribute::ValueFromTranslationModel));
$documentAttributes = $attributesByValueFrom->get(Attribute::ValueFromDocuments, collect([]));
//Get document attributes for translatable models
$translatableDocumentAttributes = $documentAttributes->filter(function(Attribute $attribute) {
return !$attribute->hasAssociatedLanguage();
});
$this->documentService->save($model, $translatableDocumentAttributes);
//Retrieve attributes whose value's determine if a translation must be saved.
//We do this because some things like documents and component areas are linked to a translation, and therefore it must exist if there are any.
$attributesThatCanForceTranslationSave = $attributesByValueFrom->flatten()->filter(function(Attribute $attribute) {
return is_a($attribute, Documents::class) || is_a($attribute, ComponentArea::class);
});
if(is_a($model, AbstractTranslatableModel::class)) {
//When a documents or componentArea attribute isn't empty, we need to save the translation
$this->translationService->forceSaveWhenTrue($model, function(AbstractTranslationModel $translation) use($attributesThatCanForceTranslationSave, $model) {
foreach($attributesThatCanForceTranslationSave as $attributeThatCanForceTranslationSave) {
if (!$translation->exists && $attributeThatCanForceTranslationSave->getValue() !== '[]') return true;
}
return false;
});
//Now we can be sure, translations exist and we can save documents and component areas if needed.
$this->executeCallbackForModelsTranslationsMatchingAttributeLanguage($model, $attributesThatCanForceTranslationSave,
function (AbstractTranslationModel $translation, $attribute) use ($model) {
if(is_a($attribute, Documents::class)) $this->documentService->save($translation, collect([$attribute]));
elseif(is_a($attribute, ComponentArea::class)) $this->componentAreaService->saveAttribute($translation, $attribute);
}
);
}
$this->siteService->save($model, $attributesByValueFrom->collapse()->filter(function (Attribute $attribute) use ($model) {
return $attribute->getsValueFromReference() == 'site_id';
}));
$this->relatedModelService->save($model, $attributesByValueFrom->get(Attribute::ValueFromModelHasManyRelation));
return $model;
}
/**
* A function that MUST delegate to services to accomplish loading.
* It MUST NOT act like a service.
*
* @param Model $model
* @param Collection $attributesByValueFrom Keys must be ValueFrom integers. Values must be Attributes that have the ValueFrom integers
* @return Collection The same collection as you've passed in. Only "filled".
*/
protected function load(Model $model, Collection $attributesByValueFrom = null): Collection
{
if($attributesByValueFrom === null) return new Collection();
/** @var Attribute $attribute */
$this->modelService->load($model, $attributesByValueFrom->collapse());
if($this->forTranslationModelName) $this->translationService->load($model, $attributesByValueFrom->get(Attribute::ValueFromTranslationModel));
$documentAttributes = $attributesByValueFrom->get(Attribute::ValueFromDocuments, collect([]));
//Get document attributes for translatable models
$translatableDocumentAttributes = $documentAttributes->filter(function(Attribute $attribute) {
return !$attribute->hasAssociatedLanguage();
});
$this->documentService->load($model, $translatableDocumentAttributes);
//Get document attributes for translation models
$translationDocumentAttributes = $documentAttributes->filter(function(Attribute $attribute) {
return $attribute->hasAssociatedLanguage();
});
//Get the component area attributes for translation models
$translationComponentAreaAttributes = $attributesByValueFrom->get(Attribute::ValueFromComponentArea, collect())->filter(function(Attribute $attribute) {
return $attribute->hasAssociatedLanguage();
});
$componentAreaService = $this->componentAreaService;
if(is_a($model, AbstractTranslatableModel::class)) {
$this->executeCallbackForModelsTranslationsMatchingAttributeLanguage($model, $translationDocumentAttributes,
function (AbstractTranslationModel $translationModel, Documents $attribute) use($componentAreaService) {
$this->documentService->load($translationModel, collect([$attribute]));
$componentAreaService->loadAttribute($translationModel, $attribute);
});
$this->executeCallbackForModelsTranslationsMatchingAttributeLanguage($model, $translationComponentAreaAttributes, function(AbstractTranslationModel $translationModel, $attribute) {
$this->componentAreaService->loadAttribute($translationModel, $attribute);
});
}
$this->siteService->load($model, $attributesByValueFrom->collapse()->filter(function (Attribute $attribute) use ($model) {
return $attribute->getsValueFromReference() == 'site_id';
}));
$this->relatedModelService->load($model, $attributesByValueFrom->get(Attribute::ValueFromModelHasManyRelation));
return $attributesByValueFrom;
}
/**
* Loops over a collection of attributes, retrieving the translation
* models of the given model, that match the attributes language.
* These translations, along with the attribute are passed to a callback function when it's called.
*
* @param AbstractTranslatableModel $model
* @param Collection $attributes
* @param \Closure $do
*/
protected function executeCallbackForModelsTranslationsMatchingAttributeLanguage(AbstractTranslatableModel $model, Collection $attributes, \Closure $do)
{
if($this->forTranslationModelName && $attributes->count() > 0) {
if($attributes) {
$attributes->each(function (Attribute $attribute) use ($model, $do) {
if (($language = $attribute->getAssociatedLanguage()) && $model) {
$translationModel = $this->translationService->getTranslationModelForModelByLanguage($model, $language);
if ($translationModel) $do->call($this, $translationModel, $attribute);
}
});
}
}
}
/**
* @param Model $model
* @param Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function outOfDateCheck(Model $model, Request $request) {
// Only allow ajax requests
if(!$request->ajax()) return response()->json('Only Ajax requests are allowed');
// If the model was deleted before, return an UNPROCESSABLE_ENTITY response
if(!$model->exists) abort(Response::HTTP_UNPROCESSABLE_ENTITY, "Model doesn't exist");
// Retrieve the fields that are out of date.
$outOfDateModelFields = [];
if(is_a($this->modelService, OutOfDateInterface::class)) $outOfDateModelFields = array_merge($outOfDateModelFields, $this->modelService->outOfDate($model));
if(is_a($this->translationService, OutOfDateInterface::class)) $outOfDateModelFields = array_merge($outOfDateModelFields, $this->translationService->outOfDate($model));
if(is_a($this->treeService, OutOfDateInterface::class)) $outOfDateModelFields = array_merge($outOfDateModelFields, $this->treeService->outOfDate($model));
if(is_a($this->documentService, OutOfDateInterface::class)) $outOfDateModelFields = array_merge($outOfDateModelFields, $this->documentService->outOfDate($model));
if(is_a($this->relatedModelService, OutOfDateInterface::class)) $outOfDateModelFields = array_merge($outOfDateModelFields, $this->relatedModelService->outOfDate($model));
if(is_a($this->componentAreaService, OutOfDateInterface::class)) $outOfDateModelFields = array_merge($outOfDateModelFields, $this->componentAreaService->outOfDate($model));
// If there are fields out of date, return a CONFLICT response containing the fields and values that are out of date.
if(count($outOfDateModelFields) > 0) return response()->json($outOfDateModelFields, Response::HTTP_CONFLICT);
// If the model was not out of date, return a success response
return response()->json(null, Response::HTTP_NO_CONTENT);
}
}