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/hem-mechatronics.komma.pro/app/Komma/Shop/Catalog/Kms/CatalogService.php
<?php

namespace App\Komma\Shop\Catalog\Kms;


use App\Komma\Kms\JsonApi\JsonApiLengthAwarePaginator;
use App\Komma\Kms\JsonApi\TranslatableResource;
use App\Komma\Kms\Core\AbstractTranslatableModel;
use App\Komma\Kms\Core\AbstractTranslationModel;
use App\Komma\Kms\Core\Sections\SectionService;
use App\Komma\Kms\Core\Sections\SectionServiceInterface;
use App\Komma\Globalization\Languages\Models\Language;
use App\Komma\Shop\Categories\Kms\CategoryServiceInterface;
use App\Komma\Shop\Categories\Models\Category;
use App\Komma\Shop\Categories\Models\CategoryTranslation;
use App\Komma\Shop\Products\Product\Product;
use App\Komma\Shop\Products\Product\ProductServiceInterface;
use App\Komma\Shop\Products\Product\ProductTranslation;
use App\Komma\Shop\Products\ProductableInterface;
use App\Komma\Shop\Products\ProductComposite\ProductComposite;
use App\Komma\Shop\Products\ProductComposite\ProductCompositeServiceInterface;
use App\Komma\Shop\Products\ProductComposite\ProductCompositeTranslation;
use App\Komma\Shop\Products\ProductGroup\ProductGroup;
use App\Komma\Shop\Products\ProductGroup\ProductGroupServiceInterface;
use App\Komma\Shop\Products\ProductGroup\ProductGroupTranslation;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;
use Symfony\Component\HttpFoundation\Request;

/**
 * Builds the product catalog for the frontend. And has methods for searching trough it.
 * Notice that searching will be done via laravel scout and not via the catalog_index table.
 * That table is just being used for displaying the catalog
 *
 * Class CatalogService
 * @package App\Komma\Shop\Catalog
 */
class CatalogService extends SectionService implements CatalogServiceInterface
{
    /**
     * @var string[] Attributes that can be indexed / sorted. Used for creating the migration. Make sure you also reflect these variables in the CatalogIndexModel
     */
    private $attributes;

    /**
     * @var SectionServiceInterface[] SectionService extensions whom forModels can be searched trough. If they also are productables they will be put in they will be put in the catalog.
     * For example, The productServiceInterfaces forModel is called Product. This Product also is a productable so it will be shown in the catalog (and indexed in the catalog_index table).
     * And it is searchable because the Product model uses laravels scout Searchable trait.
     *
     * But the CategoryServiceInterface forModel is called Category and is not a productable. Therefore it won't be indexed. But because it also has the Searchable trait from laravels scout,
     * it will be searchable.
     */
    private $catalogableServicesInterfaces = [];

    /**
     * The models that can be searched trough. They are recognized because they use the Searchable trait
     *
     * @var array
     */
    public static $searchableModelClasses = [
        Product::class,
        ProductTranslation::class,
        ProductGroup::class,
        ProductGroupTranslation::class,
        ProductComposite::class,
        ProductCompositeTranslation::class,
        Category::class,
        CategoryTranslation::class
    ];

    /**
     * CatalogService constructor
     */
    public function __construct()
    {
        $this->forModelName = CatalogIndexModel::class;

        $this->catalogableServicesInterfaces[] = ProductServiceInterface::class;
        $this->catalogableServicesInterfaces[] = ProductGroupServiceInterface::class;
        $this->catalogableServicesInterfaces[] = CategoryServiceInterface::class;
        $this->catalogableServicesInterfaces[] = ProductCompositeServiceInterface::class;

        $this->attributes = [
            'title' => 'string',
            'price' => 'integer'
        ];

        parent::__construct();
    }

    /**
     * Returns a Query builder which holds a query which returns
     * all catalogIndexModels with translations, documents and categories
     *
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function itemsQueryWithRelations()
    {
        $allItemsQuery = CatalogIndexModel::with([
            'catalogable' => function (MorphTo $query) {
                $query->with([
                    'translation',
                    'documents',
                    'categories' => function ($query) {
                        $query->with('translations');
                    }
                ]);
            }
        ]);

        return $allItemsQuery;
    }

    /**
     * Generate the model index that can be searched trough, paginated, sorted etc.
     * It will use the attributes keys as attribute names on the models retrieved from all catalogableServiceInterface implementations.
     * These attribute keys will be set with their values on the CatalogIndexModel that wil be saved to the database.
     * If the catalog model does not have one of the attributes that are specified in the attributes value it will
     */
    public function createIndex()
    {
        $this->doHouseKeeping();

        \Log::debug('CatalogService indexing...');
        $this->validateCatalogIndexModelDefinition();

        foreach ($this->catalogableServicesInterfaces as $catalogableServiceInterface) {
            /** @var SectionServiceInterface $service */
            $service = app($catalogableServiceInterface);

            $modelCollection = $service->models()->get();
            if(!$modelCollection) continue;

            $modelCollection->each(function ($model) use ($service) {
                if (!is_a($model, ProductableInterface::class)) {
                    return;
                }

                $catalogIndexModel = $this->forModelName::firstOrNew([
                    'catalogable_id' => $model->id,
                    'catalogable_type' => $service->getForModelName()
                ]);

                /** @var $model Model */
                foreach ($this->attributes as $attribute => $type) {
                    if ($this->modelHasAttribute($model, $attribute)) {
                        $catalogIndexModel->{$attribute} = $model->$attribute;
                        $catalogIndexModel->catalogable_id = $model->id;
                        $catalogIndexModel->catalogable_type = $service->getForModelName();
                    }
                }

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

        \Log::debug('CatalogService indexing done!');
    }

    /**
     * Clear the catalog.
     */
    public function clearIndex()
    {
        \Log::debug('CatalogService clearing index...');
        $this->forModelName::truncate();
        \Log::debug('CatalogService cleared index!');
    }

    /**
     * Clean up old catalog index items that reference non existing models.
     */
    public function doHouseKeeping()
    {
        \Log::debug('CatalogService doing some housekeeping...');

        $debugCounter = 0;

        /** @var CatalogIndexModel[] $indexModels */
        $indexModels = $this->forModelName::get(['id', 'catalogable_type', 'catalogable_id']);
        $indexModels->each(function ($indexModel) use (&$debugCounter) {
            /** @var CatalogIndexModel $indexModel */
            //If the catalogable_type class does not exist we need to delete it.
            if (!class_exists($indexModel->catalogable_type)) {
                $indexModel->delete();
                return; //skip to the next "each" iteration
            }

//            if($debugCounter == 1) dd($indexModel->catalogable_type::where('id', '=', $indexModel->catalogable_id)->get()); //debugging helper
            //Check if there is a catalogable_type class with the id specified. If not delete the indexModel
            if ($indexModel->catalogable_type::where('id', '=', $indexModel->catalogable_id)->get()->count() == 0) {
                $indexModel->delete();
            }

            $debugCounter++;
        });

        \Log::debug('CatalogService done housekeeping!');
    }

    /**
     * Checks if all attributes defined in this service are also attributes on the CatalogIndexModel
     */
    private function validateCatalogIndexModelDefinition()
    {
        $testModel = new $this->forModelName;
        foreach ($this->attributes as $attribute => $type) {
            if (!$this->modelHasAttribute($testModel, $attribute)) {
                throw new \RuntimeException('The "' . $this->forModelName . '" class did not have the "' . $attribute . '" attribute. Make sure it exists in the model and in the schema table (' . $testModel->getTable() . ') it belongs to');
            }
        }
    }

    /**
     * Checks is an eloquent model has the specified attribute or attributes and return true if it has the attribute(s) or false if not
     *
     * @param Model $model
     * @param $attributeName
     * @return bool
     */
    private function modelHasAttribute(Model $model, $attributeName): bool
    {
        if (!is_string($attributeName) && !is_array($attributeName)) throw new \RuntimeException('The specified columnName isn\'t an array or a string');
        if (!is_array($attributeName)) $attributeName = [$attributeName];

        $hasAttribute = true;
        foreach ($attributeName as $attribute) {
            if (!\Schema::hasColumn($model->getTable(), $attribute)) $hasAttribute = false;
        }

        return $hasAttribute;
    }

    /**
     * Create the catalog table. You can use this in your migrations
     */
    public function migrate()
    {
        Schema::create((new $this->forModelName)->getTable(), function (Blueprint $table) {
            $table->increments('id');
            $table->string('catalogable_id');
            $table->string('catalogable_type');

            foreach ($this->attributes as $attribute => $type) {
                $table->{$type}($attribute);
            }

            $table->timestamps();
        });
    }

    /**
     * Remove the catalog table. You can use this in your migrations
     */
    public function rollback()
    {
        Schema::dropIfExists((new $this->forModelName)->getTable());
    }

    /**
     * Returns CatalogIndexModels that are linked to a category that is specified by id.
     * With the paginate option you can specify how much items should be visible per page/
     *
     * @param int $categoryID
     * @param int|null $paginate
     * @return \Illuminate\Database\Eloquent\Collection|LengthAwarePaginator
     */
    public function getCatalogItemsByCategoryID(int $categoryID, int $paginate = null)
    {
        $allItems = CatalogIndexModel::all();

        foreach ($allItems as $item) {
            $type = $item->catalogable_type;
            $id = $item->catalogable_id;

            if($type == null) continue;

            if($type !== Category::class) {
                $item->item = $type::where('id', $id)->with([
                    'translation',
                    'documents',
                    'categories' => function ($query) {
                        $query->with('translations');
                    }
                ])->first();
            } else {
                $item->item = $type::where('id', $id)->with([
                    'translation',
                    'documents'
                ])->first();
            }
        }

        $allItems = $allItems->filter(function ($value) use ($categoryID) {
            if(!$value->item) return false;
            return count($value->item->categories) > 0 && $value->item->categories[0]->id == $categoryID;
        });

        if (!empty($paginate)) {
            //$allItems->paginate($paginate);
            $page = 1;
            $perPage = $paginate;
            $allItems = new \Illuminate\Pagination\LengthAwarePaginator(
                $allItems->forPage($page, $perPage),
                $allItems->count(),
                $perPage,
                $page
            );
        }

        return $allItems;
    }

    /**
     * Search for term in the models from all catalogable services
     *
     * Returns 3 different things depending on how you specify parameters and call this method.
     * 1. If you want to return paginated data as JSON. Set the page parameter and return the paginator returned by this method to the browser.
     * 2. If you want to return data as Json. Not paginated. Don't set the page parameter and return the paginator returned by this method to the browser
     * 3. If you want to return a paginator instance. Set the page parameter and don't return the paginator to the browser but use it yourself.
     *
     * @param Request $request
     * @return JsonApiLengthAwarePaginator|\Illuminate\Contracts\View\View|JsonResponse|\Illuminate\Http\Resources\Json\AnonymousResourceCollection
     */
    public function search(Request $request)
    {
        //Return the parameters from the request
        $term = $request->input('term');
        $currentPage = ($request->has('page')) ? $request->input('page') : null;
        $perPage = ($request->has('amount')) ? $request->input('amount') : 10;
        $language_id = $request->input('language_id');
        if($language_id) {
            $language = Language::find($language_id);
            if($language) \App::setLanguage($language);
        }
        $paginate = ($currentPage) ? true : false;

        //Validate the request
        if (!$term) {
            return new JsonResponse(["error" => "request should have a 'term' variable but did not have one."], 400);
        }

        //Make a collection that will hold al found models
        $foundModels = collect();

        //Loop over all defined services...
        foreach ($this->catalogableServicesInterfaces as $catalogableServicesInterface) {
            /** @var SectionServiceInterface $service */
            $service = app($catalogableServicesInterface);

            /** @var SectionServiceInterface $catalogableServicesInterface */
            $forModelName = $service->getForModelName();

            //Search models of the current service
            if (method_exists($forModelName, 'search')) {
                $models = $forModelName::search($term)->get();
                $foundModels = $foundModels->merge($models);
            }

            //Search translation models of the current service
            if (is_subclass_of($forModelName, AbstractTranslatableModel::class)) {
                /** @var AbstractTranslatableModel $forModelName */
                $translationModel = get_class((new $forModelName)->translations()->getRelated());
                if (method_exists($translationModel, 'search')) {
                    $translationModels = $translationModel::search($term)->get();
                    /** @var Collection $foundModels */
                    $foundModels = $foundModels->merge($translationModels);

                    //Only use models which language id match the current language
                    $foundModels = $foundModels->filter(function(AbstractTranslationModel $abstractTranslationModel) use($language_id) {
                        return $abstractTranslationModel->language_id === $language_id;
                    });
                }
            }
        }

        //Convert all found models into TranslatableResources.
        $resources = $foundModels->map(function (Model $model) use ($foundModels) {
            $resource = null;
            if (is_a($model, AbstractTranslationModel::class)) {
                /** @var AbstractTranslationModel $model */
                $translatable = $model->translatable()->first();
                if ($translatable && $translatable->active == 1) {
                    $resource = new TranslatableResource($translatable);
                }
            } else if (is_a($model, AbstractTranslatableModel::class) && $model->active == 1) {
                /** @var AbstractTranslatableModel $model */
                $resource = new TranslatableResource($model);
            }

            return $resource;
        })->filter(function ($value) {
            return ($value !== null);
        });

        //Remove doubles
        $uniqueResources = collect();
        $resources->each(function (TranslatableResource $model) use ($uniqueResources) {
            $found = false;

            $uniqueResources->each(function (TranslatableResource $checkModel) use ($model, &$found) {
                if ($model->resource->id == $checkModel->resource->id && get_class($model->resource) == get_class($checkModel->resource)) {
                    $found = true;
                }
            });

            if ($found == false) {
                $uniqueResources->push($model);
            }
        });

        $paginatedResources = $this->limitTranslatableResourcesByResource($uniqueResources, $perPage, $currentPage);


        //Return a json resource containing the results.
        if ($request->ajax()) {
            if ($paginate) {
                $resourcesPaginator = new JsonApiLengthAwarePaginator(
                    $paginatedResources,
                    $paginatedResources->count(),
                    $perPage,
                    $currentPage, [
                        'path' => route('catalog.search')
                    ]
                );

                return $resourcesPaginator;
            } else {
                return TranslatableResource::collection($uniqueResources);
            }
        } else {
            abort(400, 'Only ajax requests are allowed.');
        }
//TODO: Must be used in a frontend service
//        else {
//            $pageService = new PageService();
//            $page = $pageService->getPageByCodeName('search');
//            $links = $pageService->getAllTranslatedPageRoutes();
//            $uniqueResources = $uniqueResources->sortBy(function ($resource) {
//                return $resource->resource->lft;
//            });
//
//            return view('site.pages.search',[
//                'page' => $page,
//                'term' => $term,
//                'links' => $links,
//                'results' => $uniqueResources,
//            ]);
//        }
    }

    /**
     * Paginates a collection of translatable resources. Per Resource class.
     * So if your perPage variable is 5, currentPage variable is 1 and if you have 6 products and 1 category, you'll
     * get the first 5 products AND the category in the resulting collection.
     *
     * @param Collection $translatableResources
     * @param int $perPage
     * @param int $currentPage
     * @return Collection
     */
    private function limitTranslatableResourcesByResource(Collection $translatableResources, int $perPage, int $currentPage, $flatten = true) {
        //First order them by resource class name in separate collections
        $orderedPerResourceClassName = collect();
        $translatableResources->map(function (TranslatableResource $translatableResource) use(&$orderedPerResourceClassName, $perPage, $currentPage) {
            $resourceClass = get_class($translatableResource->resource);

            if(!$orderedPerResourceClassName->has($resourceClass)) $orderedPerResourceClassName->put($resourceClass, collect());
            $orderedPerResourceClassName->get($resourceClass)->push($translatableResource);
        });

        //Then paginate each resource class collection
        $orderedPerResourceClassNameAndPaginated = $orderedPerResourceClassName->map(function(Collection $translatableCollection, $translatableClassName) use ($perPage, $currentPage) {
            $paginatedCollection = $translatableCollection->forPage($currentPage, $perPage);
            return $paginatedCollection;
        });

        if($flatten) return $orderedPerResourceClassNameAndPaginated->flatten();
        return $orderedPerResourceClassNameAndPaginated;
    }

    /**
     * Clear the searchable data for scout
     */
    public static function flushSearch()
    {
        foreach(self::$searchableModelClasses as $searchableModelClass)
        {
            \Artisan::call('scout:flush', ['model' => $searchableModelClass]);
        }
    }

    /**
     * Import searchable data for scout
     */
    public static function importSearch()
    {
        foreach(self::$searchableModelClasses as $searchableModelClass)
        {
            \Artisan::call('scout:import', ['model' => $searchableModelClass]);
        }
    }

    public static function reIndexSearch()
    {
        foreach(self::$searchableModelClasses as $searchableModelClass) {
            /** @var Model $searchableModelClass */
            $searchableModelClass::all()->each(function(Model $model) {
                $model->touch(); //forces the save
                $model->save();
            });
        }
    }

    /**
     * Flushes the AbstractTranslationModels from the search datatabases where a certain attribute has a certain value on the AbstractTranslatableModel.
     * If it does not equal the value, it will index the AbstractTranslationModels.
     * Does this for all searchModel classes or a specific instance by delegating to method flushOrIndexTranslationSearchModelsFromTranslatableWhereModelAttributeEquals()
     *
     * @param string $attributeName
     * @param int $attributeValue
     * @param Model|null $model
     * @see manageSearchabilityForTranslatableWhereModelWhere()
     */
    public static function manageSearchabilityForGivenOrAllTranslatablesWhere(Model $model = null, $attributeName = 'active', $attributeValue = 0)
    {
        if(!$model) {
            foreach (self::$searchableModelClasses as $modelClassName) {
                $modelClassName::all()->each(function(Model $model) use ($attributeName, $attributeValue) {
                    self::manageSearchabilityForTranslatableWhereModelWhere($model, $attributeName, $attributeValue);
                });
            }
        } else {
            self::manageSearchabilityForTranslatableWhereModelWhere($model, $attributeName, $attributeValue);
        }
    }

    /**
     * Flushes the AbstractTranslationModels from the search datatabases where a certain attribute has a certain value on the AbstractTranslatableModel.
     * If it does not equal the value, it will index the AbstractTranslationModels.
     * Does this for a specific searchable model instance instance which is a AbstractTranslatableModel instance.
     *
     * @param string $attributeName
     * @param int $attributeValue
     * @param Model $model
     */
    private static function manageSearchabilityForTranslatableWhereModelWhere(Model $model, $attributeName = 'active', $attributeValue = 0){
        /** @var Model $model */
        if(!is_a($model, AbstractTranslatableModel::class, true)) return;
        if(!array_key_exists($attributeName, $model->attributesToArray())) return;

        if($model->$attributeName == $attributeValue) {
            /** @var AbstractTranslatableModel $model */
            $model->translations()->get()->each(function (AbstractTranslationModel $abstractTranslationModel) use ($model, $attributeName, $attributeValue) {
                if (is_callable([$abstractTranslationModel, 'unsearchable'], false) && method_exists($abstractTranslationModel, 'unsearchable')) {
                    $abstractTranslationModel->unsearchable();
//                    \Log::debug('Made ' . get_class($abstractTranslationModel) . ' instance with id ' . $abstractTranslationModel->id . ' unsearchable because the attribute ' . $attributeName . ' of class instance ' . get_class($model) . ' with id ' . $model->id . ' matched the expected ' . $attributeValue);
                }
            });
        } else {
            /** @var AbstractTranslatableModel $model */
            $model->translations()->get()->each(function (AbstractTranslationModel $abstractTranslationModel) use ($model, $attributeName, $attributeValue) {
                if (is_callable([$abstractTranslationModel, 'searchable'], false) && method_exists($abstractTranslationModel, 'unsearchable')) {
                    $abstractTranslationModel->searchable();
//                    \Log::debug('Made ' . get_class($abstractTranslationModel) . ' instance with id ' . $abstractTranslationModel->id . ' searchable because the attribute ' . $attributeName . ' of class instance ' . get_class($model) . ' with id ' . $model->id . ' did not match the expected ' . $attributeValue);
                }
            });
        }
    }
}