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/centrum8a.komma.pro/app/KommaApp/Routes/RouteService.php
<?php

namespace App\KommaApp\Routes;


use App\KommaApp\Kms\Core\AbstractTranslationModel;
use App\KommaApp\Kms\Core\HasRoutesInterface;
use App\KommaApp\Kms\Core\Kms;
use App\KommaApp\Kms\Core\AbstractTranslatableModel;
use App\KommaApp\Kms\Core\KmsInterface;
use App\KommaApp\Kms\Core\NestedSets\Nodes\EloquentNodeInterface;
use App\KommaApp\Languages\Models\Language;
use App\KommaApp\Routes\Models\RedirectRoute;
use App\KommaApp\Routes\Models\Route;
use App\KommaApp\Sites\Models\Site;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder;

/**
 * Class RouteService
 * @package App\KommaApp\Routes
 */
class RouteService extends AbstractRouteService
{
    /** @var Kms $kms */
    private $kms;

    /**
     * The date format to use when working with dates.
     * Notice that this format will be slugified when used in urls
     *
     * @see http://php.net/manual/en/function.date.php
     * @var string
     */
    public static $dateFormat = 'm/d/Y H:i:s';

    /**
     * For which class this model works for
     *
     * @var string $forModelName
     */
    protected $forModelName;

    public function __construct(Kms $kms)
    {
        $this->kms = \App::make(KmsInterface::class);

        $this->setRouteClassName(Route::class);
        $this->setRedirectRouteClassName(RedirectRoute::class);
    }

    /**
     * Creates or updates routes for the specified AbstractTranslatableModel if needed.
     * Important to know is that you need to run this method after the translation has been saved.
     *
     * @param Model $model
     * @param int $redirectCode one of the HTTPStatusCode_308 constants from RedirectRouteModelInterface. Defaults to RedirectRouteModelInterface::HTTPStatusCode_308
     * @return RouteService true on success. An array when one or more translation routes (RouteModelInterface) could not be saved because the RouteModelInterface alias already exists
     */
    public function createOrUpdateRoutesForModelIfChanged(Model $model, int $redirectCode = RedirectRouteModelInterface::HTTPStatusCode_308): AbstractRouteService
    {
        // Only update when it is a translatableModel
        if ( ! is_a($model, AbstractTranslatableModel::class)) return $this;

        $translations = $model->translations()->get();

        //We need to make sure that each translation has a language and routes method. If not trow an exception
        $this->verifyThatEachHasTranslationIsAnAbstractTranslationModel($translations);

        $translations->each(function ($translation) use ($redirectCode, $model, &$existingRouteFails) {

            /** @var AbstractTranslationModel|HasRoutesInterface $translation */
            /** @var Language $language */
            if ( ! is_a($translation, HasRoutesInterface::class)) return;

            // Don't make a route for an empty translation, because they shouldn't even exists
            if ($translation->name === '') return;

            $language = $translation->language()->get()->first();
            $newRouteAliasStringForCurrentTranslation = $this->createRouteAliasString($language, $model, $translation);

            //Get route and redirect route
            $translationRoute = $translation->route()->first();

            //Skip if the translationRoute is the home page so alias '/'
            if ($translationRoute && $translationRoute->alias === '/') {
                return;
            }

            $routeWithNewAlias = $this->getRouteClassName()::where('alias', '=', $newRouteAliasStringForCurrentTranslation)
                ->where('site_id', $model->site_id)
                ->first();
            $translationRedirectRouteWithNewAlias = $translation->redirectRoutes()
                ->where('alias', '=', $newRouteAliasStringForCurrentTranslation)
                ->where('site_id', $model->site_id)
                ->first();

            //skip this iteration. Route did not change
            if ($translationRoute && $this->removeNumberSuffixIfPresent($translationRoute->alias) == $this->removeNumberSuffixIfPresent($newRouteAliasStringForCurrentTranslation)) return;

            // Handle different the of situations between both route types.
            // When the new route is already in the redirect table we need to swap the detail of the current regular route
            if ($translationRoute && $translationRedirectRouteWithNewAlias) {

                $translationRedirectRouteWithNewAlias->alias = $translationRoute->alias;
                $translationRoute->alias = $newRouteAliasStringForCurrentTranslation;

                $translationRedirectRouteWithNewAlias->save();
                $translationRoute->save();
            }// When the new route does exist as a regular route we create a new redirect route containing the regular route's details. We then give the already existing regular route the new details
            elseif ($translationRoute && ! $translationRedirectRouteWithNewAlias) {

                $redirectRoute = $this->createRedirectRouteFromRouteAndRedirectCode($translationRoute,
                    $this->getRedirectRouteClassName()::Http11PermanentCachedByDefault);
                $redirectRoute->site_id = $model->site_id;

                $translationRoute->alias = $newRouteAliasStringForCurrentTranslation;

                //Check if there already is an redirect route with the same alias
                $possibleExistingRouteRedirectRoute = $this->getRedirectRouteClassName()::where('alias', '=', $translationRoute->alias)
                    ->where('site_id', $model->site_id)
                    ->first();

                if ($possibleExistingRouteRedirectRoute) {
                    \Log::warning('Deleted redirect route with alias "' . $newRouteAliasStringForCurrentTranslation . '". Because a new one with the same alias needed to be created.');
                    $possibleExistingRouteRedirectRoute->delete();
                }

                $redirectRoute->save();
                $translationRoute->save();
            }//Else if the new route exists as redirect route but not as regular route, we only log this result. The redirect route works but could be improved by storing him as regular route
            elseif ( ! $translationRoute && $translationRedirectRouteWithNewAlias) {
//                dd('2');
                $route = $this->createRoute($newRouteAliasStringForCurrentTranslation, $model, $translation);

                $translationRedirectRouteWithNewAlias->delete();
                $route->save();
                $translationRedirectRouteWithNewAlias = null;
            }//Else if when the route does not exist on both route types we create it as regular route
            elseif ( ! $translationRoute && ! $routeWithNewAlias && ! $translationRedirectRouteWithNewAlias) {
//                dd('3');
                $route = $this->createRoute($newRouteAliasStringForCurrentTranslation, $model, $translation);

                $route->save();
            }
        });

        return $this;
    }

    /**
     * Removes the number suffix from a string.
     * @param string $string The subject
     * @param string $delimiter Character that separates the number from the rest.
     * @return string
     */
    private function removeNumberSuffixIfPresent(string $string, string $delimiter = '-')
    {
        $shrapnel = explode($delimiter, $string);
        $potentialNumber = last($shrapnel);
        if ( ! is_numeric($potentialNumber)) {
            return $string;
        }
        array_pop($shrapnel);

        return implode($delimiter, $shrapnel);
    }

    /**
     * Check if the given route alias only is defined once in either RouteModelInterface and RedirectRouteModelInterface instances.
     * If not throw a RunTimeException
     *
     * CAN THIS BE DELETED?
     *
     * @param string $alias
     * @return void
     * @throws \RuntimeException
     */
    private function checkRouteTableIntegrityForAlias(string $alias)
    {
        $redirectRouteClassName = $this->getRedirectRouteClassName();
        $routeClassName = $this->getRouteClassName();

        $redirectRoutesWithNewAlias = $redirectRouteClassName::where('alias', '=', $alias);
        $routesWithNewAlias = $routeClassName::where('alias', '=', $alias);
        $routesWithNewAliasCount = $routesWithNewAlias->count(['id']);
        $redirectRoutesWithNewAliasCount = $redirectRoutesWithNewAlias->count(['id']);

        if ($routesWithNewAliasCount > 1) {
            throw new \RuntimeException('Check your "' . (new $routeClassName)->getTable() . '" table. It contains multiple routes with alias "' . $alias . '" which is not allowed. Did not update routes or redirect routes');
        }
        if ($redirectRoutesWithNewAliasCount > 1) {
            throw new \RuntimeException('Check your "' . (new $redirectRouteClassName)->getTable() . '" table. It contains multiple routes with alias "' . $alias . '" which is not allowed. Did not update routes or redirect routes');
        }
        if ($redirectRoutesWithNewAliasCount + $routesWithNewAliasCount > 1) {
            throw new \RuntimeException('Check your "' . (new $redirectRouteClassName)->getTable() . '" and "' . (new $routeClassName)->getTable() . '" tables. They both contain a "' . $alias . '" which is not allowed. Only one in both tables combined is allowed');
        }
    }

    /**
     * Creates a route model (does not save it to the database)
     *
     * If you give it the AbstractTranslatableModel model it will update the route attribute to the appropiate value.
     * If you give it the AbstractTranslationModel model it will update the routable_id and routable_type attributes so
     * that the AbstractTranslationModel instance is associated with it
     *
     * @param string $alias The alias for the route.
     * @param AbstractTranslatableModel|null $model
     * @param AbstractTranslationModel|null $translationModel
     * @return RouteModelInterface
     */
    private function createRoute(string $alias, AbstractTranslatableModel $model, AbstractTranslationModel $translationModel): RouteModelInterface
    {
        $currentSite = $this->kms->getSite();

        $routeClass = $this->getRouteClassName();
        $routeModel = new $routeClass;
        $routeModel->route = ($model) ? $this->generateRealRouteForModel($model) : '';
        $routeModel->alias = $alias;
        $routeModel->language_id = $translationModel->language()->get(['id'])->first()->id;
        $routeModel->site()->associate($currentSite);
        $routeModel->routeable()->associate($translationModel);

        return $routeModel;
    }

    /**
     * Generates a real route for a model that should be compatible with the routes listed
     * with php artisan route:list.
     * Example: when you pass a page model, it will return: pages/4 if the page id is 4.
     *
     * @param Model $model
     * @return string
     */
    private function generateRealRouteForModel(Model $model)
    {
        $shortName = $this->kms->getShortNameFromClass($model);

        $shortName = strtolower($shortName);
        $pluralShortName = str_plural($shortName);

        return $pluralShortName . '/' . $model->id;
    }

    /**
     * Creates an instance of a RedirectRouteModelInterface based on an instance of RouteModelInterface.
     * And then returns the RedirectRouteModelInterface
     *
     * @param RouteModelInterface $route
     * @param int $redirectCode
     * @return RedirectRouteModelInterface;
     */
    private function createRedirectRouteFromRouteAndRedirectCode(RouteModelInterface $route, int $redirectCode): RedirectRouteModelInterface
    {
        $attributes = $route->toArray();

        $redirectRouteClassName = $this->getRedirectRouteClassName();
        $redirectRoute = new $redirectRouteClassName();

        $redirectRoute->fill($attributes);

        if ( ! $this->getRedirectRouteClassName()::isValidRedirectCode($redirectCode)) {
            throw new \InvalidArgumentException("The redirect code '" . $redirectCode . "' was not a valid one. It must be one of the constants defined in the RedirectRouteModelInterface.");
        }
        $redirectRoute->redirect_code = $redirectCode;

        return $redirectRoute;
    }

    /**
     * Returns a route alias string for an instance that extends the AbstractTranslationModel class
     *
     * @param Language $language
     * @param AbstractTranslatableModel $model
     * @param AbstractTranslationModel $translation
     * @return string
     * @see AbstractTranslationModel
     */
    private function createRouteAliasString(Language $language, AbstractTranslatableModel $model, AbstractTranslationModel $translation): string
    {
        $availableLanguages = $this->kms->getSiteLanguages();

        if (count($availableLanguages) == 0) {
            throw new \RuntimeException("Site has no language");
        }

        // Check if model has EloquentNodeInterface
        if (is_a($model, EloquentNodeInterface::class)) {

            // Handles the route creating for eloquent nodes
            $potentialRoute = $this->createPotentialRouteForEloquentNode($language, $model, $translation,
                $availableLanguages);

        }// Shouldn't be used, because only pages are using this function
        else {
            if (count($availableLanguages) == 1) {
                $potentialRoute = str_slug($translation->name);
            }else {
                if ($translation->slug == 'home') {
                    $potentialRoute = $language->iso_2;
                }else {
                    $potentialRoute = $language->iso_2 . '/' . $translation->slug;
                }
            }
        }


//        $alreadyExists = $this->getRouteClassName()::where([['alias', '=', $potentialRoute]])->get()->first();
        $alreadyExists = $this->getRouteClassName()::where('alias', '=', $potentialRoute)
            ->where('site_id', $model->site_id)
            ->first();

        if ($alreadyExists) {
            $suffixedRoutes = $this->getRouteClassName()::where('alias', 'like', $potentialRoute . '-%')
                ->where('routeable_id', '!=', $translation->id)
                ->where('site_id', $model->site_id)
                ->get(['alias']);
            if ($suffixedRoutes->count() > 0) {
                $highestNumber = null;
                $suffixedRoutes->each(function ($suffixedRoute) use (&$highestNumber) {
                    $kaboomedAlias = explode('-', $suffixedRoute->alias);
                    $number = end($kaboomedAlias);

                    if (is_numeric($number)) {
                        $currentNumber = intval($number);
                        if ($highestNumber < $currentNumber) {
                            $highestNumber = $currentNumber;
                        }
                    }
                });

//                dd('is suffixed. '.$highestNumber);
                $potentialRoute .= ($highestNumber) ? '-' . ($highestNumber + 1) : '-1';
            }else {
//                dd('is not suffixed');
                $potentialRoute .= '-1';
            }
        }

//        dd('halt');
        return $potentialRoute;
    }

    /**
     * Returns a possible route alias string for an instance that extends the AbstractTranslationModel and has an EloquentNodeInterface
     *
     * @param Language $language
     * @param AbstractTranslatableModel $model
     * @param AbstractTranslationModel $translation
     * @param array $availableLanguages
     * @return string
     */
    private function createPotentialRouteForEloquentNode(Language $language, AbstractTranslatableModel $model, AbstractTranslationModel $translation, $availableLanguages): string
    {
        $parent = $model->getParent();

        // Check if model is direct parent of root
        if ($parent->lft == '1') {
            //Then do as normal
            if (count($availableLanguages) == 1) {
                return str_slug($translation->name);
            }else {
                if ($translation->slug == 'home') {
                    return $potentialRoute = $language->iso_2;
                }else {
                    return $language->iso_2 . '/' . $translation->slug;
                }
            }
        }else { // Then model is a sub model so generate route by alias

            // Load parent translation
            $parentTranslation = $parent->translations->where('language_id', '=', $language->id)->first();
            $parentTranslationRoute = $parentTranslation->route;

            // Sub model route is parent route with slugify name
            return $parentTranslationRoute->alias . '/' . str_slug($translation->name);
        }
    }


    /**
     * Returns the route whom alias is /
     *
     * CAN THIS BE DELETED?
     *
     * @throws \RuntimeException when the route does not exist
     * @return Route $route The Root route;
     */
    public function getRootRoute(): RouteModelInterface
    {
        /** @var Builder $route */
        $route = (new $this->getRouteClassName())->where('alias', '/');
        if ( ! $route) {
            throw new \RuntimeException("The root route does not exist. This is a route with alias '/'");
        }

        return $route->get()->first();
    }

    /**
     * Check that all translations in the collection are AbstractTranslationModel instances.
     * This means that they have a routes and language method at least.
     *
     * @see HasLanguageAndRoutesInterface
     * @see HasTranslationsInterface
     * @param Collection $translations A Collection with AbstractTranslationModel implementations
     * @throws \RuntimeException
     * @return void
     */
    private function verifyThatEachHasTranslationIsAnAbstractTranslationModel(Collection $translations)
    {
        $translations->each(function ($translation) {
            if ( ! is_a($translation, AbstractTranslationModel::class)) {
                throw new \RuntimeException("One of the translations wasn't an AbstractTranslationModel implementation while it must be one.");
            }
        });
    }

    /**
     * Returns the redirect route (if any) with the specified alias or
     * if that not exists it returns the regular route or if that does not exists null
     *
     * @param string $alias
     * @param Site $site
     * @param string $routeInterfaceNameToReturn if null it will try to find a RouteRedirectModelInterface or a RouteModelInterface if the RouteRedirectModelInterface does not exist.
     * You can search for a specific type by passing either RouteRedirectModelInterface::class or RouteModelInterface::class
     * @param bool $mustBeActive The route must link to an active routeable (true) or we don't care (false)
     * @return RedirectRouteModelInterface|RouteModelInterface|null
     */
    public function getRouteByAlias(string $alias, Site $site, bool $mustBeActive = true, string $routeInterfaceNameToReturn = null): ?Model
    {
        $routes = null;

        if($routeInterfaceNameToReturn == null) {
            /** @var RouteModelInterface $route */
            $redirectRoutes = $this->getRedirectRouteClassName()::where('alias', $alias)
                ->orderBy('updated_at', 'desc')
                ->where('site_id', $site->id)
                ->get();
            if ($redirectRoutes->count() == 0) {
                $regularRoutes = $this->getRouteClassName()::where('alias', $alias)
                    ->orderBy('updated_at', 'desc')
                    ->where('site_id', $site->id)
                    ->get();
                if ($regularRoutes->count() > 0) $routes = $regularRoutes;
            } else {
                $routes = $redirectRoutes;
            }
        }
        elseif($routeInterfaceNameToReturn == RedirectRouteModelInterface::class)
        {
            $redirectRoutes = $this->getRedirectRouteClassName()::where('alias', $alias)
                ->orderBy('updated_at', 'desc')
                ->where('site_id', $site->id)
                ->get();
            if ($redirectRoutes->count() == 0) return null;
            $routes = $redirectRoutes;
        }
        elseif($routeInterfaceNameToReturn == RouteModelInterface::class)
        {
            $regularRoutes = $this->getRedirectRouteClassName()
                ->orderBy('updated_at', 'desc')
                ->where('site_id', $site->id)
                ->get();
            if ($regularRoutes->count() == 0) return null;
            $routes = $regularRoutes;
        } else {
            throw new \InvalidArgumentException("The routeType argument must be either null, RouteRedirectModelInterface::class or RouteModelInterface::class");
        }

        if($routes)
        {
            if($mustBeActive == false) return $routes->first();

            //check if one of the routeables is active and return the first
            foreach($routes as $route){

                /** @var RouteModelInterface $route */
                $routeable = $route->routeable()->first()->translatable;
                if(!is_a($routeable, AbstractTranslatableModel::class)) throw new \RuntimeException('The model returned with the "routable" method from a RedirectRouteModelInterface implementation "'.get_class($route).'" did not return the expected model that extends the AbstractTranslatableModel.');

                /** @var AbstractTranslatableModel $routeable */
                if(isset($routeable->active) && $routeable->active && $routeable->id != false){
                    return $route;
                } //Skip this iteration
            }
        }

        return null;
    }

    /**
     * Deletes all routes for a model. If it is a AbstractTranslatableModel, for example a page, it will also delete the routes for the translations if you set the second property to true.
     * If it is a AbstractTranslationModel, for example a page translations it will look at its translatable first and then delete al routes for the translatable and translations too if you set the second property to true.
     *
     * @param Model|AbstractTranslationModel|AbstractTranslationModel $theModel
     * @return void
     */
    public function deleteRoutesForModel(Model $theModel): void
    {
        if ( ! is_a($theModel, AbstractTranslatableModel::class) && ! is_a($theModel,
                AbstractTranslationModel::class)) {
            throw new \InvalidArgumentException("The model must be either an AbstractTranslatableModel or an AbstractTranslationModel. Not '" . get_class($theModel) . "'");
        }

        /** @var AbstractTranslatableModel $model */
        $model = null;

        /** @var AbstractTranslatableModel $translations */
        $translations = null;

        if (is_a($theModel, AbstractTranslationModel::class)) {
            $model = $theModel->translatable();
            if ($model) {
                $translations = ($model->translations());
            }
        }else {

            $model = $theModel;
            $translations = $model->translations();
        }

        $translations->each(function ($translation) use ($model) {
            if ( ! is_a($translation, HasRoutesInterface::class)) {
                return;
            }

            /** @var AbstractTranslationModel $translation */

            $routableId = $translation->id;
            $routableType = get_class($translation);

            $this->getRedirectRouteClassName()::where([
                'routeable_type' => $routableType,
                'routeable_id'   => $routableId,
                'site_id' => $model->site_id
            ])->delete();
            $this->getRouteClassName()::where([
                'routeable_type' => $routableType,
                'routeable_id'   => $routableId,
                'site_id' => $model->site_id
            ])->delete();
        });

        return;
    }

    /**
     * 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)
    {
        $siteSlug = $this->kms->getSiteSlug();
//        dd($this->kms->getSiteSlug());

        //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 ($siteSlug) {
            $routeParameters['siteSlug'] = $siteSlug;
        }
        $modelName = $this->kms->getShortNameFromClass($this->forModelName, true);
        if ($modelId) {
            $routeParameters[$modelName] = $modelId;
        } //example key is 'page' value = 89

        return [
            'route' => route($route, $routeParameters),
            'method' => $method
        ];
    }

    /**
     * For which model class the service works for
     *
     * @param string $forModelName
     */
    public function setForModelName(string $forModelName)
    {
        $this->forModelName = $forModelName;
    }

    /**
     * Returns a query builder for getting all wildcard routes. These are routes without a / in the name
     *
     * @return Builder|string
     */
    public function wildCardRoutes()
    {
        return $this->getRouteClassName()::where('alias', 'not like', '%/%');
    }

    /**
     * Does some housekeeping by removing old routes.
     * Should be triggered by cron jobs via a houseKeeperService
     */
    public function doHouseKeeping()
    {
        // TODO: Implement doHouseKeeping() method.
    }
}