ProductVariant.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. <?php
  2. namespace Webkul\BagistoApi\Models;
  3. use ApiPlatform\Metadata\ApiProperty;
  4. use ApiPlatform\Metadata\ApiResource;
  5. use ApiPlatform\Metadata\GraphQl\Query;
  6. use Illuminate\Database\Eloquent\Relations\BelongsTo;
  7. use Illuminate\Database\Eloquent\Relations\BelongsToMany;
  8. use Longyi\Core\Models\ProductVariant as BaseProductVariant;
  9. use Spatie\MediaLibrary\MediaCollections\Models\Media;
  10. use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
  11. /**
  12. * BagistoApi wrapper around the Longyi1 flexible variant model so that
  13. * ApiPlatform can expose variants (and their images) as part of the
  14. * single product query.
  15. */
  16. #[ApiResource(
  17. routePrefix: '/api/shop',
  18. shortName: 'ProductVariant',
  19. operations: [],
  20. graphQlOperations: [
  21. new Query(resolver: BaseQueryItemResolver::class),
  22. ]
  23. )]
  24. class ProductVariant extends BaseProductVariant
  25. {
  26. protected $appends = [
  27. 'effective_price',
  28. 'variant_images',
  29. 'option_values',
  30. ];
  31. #[ApiProperty(identifier: true, writable: false)]
  32. public function getId(): ?int
  33. {
  34. return $this->id;
  35. }
  36. #[ApiProperty(writable: false, readable: true, required: false)]
  37. public function getProduct_id(): ?int
  38. {
  39. return $this->product_id;
  40. }
  41. #[ApiProperty(writable: false, readable: true, required: false)]
  42. public function getSku(): ?string
  43. {
  44. return $this->sku;
  45. }
  46. #[ApiProperty(writable: false, readable: true, required: false)]
  47. public function getName(): ?string
  48. {
  49. return $this->name;
  50. }
  51. #[ApiProperty(writable: false, readable: true, required: false)]
  52. public function getPrice(): ?float
  53. {
  54. return $this->price !== null ? (float) $this->price : null;
  55. }
  56. #[ApiProperty(writable: false, readable: true, required: false)]
  57. public function getCompare_price(): ?float
  58. {
  59. return $this->compare_price !== null ? (float) $this->compare_price : null;
  60. }
  61. #[ApiProperty(writable: false, readable: true, required: false)]
  62. public function getSpecial_price(): ?float
  63. {
  64. return $this->special_price !== null ? (float) $this->special_price : null;
  65. }
  66. #[ApiProperty(writable: false, readable: true, required: false)]
  67. public function getSpecial_price_from(): ?string
  68. {
  69. return $this->special_price_from ? $this->special_price_from->toDateString() : null;
  70. }
  71. #[ApiProperty(writable: false, readable: true, required: false)]
  72. public function getSpecial_price_to(): ?string
  73. {
  74. return $this->special_price_to ? $this->special_price_to->toDateString() : null;
  75. }
  76. #[ApiProperty(writable: false, readable: true, required: false)]
  77. public function getCost(): ?float
  78. {
  79. return $this->cost !== null ? (float) $this->cost : null;
  80. }
  81. #[ApiProperty(writable: false, readable: true, required: false)]
  82. public function getWeight(): ?float
  83. {
  84. return $this->weight !== null ? (float) $this->weight : null;
  85. }
  86. #[ApiProperty(writable: false, readable: true, required: false)]
  87. public function getQuantity(): ?int
  88. {
  89. return $this->quantity !== null ? (int) $this->quantity : null;
  90. }
  91. #[ApiProperty(writable: false, readable: true, required: false)]
  92. public function getStatus(): ?bool
  93. {
  94. return (bool) $this->status;
  95. }
  96. #[ApiProperty(writable: false, readable: true, required: false)]
  97. public function getSort_order(): ?int
  98. {
  99. return $this->sort_order !== null ? (int) $this->sort_order : null;
  100. }
  101. /**
  102. * Effective price considering variant-level special_price window.
  103. */
  104. public function getEffectivePriceAttribute(): float
  105. {
  106. return $this->getBasicEffectivePrice();
  107. }
  108. #[ApiProperty(writable: false, readable: true, required: false)]
  109. public function getEffective_price(): ?float
  110. {
  111. return $this->getEffectivePriceAttribute();
  112. }
  113. #[ApiProperty(writable: false, readable: true, required: false)]
  114. public function getIs_saleable(): bool
  115. {
  116. return $this->isSaleable();
  117. }
  118. /**
  119. * Variant images stored in the Spatie media table via product_variant_images pivot.
  120. */
  121. public function variant_images(): BelongsToMany
  122. {
  123. return $this->belongsToMany(
  124. Media::class,
  125. 'product_variant_images',
  126. 'product_variant_id',
  127. 'media_id'
  128. )->withPivot('position')
  129. ->orderByPivot('position');
  130. }
  131. /**
  132. * Serialize variant images inline as a JSON string to avoid IRI generation
  133. * (Spatie Media is not an ApiResource).
  134. *
  135. * Must be named getVariantImagesAttribute so that ApiPlatform's
  136. * EloquentPropertyNameCollectionMetadataFactory picks it up as a virtual
  137. * attribute (snake_cased: 'variant_images', camelCase in GraphQL:
  138. * 'variantImages').
  139. *
  140. * Returns: '[{"id":1,"url":"https://...","position":0}, ...]' or null.
  141. */
  142. public function getVariantImagesAttribute(): ?string
  143. {
  144. if (! $this->relationLoaded('variant_images')) {
  145. $this->load('variant_images');
  146. }
  147. $payload = ($this->getRelation('variant_images') ?? collect())
  148. ->map(fn (Media $m) => [
  149. 'id' => $m->id,
  150. 'url' => $m->getUrl(),
  151. 'position' => $m->pivot->position ?? 0,
  152. ])
  153. ->values()
  154. ->all();
  155. return empty($payload) ? null : json_encode($payload);
  156. }
  157. /**
  158. * Option values attached to the variant (e.g. Red + Small).
  159. */
  160. public function values(): BelongsToMany
  161. {
  162. return $this->belongsToMany(
  163. \Longyi\Core\Models\ProductOptionValue::class,
  164. 'product_variant_option_values',
  165. 'product_variant_id',
  166. 'product_option_value_id'
  167. )->withTimestamps();
  168. }
  169. /**
  170. * Serialize option values inline as a JSON string to avoid registering a
  171. * separate ApiResource for ProductOptionValue.
  172. *
  173. * Named getOptionValuesAttribute so ApiPlatform picks it up as a virtual
  174. * attribute: snake_cased 'option_values', GraphQL field 'optionValues'.
  175. *
  176. * Returns: '[{"id":1,"label":"Red","code":"red","option_id":5,"option_code":"color"}, ...]' or null.
  177. */
  178. public function getOptionValuesAttribute(): ?string
  179. {
  180. if (! $this->relationLoaded('values')) {
  181. $this->load('values.option');
  182. }
  183. $payload = ($this->getRelation('values') ?? collect())
  184. ->map(fn ($value) => [
  185. 'id' => $value->id,
  186. 'label' => $value->label,
  187. 'code' => $value->code,
  188. 'option_id' => $value->product_option_id,
  189. 'option_code' => $value->option?->code,
  190. ])
  191. ->values()
  192. ->all();
  193. return empty($payload) ? null : json_encode($payload);
  194. }
  195. public function product(): BelongsTo
  196. {
  197. return $this->belongsTo(Product::class, 'product_id');
  198. }
  199. }