File: D:/HostingSpaces/SBogers10/stielman.komma.nl/vendor/komma/kms/src/Core/ModelService.php
<?php declare(strict_types=1);
namespace Komma\KMS\Core;
use Komma\KMS\Core\Sections\AttributeKey;
use Komma\KMS\Helpers\KommaHelpers;
use Komma\KMS\Documents\Kms\DocumentableInterface;
use Komma\KMS\Core\Attributes\Attribute;
use Komma\KMS\Core\Attributes\Models\SelectOptionInterface;
use Komma\KMS\Core\Attributes\Models\Traits\HasThumbnailInterface;
use Komma\KMS\Core\Entities\DisplayNameInterface;
use Komma\KMS\Core\Entities\DisplayNameTrait;
use Komma\KMS\Core\Sections\SidebarListItem;
use Komma\KMS\Core\Tree\NestedSets\Nodes\TreeModelInterface;
use Komma\KMS\Core\Tree\TreeServiceInterface;
use Komma\KMS\Sites\HasSiteInterface;
use Komma\KMS\Sites\HasSitesInterface;
use Komma\KMS\Sites\Kms\SiteService;
use Komma\KMS\Sites\SiteServiceInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection as BaseCollection;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
/**
* Class ModelService
*
* Does know how to work with eloquent models.
*
* @package App\Kms\Core
*/
class ModelService extends AbstractModelHandler implements ModelServiceInterface, OutOfDateInterface
{
use CanSaveSlugsTrait;
/** @var string */
protected $modelClassName = null;
/** @var SiteService */
protected $siteService;
/** @var bool */
private $orderByDisplayName;
/** @var bool */
private $orderReverse;
/** @var TreeServiceInterface $treeService */
protected $treeService;
/**
* ModelService constructor.
*/
public function __construct()
{
$this->siteService = app(SiteServiceInterface::class);
$this->treeService = app(TreeServiceInterface::class);
$this->orderByDisplayName = false;
$this->orderReverse = false;
}
/**
* Sets the for forModelInstance variable to the model that is in the route, if it matches the singular form
* of the slug string variable. If it does not match, forModelInstance will remain null. Take this into account
* in other methods.
*
* @return Model
*/
public function getForModelFromRoute($slug): ? Model
{
$currentRoute = Route::current();
if(!isset($currentRoute)) return null;
$toResolve = $currentRoute->parameter(Str::singular($slug));
if($toResolve && is_numeric($toResolve)) {
if(is_callable([$this->modelClassName, 'find'])) {
return $this->modelClassName::find($toResolve);
}
}
return null;
}
/**
* 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);
$keys = array_keys($model->getAttributes());
$attributes->each(function(Attribute $attribute) use(&$model, &$keys) {
$valueFrom = $attribute->getsValueFrom();
$valueReference = $attribute->getsValueFromReference();
$value = $attribute->getValue();
if($valueFrom !== Attribute::ValueFromModel) return $model;
if (!array_key_exists($valueReference, $keys) === false)
throw new \RuntimeException("The value reference field name ('" . $valueReference . "') does not exist on the model with class name: " . (get_class($model)) . ". Please check the attribute configuration in the section. All available field names: " . implode(', ', $keys));
$model->$valueReference = $value;
});
$model = $this->treeService->save($model, $attributes);
$model->save();
$model = $this->saveSlugForModel($model);
return $model;
}
/**
* Gets the values of an Eloquent model and passes them to a collection of attributes
*
* @param Model $model
* @param Collection $attributes
* @return mixed
*/
public function load(Model $model, Collection $attributes = null): Collection
{
if($attributes == null) return new Collection();
$attributes = $attributes->map(function(Attribute $attribute) use(&$model) {
$valueReference = $attribute->getsValueFromReference();
$valueFrom = $attribute->getsValueFrom();
if ($valueFrom !== Attribute::ValueFromModel) return $attribute;
$attribute->setValue((string)$model->$valueReference);
return $attribute;
});
return $this->treeService->load($model, $attributes);
}
/**
* This method will create a new model instance.
*
* @return Model
*/
public function newModel(): Model
{
//Create a new TranslatableModelInterface
$model = new $this->modelClassName;
//If Model implements HasSiteInterface, append the site to new model
if(is_a($this->modelClassName, HasSiteInterface::class, true)) {
$model->site_id = $this->siteService->getCurrentSite()->id;
}
//Set the thumbnail
/** @var HasThumbnailInterface|Model $model */
if(!is_a($model, HasThumbnailInterface::class)) throw new \RuntimeException('The class "'.get_class($model).'" is expected to implement the "'.HasThumbnailInterface::class.'" but did not');
$model = $model->generateThumbnail();
return $model;
}
/**
* Gets the model query
*
* @return \Illuminate\Database\Query\Builder
*/
public function models()
{
return $this->modelClassName::query();
}
/**
* This method will build an model tree.
* Used by the Controllers get and set-structure method
*
* @return Model|TreeModelInterface
*/
public function getRootModelForTree():?Model
{
if(!is_a(new $this->modelClassName, TreeModelInterface::class) && is_a(new $this->modelClassName, AbstractTranslatableModel::class))
throw new \RuntimeException('The model must implement '.TreeModelInterface::class.' but did not implement it.');
if(!is_a($this->modelClassName, DisplayNameInterface::class, true)) throw new \RuntimeException('Could not get the display name of a model instance. Please let the class "'.$this->modelClassName.'" use the '.DisplayNameTrait::class.' or implement the method yourself.');
//Create an new eloquent query
$modelQuery = $this->modelClassName;
//Take the current site into account. But only when the model implements the HasSiteInterface
if(is_a($this->modelClassName, HasSiteInterface::class, true)) {
$site = $this->siteService->getCurrentSite();
if ($site->exists) {
//Only load models that belong to the current site
$modelQuery = $modelQuery::whereHas('site', function ($query) use ($site) {
$query->where('site_id', '=', $site->id);
});
} else {
//Models that don't have a site automatically belong to the default site. So load them
$modelQuery = $modelQuery::doesntHave('site');
}
//Only get the root model
$modelQuery->where('lft', '=', 1);
}
else {
$modelQuery = $modelQuery::where('lft', '=', 1);
}
//Eager load some commonly used relations when possible
if(is_a(new $this->modelClassName, AbstractTranslatableModel::class)) {
$modelQuery = $modelQuery->with('translation', 'translations');
}
elseif(is_a(new $this->modelClassName, DocumentableInterface::class)) $modelQuery = $modelQuery->with('images');
$rootModel = $modelQuery->first();
if(is_a(HasThumbnailInterface::class, $this->modelClassName)) {
/** @var HasThumbnailInterface|Model|TreeModelInterface $rootModel */
$rootModel->generateThumbnail();
}
return $rootModel;
}
/**
* Converts TreeModelInterface instance and their children to Select option models
*
* @param TreeModelInterface|BaseCollection $parents
* @param int $indents
* @return BaseCollection
*/
private function convertAllModelsToSelectOptionModels($parents, $indents = 0): BaseCollection
{
$selectOptions = collect();
if(!is_a($parents, BaseCollection::class)) {
$parents = collect([$parents]);
}
$parents->each(function(TreeModelInterface $parent) use(&$selectOptions, $indents) {
// the $parents array does not allow loading translations, so we get the children with a where so we can preload the translations
// if we don't do this, down the line the translations are fetched in a foreach loop, what we want to prevent
if(is_a($parent, AbstractTranslatableModel::class) && $indents == 0) {
$childrenArray = $parent::where('lft', '>', $parent->lft)->where('rgt', '<', $parent->rgt)->with('translations')->get();
} else {
$childrenArray = $parent->findChildren();
}
$selectOptions->push($this->convertTreeModelToSelectOption($parent, $indents));
foreach($childrenArray as $child)
{
$this->convertAllModelsToSelectOptionModels(collect([$child]), $indents+1)->each(function(SelectOptionInterface $childChild) use ($selectOptions) {
$selectOptions->push($childChild);
});
}
});
return $selectOptions;
}
/**
* Convert a TreeModel to a SelectOption
* @param TreeModelInterface $treeModel
* @param int $indents
* @return SelectOptionInterface
*/
private function convertTreeModelToSelectOption(TreeModelInterface $treeModel, $indents = 0): SelectOptionInterface
{
/** @var TreeModelInterface $child */
if(!method_exists($treeModel, 'getSidebarName')) throw new \RuntimeException('Could not get the name of an instance using a call to a getDisplayName method. Please let the class "'.get_class($treeModel).'" use the '.DisplayNameTrait::class.' or implement the method yourself.');
$displayName = ($treeModel->lft > 1) ? $treeModel->getSidebarName() : '/';
$indentString = str_repeat('-', $indents);
/** @var SelectOptionInterface $selectOption */
$selectOption = app(SelectOptionInterface::class);
$selectOption
->setValue($treeModel->id)
->setContent($displayName ?? '')
->setHtmlContent($indentString.$displayName);
return $selectOption;
}
/**
* Uses the siteService and the Tree service to create a root model if the model uses a tree algorithm
*/
public function createRootTreeModelIfNeeded()
{
// Only handle treeModelInterface instances. Ignore the rest
if (!is_a($this->modelClassName, TreeModelInterface::class, true)) {
$this->debug('Skipping creating the root model for the model class:"' . $this->modelClassName . '". Because the model class did not have a "' . TreeModelInterface::class . '" implemented.');
return null;
}
if(is_a($this->modelClassName, HasSitesInterface::class, true)) {
throw new \BadFunctionCallException(self::class.': Tree logic should not be used when the model is for multiple sites.');
}
if(is_a($this->modelClassName, HasSiteInterface::class, true)) {
$currentSite = $this->siteService->getCurrentSite();
//Also consider the root model as non existent when the site_id of all existing models is not the same as the current site_id.
$constrainingClosure = function ($query) use($currentSite) {
return $query->where('site_id', '=', $currentSite->id);
};
//Extra data to set on the root model when it needed to be created
$extraModelDataForNewRoot = ['site_id' => $currentSite->id];
$this->treeService->makeRootModelIfNeeded($this->modelClassName, $extraModelDataForNewRoot, $constrainingClosure);
return;
}
$this->treeService->makeRootModelIfNeeded($this->modelClassName);
}
public function getOptionsForSelect(bool $allowNullableSelectOption = false)
{
$selectOptions = collect();
if($allowNullableSelectOption){
$selectOption = (App::make(SelectOptionInterface::class))
->setContent(__('KMS::global.none'))
->setHtmlContent(__('KMS::global.none'))
->setValue(null);
$selectOptions->push($selectOption);
}
/** @var $sidebarListItems SidebarListItem[] */
$sidebarListItems = $this->getModelsForSideBar();
$models = $this->modelClassName::all();
foreach ($sidebarListItems as $sidebarListItem) {
$model = $models->where('id', $sidebarListItem->getId())->first();
if ( ! $model) {
continue;
}
/** @var SelectOptionInterface $selectOption */
$selectOption = (App::make(SelectOptionInterface::class))
->setContent($sidebarListItem->getName())
->setHtmlContent($sidebarListItem->getName())
->setValue($model->id);
$selectOptions->push($selectOption);
}
return $selectOptions;
}
public function getOptionsForSelectAsTree(): Collection
{
/** @var TreeModelInterface $tree */
$model = $this->getRootModelForTree();
if(!$model) return collect();
$rootSelectOption = $this->convertTreeModelToSelectOption($model);
$modelWithAllChildren = $model->findChildren();
$selectOptions = $this->convertAllModelsToSelectOptionModels(collect($modelWithAllChildren));
$selectOptions->prepend($rootSelectOption);
return $selectOptions;
}
/**
* Returns all models for the sidebar menu in the backend
*
* @return array
*/
public function getModelsForSideBar():array
{
if(!is_a($this->modelClassName, DisplayNameInterface::class, true)) throw new \RuntimeException('Please let class "'.$this->modelClassName.'" implement '.DisplayNameInterface::class.'. it is needed to build a sidebar.');
if(!is_a($this->modelClassName, HasThumbnailInterface::class, true)) throw new \RuntimeException('Please let class "'.$this->modelClassName.'" implement '.HasThumbnailInterface::class.'. It is needed to build a sidebar');
if(isset($this->orderBy)){
if($this->orderReverse) $models = $this->modelClassName::orderBy($this->orderBy, 'desc');
else $models = $this->modelClassName::orderBy($this->orderBy);
}
else $models = $this->modelClassName::orderBy('id');
//Only load the models from the current site. Or load all when we currently have the default site
$site = $this->siteService->getCurrentSite();
if(new $this->modelClassName instanceof HasSitesInterface && $site->exists) {
$models->whereHas('Sites', function ($query) use ($site) {
$query->where('site_id', '=', $site->id);
});
}
elseif(is_a($this->modelClassName, HasSiteInterface::class)) {
$models = $models->where('site_id', $site->id);
}
// Get the models
$models = $models->get();
// Preload translation to prevent additional queries later on
if(is_a(new $this->modelClassName, AbstractTranslatableModel::class)){
$models = $models->load('translations', 'translation');
}
//Preload documents to prevent additional queries later on
if(is_a(new $this->modelClassName, DocumentableInterface::class)){
$models = $models->load('documents');
}
$sidebarList = [];
foreach ($models as $model) {
/** @var Model|HasThumbnailInterface|DisplayNameInterface $model */
if($model->getAttribute('lft') == 1) continue; //Skip the root model if it is one
$sidebarListItem = new SidebarListItem();
$sidebarListItem->setThumbnail($model);
$sidebarListItem->alsoSearchInAttributesOfModel($model);
$model->generateThumbnail();
//Set the values for the sidebar
$sidebarListItem->setId($model->id);
$sidebarListItem->setStatus($model->active);
$title = KommaHelpers::str_limit_full_word($model->getSidebarName(), 75);
$sidebarListItem->setName($title);
$sidebarListItem->setThumbnail($model->getThumbnail());
// Make sure there is always a unique key
$key = $title;
if(isset($sidebarList[$key])) $key = $title.$model->id;
$sidebarList[$key] = $sidebarListItem;
}
if($this->orderByDisplayName){
ksort($sidebarList);
if($this->orderReverse) $sidebarList = array_reverse($sidebarList);
}
return $sidebarList;
}
/**
* Generates a route string for saving a new or existing route. And generates the correct http verb for it.
*
* @param int|null $modelId In a route of /kms/default/pages/89 the modelId is 89. Referencing to a page for example.
* @param string $slug In a route of /kms/default/pages/89 the slug is 'pages'. default is the site name
* @return array with 2 keys. route and method.
*/
public function getSaveRoute(string $slug, int $modelId = null)
{
$site = $this->siteService->getCurrentSite();
//If we have a modelId, then we need to update (PUT) the model. Else we need to store (POST) a new model.
$route = ($modelId) ? strtolower($slug) . '.update' : strtolower($slug) . '.store';
$method = ($modelId) ? 'PUT' : 'POST';
//Set route parameters
$routeParameters = [];
if ($site->exists) {
$routeParameters['siteSlug'] = $site->slug;
}
$modelName = KommaHelpers::getShortNameFromClass($this->modelClassName, true);
if ($modelId) {
$routeParameters[$modelName] = $modelId;
} //example key is 'page' value = 89
// dd($route, $routeParameters);
return [
'route' => route($route, $routeParameters),
'method' => $method
];
}
/**
* Returns an array of form field names that are not up to date with the given model.
* Or an empty array if the model is up to date with form fields. The form fields must
* come from the outOfDateCheckFormFields method.
*
* @param Model $model
* @return array
*/
public function outOfDate(Model $model): array
{
//Create an array to keep track of the fields (attributes) which are out of date.
$outOfDateFields = [];
//Get the form fields and their values
$outOfDateCheckFormFields = $this->outOfDateCheckFormFields();
//Loop over them and check if they are out of date.
foreach($outOfDateCheckFormFields as $attributeKey => $value) {
// Create an attribute key if possible
if(!AttributeKey::couldBeAKeyString((string) $attributeKey)) continue;
$attributeKey = AttributeKey::createInstanceFromString($attributeKey);
// If the attribute is for a translation, we ignore it here. It must be processed in the TranslationModelService
if($attributeKey->getTranslationIso2() !== '') continue;
//Check if the form field value does not equal the models value. If so, the form field is out of date.
if($model && $value !== (string) $model->{(string) $attributeKey->getValuePart()}) { // Notice that when the attribute key does not exist on the model, the value will be null.
$outOfDateFields[(string) $attributeKey] = $value;
}
}
return $outOfDateFields;
}
/**
* Return key value pairs of data (usually form fields)
* that will be used for the outOfDateCheck
*
* @return array
*/
public function outOfDateCheckFormFields(): array
{
return request()->all();
}
/**
* @param Model $model
* @return Model of loaded attributes
*/
public function destroyForModel(Model $model): Model
{
return $model;
}
/**
* @return string
*/
public function getModelClassName(): string
{
return $this->modelClassName;
}
/**
* @param string $modelClassName
*/
public function setModelClassName(string $modelClassName): void
{
$this->modelClassName = $modelClassName;
}
}