id; } #[ApiProperty(writable: false, readable: true, required: false)] public function getProduct_id(): ?int { return $this->product_id; } #[ApiProperty(writable: false, readable: true, required: false)] public function getSku(): ?string { return $this->sku; } #[ApiProperty(writable: false, readable: true, required: false)] public function getName(): ?string { return $this->name; } #[ApiProperty(writable: false, readable: true, required: false)] public function getPrice(): ?float { return $this->price !== null ? (float) $this->price : null; } #[ApiProperty(writable: false, readable: true, required: false)] public function getCompare_price(): ?float { return $this->compare_price !== null ? (float) $this->compare_price : null; } #[ApiProperty(writable: false, readable: true, required: false)] public function getSpecial_price(): ?float { return $this->special_price !== null ? (float) $this->special_price : null; } #[ApiProperty(writable: false, readable: true, required: false)] public function getSpecial_price_from(): ?string { return $this->special_price_from ? $this->special_price_from->toDateString() : null; } #[ApiProperty(writable: false, readable: true, required: false)] public function getSpecial_price_to(): ?string { return $this->special_price_to ? $this->special_price_to->toDateString() : null; } #[ApiProperty(writable: false, readable: true, required: false)] public function getCost(): ?float { return $this->cost !== null ? (float) $this->cost : null; } #[ApiProperty(writable: false, readable: true, required: false)] public function getWeight(): ?float { return $this->weight !== null ? (float) $this->weight : null; } #[ApiProperty(writable: false, readable: true, required: false)] public function getQuantity(): ?int { return $this->quantity !== null ? (int) $this->quantity : null; } #[ApiProperty(writable: false, readable: true, required: false)] public function getStatus(): ?bool { return (bool) $this->status; } #[ApiProperty(writable: false, readable: true, required: false)] public function getSort_order(): ?int { return $this->sort_order !== null ? (int) $this->sort_order : null; } /** * Effective price considering variant-level special_price window. */ public function getEffectivePriceAttribute(): float { return $this->getBasicEffectivePrice(); } #[ApiProperty(writable: false, readable: true, required: false)] public function getEffective_price(): ?float { return $this->getEffectivePriceAttribute(); } #[ApiProperty(writable: false, readable: true, required: false)] public function getIs_saleable(): bool { return $this->isSaleable(); } /** * Variant images stored in the Spatie media table via product_variant_images pivot. */ public function variant_images(): BelongsToMany { return $this->belongsToMany( Media::class, 'product_variant_images', 'product_variant_id', 'media_id' )->withPivot('position') ->orderByPivot('position'); } /** * Serialize variant images inline as a JSON string to avoid IRI generation * (Spatie Media is not an ApiResource). * * Must be named getVariantImagesAttribute so that ApiPlatform's * EloquentPropertyNameCollectionMetadataFactory picks it up as a virtual * attribute (snake_cased: 'variant_images', camelCase in GraphQL: * 'variantImages'). * * Returns: '[{"id":1,"url":"https://...","position":0}, ...]' or null. */ public function getVariantImagesAttribute(): ?string { if (! $this->relationLoaded('variant_images')) { $this->load('variant_images'); } $payload = ($this->getRelation('variant_images') ?? collect()) ->map(fn (Media $m) => [ 'id' => $m->id, 'url' => $m->getUrl(), 'position' => $m->pivot->position ?? 0, ]) ->values() ->all(); return empty($payload) ? null : json_encode($payload); } /** * Option values attached to the variant (e.g. Red + Small). */ public function values(): BelongsToMany { return $this->belongsToMany( \Longyi\Core\Models\ProductOptionValue::class, 'product_variant_option_values', 'product_variant_id', 'product_option_value_id' )->withTimestamps(); } /** * Serialize option values inline as a JSON string to avoid registering a * separate ApiResource for ProductOptionValue. * * Named getOptionValuesAttribute so ApiPlatform picks it up as a virtual * attribute: snake_cased 'option_values', GraphQL field 'optionValues'. * * Returns: '[{"id":1,"label":"Red","code":"red","option_id":5,"option_code":"color"}, ...]' or null. */ public function getOptionValuesAttribute(): ?string { if (! $this->relationLoaded('values')) { $this->load('values.option'); } $payload = ($this->getRelation('values') ?? collect()) ->map(fn ($value) => [ 'id' => $value->id, 'label' => $value->label, 'code' => $value->code, 'option_id' => $value->product_option_id, 'option_code' => $value->option?->code, ]) ->values() ->all(); return empty($payload) ? null : json_encode($payload); } public function product(): BelongsTo { return $this->belongsTo(Product::class, 'product_id'); } }