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/komma.pro/app/KommaApp/Shop/Catalog/Kms/CatalogService.php
<?php
namespace App\KommaApp\Shop\Catalog\Kms;


use App\KommaApp\JsonApi\JsonApiLengthAwarePaginator;
use App\KommaApp\JsonApi\TranslatableResource;
use App\KommaApp\Kms\Core\AbstractTranslatableModel;
use App\KommaApp\Kms\Core\AbstractTranslationModel;
use App\KommaApp\Kms\Core\Sections\SectionService;
use App\KommaApp\Kms\Core\Sections\SectionServiceInterface;
use App\KommaApp\Shop\Categories\Kms\CategoryServiceInterface;
use App\KommaApp\Shop\Products\Product\ProductServiceInterface;
use App\KommaApp\Shop\Products\ProductComposite\ProductCompositeServiceInterface;
use App\KommaApp\Shop\Products\ProductGroup\ProductGroupServiceInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Schema;
use Symfony\Component\HttpFoundation\Request;

/**
 * Builds the product catalog for the frontend
 *
 * Class CatalogService
 * @package App\KommaApp\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 Model[] $catalogable items
     */
    private $catalogableServicesInterfaces = [];

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

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

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

        parent::__construct();
    }

    public function getAllItems() {
        $allItems = CatalogIndexModel::all();

        foreach ($allItems as $item) {
                $type = $item->catalogable_type;
                $id = $item->catalogable_id;
                $item->item = $type::with('translation')->where('id', $id)->first();
            }

        return $allItems;
    }

    /**
     * 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::make($catalogableServiceInterface);

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

            $modelCollection->each(function($model) use ($service) {
//                $catalogIndexModel = new $this->forModelName;
                $catalogIndexModel = $this->forModelName::firstOrNew([
                    'catalogable_type' => $service->getForModelName(),
                    'catalogable_id' => $model->id
                ]);

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

    /**
     * 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|JsonResponse|\Illuminate\Http\Resources\Json\AnonymousResourceCollection
     */
    public function search(Request $request) {
        $term = $request->input('term');
        $currentPage = ($request->has('page')) ? $request->input('page') : null;
        $perPage = ($request->has('amount')) ? $request->input('amount') : 10;
        $paginate = ($currentPage) ? true : false;

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

        $foundModels = collect();

        foreach($this->catalogableServicesInterfaces as $catalogableServicesInterface)
        {
            /** @var SectionServiceInterface $service */
            $service = \App::make($catalogableServicesInterface);

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

            //Search models
            $models = $forModelName::search($term)->get();
            if(method_exists($forModelName, 'search')) $foundModels = $foundModels->merge($models);

            //Search translation models
            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();
                    $foundModels = $foundModels->merge($translationModels);
                }
            }
        }

        $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) {
                    $resource = new TranslatableResource($translatable);
                }
            } else if(is_a($model, AbstractTranslatableModel::class)) {
                /** @var AbstractTranslatableModel $model */
                $resource = new TranslatableResource($model);
            }
            return $resource;
        })->filter(function($value) { return $value !== null; });


        if($paginate) {
            $resourcesPaginator = new JsonApiLengthAwarePaginator(
                $resources->forPage($currentPage, $perPage),
                $resources->count(),
                $perPage,
                $currentPage, [
                    'path' => route('catalog.search')
                ]
            );

            return $resourcesPaginator;
        } else {
            return TranslatableResource::collection($resources);
        }
    }
}