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/beerten.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;
    }
}