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