*/ protected $fillable = [ 'seo_title', 'seo_description', 'label', 'slug', 'claim_title', 'picture', 'picture_alt', 'picture_title', 'banner', 'banner_alt', 'banner_title', 'packaging', 'packaging_alt', 'packaging_title', 'description', 'details', 'preparation', 'ingredients', 'sales_name', 'approach_id', 'range_id', 'subrange_id', 'meat_id', 'submeat_id', 'new', 'discover', 'published', ]; /** * The attributes that should be cast. * * @var array */ protected $casts = [ 'new' => 'boolean', 'discover' => 'boolean', 'published' => 'boolean', ]; /** * Get model's setting to apply on scout search (meilisearch). * * @var array */ public static $searchSettings = [ 'filterableAttributes' => ['label', 'description', 'preparation', 'ingredients', 'published'], 'sortableAttributes' => ['label', 'description', 'preparation', 'ingredients'], ]; /** * Get sortable virtual column. * * @var array */ public static $sortableVirtualColumn = [ 'ratings_avg_rate', ]; /** * Perform any actions required after the model boots. */ protected static function booted(): void { static::creating(function (self $product) { self::assertLabelIsUnique($product->label); self::manageFields($product); }); static::created(function (self $product) { self::resetCache(); Rating::resetCache($product); }); static::updating(function (self $product) { self::assertLabelIsUnique($product->label, $product->getKey()); self::manageFields($product); }); static::updated(function (self $product) { FileStorageHelper::removeOldFile($product, 'picture'); FileStorageHelper::removeOldFile($product, 'banner'); FileStorageHelper::removeOldFile($product, 'packaging'); self::resetCache(); Rating::resetCache($product); }); static::deleted(function (self $product) { FileStorageHelper::removeFile($product, 'picture'); FileStorageHelper::removeFile($product, 'banner'); FileStorageHelper::removeFile($product, 'packaging'); self::resetCache(); Rating::resetCache($product); }); } // * METHODS /** * Manage model fields. */ private static function manageFields(self $product): void { $product->slug = Str::slug($product->label); $product->picture = FileStorageHelper::storeFile($product, $product->picture, true); $product->banner = FileStorageHelper::storeFile($product, $product->banner, true); $product->packaging = FileStorageHelper::storeFile($product, $product->packaging, true); } /** * Asserts using validation that the label is unique * * @throws \Illuminate\Validation\ValidationException If a range label already exists. */ private static function assertLabelIsUnique(?string $label, ?int $id = null): void { $table = (new self())->getTable(); ToolboxHelper::assertFieldIsUnique($table, 'label', $label, $id); ToolboxHelper::assertFieldIsUnique($table, 'slug', Str::slug($label), $id); } /** * Display a listing of the resource. * * @return array */ public static function getProductsViewsByRange(?string $slug = null): array { $rangeModel = ! is_null($slug) ? Range::query()->where([['slug', $slug], ['published', true]])->first() : Range::query()->where('published', true)->orderBy('slug', 'ASC')->first(); return Product::query() ->whereHas('subrange', function (Builder $q) use ($rangeModel) { $q->where('published', true)->whereHas('range', function (Builder $q) use ($rangeModel) { $q->where('id', $rangeModel->getKey()); }); })->where('published', true) ->orWhereHas('range', function (Builder $q) use ($rangeModel) { $q->where('id', $rangeModel->getKey()); })->where('published', true) ->inRandomOrder() ->limit(6) ->get() ->map(function ($productModel) { return view('fo.partials.product-card', ['model' => $productModel])->render(); })->toArray(); } /** * Transform to seoModel. */ public function toSeoItem(): SeoItem { return new SeoItem( 'product', $this->seo_title, $this->seo_description, $this->published ?? false, \route(\config('seo.bo_route_prefix') . $this->getTable() . '.edit', $this), route('fo.products.show', $this->slug) ); } /** * Reset all cache elements related to this model. */ private static function resetCache(): void { Cache::forget('publishedDiscoveredProducts'); self::getPublishedDiscovered(); } /** * Get all published products with discover === true. */ public static function getPublishedDiscovered(): Collection { return Cache::remember('publishedDiscoveredProducts', 86400, function () { return self::query()->where('published', true)->where('discover', true)->get(); }); } /** * toSitemapTag method */ public function toSitemapTag(): Url | string | array { return SitemapHelper::generateUrl('fo.products.show', ['slug' => $this->slug], $this); } /** * Determine if the model should be searchable. */ public function shouldBeSearchable(): bool { return $this->published === true; } /** * Create a SchemaOrg. */ public function toSchemaOrg(bool $asReference = false): \Spatie\SchemaOrg\Product { $model = $this->load([ 'recipes', 'claims', 'productTypes', 'meat', 'submeat', 'range', 'subrange', 'videos', 'ratings', ]); // Main. $schema = Schema::Product() ->name($model->seo_title) ->description($model->seo_description) ->url(route('fo.products.show', $model->slug)) ->keywords('produit') ->releaseDate($model->created_at) ->image([ Schema::imageObject() ->url(asset($model->picture)) ->headline($model->picture_title), Schema::imageObject() ->url(asset($model->packaging)) ->headline($model->packaging_title), ]) ->brand($model->getBrandSchema()); // Ratings. if ($model->ratings->count() && Rating::getAverageRating($model->ratings) > 2.0) { $schema->aggregateRating( Rating::toSchemaOrg($model->ratings) ); } // Recipes associated. if ($model->recipes->count() && ! $asReference) { $schema->subjectOf($model->recipes->where('published', true)->map(function (Recipe $recipe) { return $recipe->toSchemaOrg(true); })->toArray()); } // Claim associated. if ($model->claims->count()) { $schema->subjectOf($model->claims->where('published', true)->map(function (Claim $claim) { return $claim->toSchemaOrg(); })->toArray()); } // Product types associated. if ($model->productTypes->count()) { $schema->subjectOf($model->productTypes->where('published', true)->map(function (ProductType $productType) { return $productType->toSchemaOrg(); })->toArray()); } // Range/Subrange & Meat/Submeat associated. $categories = []; if ($model->range_id) { array_push($categories, $model->range->toSchemaOrg()); } else { array_push($categories, $model->subrange->toSchemaOrg()); } if ($model->meat_id) { array_push($categories, $model->meat->toSchemaOrg()); } else { array_push($categories, $model->submeat->toSchemaOrg()); } $schema->category($categories); if ($model->videos->count()) { $model->videos->map(function (Video $videoModel) use ($schema) { $schema->subjectOf( Schema::creativeWork()->associatedMedia($videoModel->toSchemaOrg()) ); }); } return $schema; } // * REDIRECTIONS. /** * Get the front url of this model. * Should return the model.show route, with all binded elements if needed. */ public function getFrontUrl(string $modelKey): string { return route('fo.products.show', $modelKey); } // * RELATIONSHIPS /** * Get the range that are assigned this product. */ public function range(): BelongsTo { return $this->belongsTo(Range::class); } /** * Get the subrange that are assigned this product. */ public function subrange(): BelongsTo { return $this->belongsTo(Subrange::class); } /** * Get the meat that are assigned this product. */ public function meat(): BelongsTo { return $this->belongsTo(Meat::class); } /** * Get the submeat that are assigned this product. */ public function submeat(): BelongsTo { return $this->belongsTo(Submeat::class); } /** * Get all of the recipes that are assigned this product. */ public function recipes(): MorphToMany { return $this->morphedByMany(Recipe::class, 'productable'); } /** * Get the demarch assigned to this product. */ public function approach(): BelongsTo { return $this->belongsTo(Approach::class); } /** * Get all labels that are assigned to this product. */ public function labels(): MorphToMany { return $this->morphedByMany(Approach::class, 'productable'); } /** * Get all product types that are assigned to this product. */ public function productTypes(): MorphToMany { return $this->morphedByMany(ProductType::class, 'productable'); } /** * Get all product types that are assigned to this product. */ public function claims(): MorphToMany { return $this->morphedByMany(Claim::class, 'productable'); } /** * Get the videos for the product. */ public function videos(): HasMany { return $this->hasMany(Video::class); } /** * Get the nutritionnal values for the product. */ public function getNutritionnalValue(): ?IncoNutritionalValue { return $this->hasMany(IncoNutritionalValue::class)->first(); } /** * Get the nutritionnal values for the product. */ public function ratings(): MorphMany { return $this->morphMany(Rating::class, 'rating'); } }