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/blije-gasten.komma.pro/app/Komma/Shop/Search/_SearchService.php
<?php

namespace App\Komma\Shop\Search;


use App\Komma\Kms\Core\ModelService;
use App\Komma\Kms\Core\ModelServiceInterface;
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\Pages\PageService;
use App\Komma\Shop\Categories\Kms\CategoryModelService;
use App\Komma\Shop\Categories\Kms\CategoryTranslationModelService;
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\ProductModelService;
use App\Komma\Shop\Products\Product\ProductTranslation;
use App\Komma\Shop\Products\Product\ProductTranslationModelService;
use App\Komma\Shop\Products\ProductableInterface;
use App\Komma\Shop\Products\ProductComposite\ProductCompositeModelService;
use App\Komma\Shop\Products\ProductComposite\ProductCompositeTranslation;
use App\Komma\Shop\Products\ProductGroup\ProductGroupModelService;
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\App;
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 SearchService
 * @package App\Komma\Shop\Search
 */
class _SearchService extends ModelService
{
    /**
     * @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 ModelServiceInterface[] ModelService 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,
//        Category::class,
        CategoryTranslation::class
    ];

    /**
     * SearchService constructor
     */
    public function __construct()
    {
        $this->catalogableServicesInterfaces[] = ProductTranslationModelService::class;
//        $this->catalogableServicesInterfaces[] = ProductGroupModelService::class;
        $this->catalogableServicesInterfaces[] = CategoryTranslationModelService::class;
//        $this->catalogableServicesInterfaces[] = ProductCompositeModelService::class;

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

        $this->setModelClassName(CatalogIndexModel::class);
        parent::__construct();
    }

    /**
     * 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)
    {
        //Validate that is ajax request
        if(!$request->ajax()) abort(400, 'This url should only be accessed via ajax requests. Set the X-Requested-With header and make sure you are POSTing to this url');

        //Return the parameters from the request
        $query = $request->input('query');

        // Optional request parameters
        $currentPage = ($request->has('page')) ? $request->input('page') : 1;
        $perPage = ($request->has('amount')) ? $request->input('amount') : 10;
        $paginate = ($currentPage) ? true : false;

        //Validate the request
        if (empty($query)) {
            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 ModelServiceInterface $service */
            $service = app($catalogableServicesInterface);

            /** @var ModelServiceInterface $catalogableServicesInterface */
            $modelClassName = $service->getModelClassName();

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

            //Search translation models of the current service
            if (is_subclass_of($modelClassName, AbstractTranslatableModel::class)) {

                /** @var AbstractTranslatableModel $modelClassName */
                $translationModel = get_class((new $modelClassName)->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) {
                        return $abstractTranslationModel->language_id === app()->getLanguage()->id;
                    });
                }
            }
        }

        debug($foundModels);

        //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);
        });

        debug($resources); 

        //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 {

            $pageService = new PageService();
            $links = $pageService->getAllTranslatedPageRoutes();
            $page = $links->search->node;
            $uniqueResources = $uniqueResources->sortBy(function ($resource) {
                return $resource->resource->lft;
            });

            return \View::make('shop.pages.search',[
                'page' => $page,
                'term' => $term,
                'links' => $links,
                'results' => $uniqueResources,
            ]);
        }
    }

    /**
     * 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()
    {
        if(!\Schema::hasTable('catalog_index')) throw abort(500, 'De shop catalog table is nog niet aangemaakt. Run "php artisan shop:migrate" om deze aan te maken');
        $productCount = Product::count();
        if($productCount == 0) throw abort(404, 'Er zijn geen producten aanwezig in de database. Heb je "php artisan shop:seed" gedraaid?');
        if($productCount > 0 && CatalogIndexModel::count() == 0) throw abort(404, 'Er zijn geen producten aanwezig in de catalog. Heb je "php artisan shop:catalog" gedraaid?');
        $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('SearchService indexing...');
        $this->validateCatalogIndexModelDefinition();

        foreach ($this->catalogableServicesInterfaces as $catalogableServiceInterface) {
            /** @var ModelServiceInterface $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->modelClassName::firstOrNew([
                    'catalogable_id' => $model->id,
                    'catalogable_type' => $service->getModelClassName()
                ]);

                /** @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->getModelClassName();
                    }
                }

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

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

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

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

        $debugCounter = 0;

        /** @var CatalogIndexModel[] $indexModels */
        $indexModels = $this->modelClassName::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('SearchService done housekeeping!');
    }

    /**
     * Checks if all attributes defined in this service are also attributes on the CatalogIndexModel
     */
    private function validateCatalogIndexModelDefinition()
    {
        $testModel = new $this->modelClassName;
        foreach ($this->attributes as $attribute => $type) {
            if (!$this->modelHasAttribute($testModel, $attribute)) {
                throw new \RuntimeException('The "' . $this->modelClassName . '" 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;
    }

    /**
     * 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;
    }



    /**
     * 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);
                }
            });
        }
    }
}