$products * @property-read \App\Models\Range $range * @property-read \Illuminate\Database\Eloquent\Collection $stats * @property-read \Illuminate\Database\Eloquent\Collection $tags * @property-read \App\Models\Discount|null $validDiscount * @property-read float $weight_grams * * @method static Builder|Product onlyTrashed() * @method static Builder|Product orderable() * @method static Builder|Product published() * @method static Builder|Product withTrashed(bool $withTrashed = true) * @method static Builder|Product withoutTrashed() * * @mixin \Eloquent */ #[Fillable([ 'name', 'brand', 'description', 'invoicing_unit', 'stock', 'weight', 'orderable_start', 'orderable_end', 'range_id', 'star', 'status', 'weight_variable', 'sell_unit', 'order_unit', 'tax_rate', 'picture', 'short_description', 'picture_alt', 'picture_title', 'picture_large_alt', 'picture_large_title', 'picture_large', ])] class Product extends Model implements ModelWithOrder, ModelWithStar, ModelWithStatus, RedirectableInterface { use HasOrder; use HasStar; use HasStatus; use HasUuidV7AsPrimaryKey; use IsRedirectable; use SoftDeletes; public const CREATED_AT = null; public function getId(): ProductId { return new ProductId(Uuid::fromString($this->id)); } #[\Override] protected static function booted(): void { static::saving(function (self $model): void { self::manageFields($model); self::makeStatusSense($model); self::makeStarSense($model); }); static::deleting(function (self $model): void { FileStorageHelper::removeFile($model, 'picture'); FileStorageHelper::removeFile($model, 'picture_large'); }); } private static function manageFields(self $model): void { self::assertNameIsUnique($model->name, $model->id); $model->slug = Str::slug($model->name); $model->picture = FileStorageHelper::storeFile($model, $model->picture, true); $model->picture_large = FileStorageHelper::storeFile($model, $model->picture_large, true); if ($model->order === null) { $max = self::query()->max('order'); $model->order = is_scalar($max) ? intval($max) + 1 : 1; } } private static function assertNameIsUnique(?string $name, ?string $id = null): void { $table = new self()->getTable(); ToolboxHelper::assertFieldIsUnique($table, 'name', $name, $id); ToolboxHelper::assertFieldIsUnique($table, 'slug', Str::slug($name ?? ''), $id); } #[\Override] protected function casts(): array { return [ 'orderable_start' => 'datetime', 'orderable_end' => 'datetime', 'price' => MoneyCast::class, 'brand' => ProductBrand::class, 'invoicing_unit' => InvoicingUnit::class, 'sell_unit' => SellUnit::class, 'order_unit' => OrderUnit::class, 'weight_variable' => 'bool', 'star' => 'bool', 'status' => 'bool', 'deleted_at' => 'datetime', 'tax_rate' => TaxRate::class, ]; } #[\Override] public function getRouteKeyName(): string { return 'slug'; } #[\Override] public function getFrontUrl(string $modelKey): string { return route('fo.products.show', $modelKey); } /** * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ protected function scopePublished(Builder $query): Builder { return $query->where('status', true); } /** * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ protected function scopeOrderable(Builder $query): Builder { $now = Date::now(); // NOTE: Scopes should be layed down to a repository. return $query // * Must have an associated inco ->has('inco') // * Orderable ->where(function (Builder $q) use ($now): void { $q->whereNull('orderable_start') ->orWhere('orderable_start', '<', $now); }) ->where(function (Builder $q) use ($now): void { $q->whereNull('orderable_end') ->orWhere('orderable_end', '>', $now); }) // * Available ->published(); } // * -- RELATIONSHIPS /** @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\Range,$this> */ public function range(): BelongsTo { return $this->belongsTo(Range::class); } /** @return \Illuminate\Database\Eloquent\Relations\HasMany<\App\Models\Stat,$this> */ public function stats(): HasMany { return $this->hasMany(Stat::class, 'product_id'); } /** @return \Illuminate\Database\Eloquent\Relations\HasOne<\App\Models\ProductInco,$this> */ public function inco(): HasOne { return $this->hasOne(ProductInco::class); } /** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function products(): BelongsToMany { return $this->belongsToMany( Product::class, 'product_product', 'product_id', 'associated_id' ) ->withPivot('sort_order') ->orderBy('pivot_sort_order', 'asc'); } /** @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\Discount,$this> */ public function discount(): BelongsTo { return $this->belongsTo(Discount::class, 'discount_id'); } /** @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\Discount,$this> */ public function validDiscount(): BelongsTo { return $this->discount() ->where(function ($q) { $q->whereNull('date_start')->orWhere('date_start', '<=', Date::now()); }) ->where(function ($q) { $q->whereNull('date_end')->orWhere('date_end', '>=', Date::now()); }); } /** @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\App\Models\Tag,$this> */ public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); } // * -- GETTERS /** @return \Illuminate\Database\Eloquent\Casts\Attribute */ protected function weightGrams(): Attribute { return Attribute::make( get: fn (): float => $this->product_weight ? (float) $this->product_weight->toGrams() : 0.0 ); } /** @return \Illuminate\Database\Eloquent\Casts\Attribute<\Domain\Models\ValueObjects\ProductWeight|null,\Domain\Models\ValueObjects\ProductWeight|null> */ protected function productWeight(): Attribute { return Attribute::make( get: function (mixed $_, array $attributes): ?ProductWeight { if (! isset($attributes['weight'])) { return null; } if (! is_int($attributes['weight'])) { throw new \UnexpectedValueException('Invalid weight attribute'); } return new ProductWeight( (int) $attributes['weight'], (bool) ($attributes['weight_variable'] ?? false) ); }, set: function (?ProductWeight $value): array { if ($value === null) { return [ 'weight' => null, 'weight_variable' => false, ]; } return [ 'weight' => $value->value, 'weight_variable' => $value->isVariable, ]; } ); } }