|
|
@@ -22,6 +22,8 @@ use Webkul\BagistoApi\State\ProductGraphQLProvider;
|
|
|
use Webkul\BagistoApi\State\ProductProcessor;
|
|
|
use Webkul\Product\Models\Product as BaseProduct;
|
|
|
use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
|
|
|
+use Longyi\Core\Models\ProductOption;
|
|
|
+use Longyi\Core\Models\ProductOptionValue;
|
|
|
|
|
|
#[ApiResource(
|
|
|
routePrefix: '/api/shop',
|
|
|
@@ -499,6 +501,7 @@ class Product extends BaseProduct
|
|
|
'variants',
|
|
|
'price_indices',
|
|
|
'flexibleVariants',
|
|
|
+ 'options',
|
|
|
|
|
|
];
|
|
|
|
|
|
@@ -512,6 +515,7 @@ class Product extends BaseProduct
|
|
|
'color', 'size', 'brand', 'locale', 'channel', 'description_html',
|
|
|
'minimum_price', 'maximum_price', 'regular_minimum_price', 'regular_maximum_price',
|
|
|
'index', 'combinations', 'super_attribute_options',
|
|
|
+ 'product_options',
|
|
|
];
|
|
|
|
|
|
|
|
|
@@ -744,6 +748,147 @@ class Product extends BaseProduct
|
|
|
{
|
|
|
$this->images = $value;
|
|
|
}
|
|
|
+ /**
|
|
|
+ * Get product options relationship.
|
|
|
+ */
|
|
|
+ public function options(): BelongsToMany
|
|
|
+ {
|
|
|
+ return $this->belongsToMany(ProductOption::class, 'product_product_options', 'product_id', 'product_option_id')
|
|
|
+ ->withPivot(['position', 'is_required', 'meta'])
|
|
|
+ ->withTimestamps();
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Get product options.
|
|
|
+ */
|
|
|
+ #[ApiProperty(
|
|
|
+ writable: false,
|
|
|
+ readable: true,
|
|
|
+ required: false
|
|
|
+ )]
|
|
|
+ #[Groups(['mutation'])]
|
|
|
+ public function getOptions(): ?string
|
|
|
+ {
|
|
|
+ $jsonData= $this->getOptionsAttribute();
|
|
|
+ return $jsonData !== '{}' ? $jsonData: null;
|
|
|
+ }
|
|
|
+ public function setOptions($value): void
|
|
|
+ {
|
|
|
+ $this->options = $value;
|
|
|
+ }
|
|
|
+ public function getOptionsAttribute(): string
|
|
|
+ {
|
|
|
+ if ($this->type !== 'flexible_variant') {
|
|
|
+ return '{}';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (! $this->relationLoaded('options')) {
|
|
|
+ $this->load('options');
|
|
|
+ }
|
|
|
+
|
|
|
+ // NOTE: Use getRelation('options') rather than $this->options — the
|
|
|
+ // latter would recursively invoke this mutator because Eloquent's
|
|
|
+ // attribute accessor takes precedence over the relation.
|
|
|
+ $options = $this->getRelation('options') ?? collect();
|
|
|
+
|
|
|
+ if ($options->isEmpty()) {
|
|
|
+ return '{}';
|
|
|
+ }
|
|
|
+
|
|
|
+ $optionIds = $options->pluck('id')->toArray();
|
|
|
+ $optionCodeMap = $options->pluck('code', 'id')->toArray();
|
|
|
+
|
|
|
+ $index = [];
|
|
|
+
|
|
|
+ foreach ($options as $option) {
|
|
|
+ if (! isset($index[$option->id])) {
|
|
|
+ $index[$option->id] = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (! $option->relationLoaded('values')) {
|
|
|
+ $option->load('values');
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($option->values as $value) {
|
|
|
+ if (in_array($value->id, $optionIds)) {
|
|
|
+ $optionCode = $optionCodeMap[$value->id] ?? null;
|
|
|
+ if ($optionCode) {
|
|
|
+ $index[$option->id][$optionCode] = $value->value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return json_encode($index);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Full product option relation data serialized inline as a JSON string.
|
|
|
+ *
|
|
|
+ * Follows the same pattern as ProductVariant::variant_images / option_values
|
|
|
+ * — returns JSON so that ApiPlatform does not attempt IRI generation for
|
|
|
+ * ProductOption / ProductOptionValue (they are not registered as
|
|
|
+ * ApiResources).
|
|
|
+ *
|
|
|
+ * Named getProductOptionsAttribute so it is picked up by ApiPlatform's
|
|
|
+ * EloquentPropertyNameCollectionMetadataFactory as a virtual attribute:
|
|
|
+ * snake_case 'product_options', GraphQL field 'productOptions'.
|
|
|
+ *
|
|
|
+ * Returns JSON like:
|
|
|
+ * [
|
|
|
+ * {
|
|
|
+ * "id": 1, "label": "Size", "code": "size", "type": "dropdown",
|
|
|
+ * "position": 0, "is_required": true,
|
|
|
+ * "values": [
|
|
|
+ * {"id": 1, "label": "Small", "code": "s", "position": 0},
|
|
|
+ * ...
|
|
|
+ * ]
|
|
|
+ * },
|
|
|
+ * ...
|
|
|
+ * ]
|
|
|
+ * Returns null when the product has no options.
|
|
|
+ */
|
|
|
+ public function getProductOptionsAttribute(): ?string
|
|
|
+ {
|
|
|
+ if (! $this->relationLoaded('options')) {
|
|
|
+ $this->load('options.values');
|
|
|
+ }
|
|
|
+
|
|
|
+ // NOTE: Use getRelation('options') — accessing $this->options would
|
|
|
+ // recursively invoke the getOptionsAttribute mutator (which returns a
|
|
|
+ // JSON-string variant map, not the relation Collection).
|
|
|
+ $options = $this->getRelation('options') ?? collect();
|
|
|
+
|
|
|
+ if ($options->isNotEmpty() && ! $options->first()->relationLoaded('values')) {
|
|
|
+ $options->loadMissing('values');
|
|
|
+ }
|
|
|
+
|
|
|
+ $payload = $options
|
|
|
+ ->map(function ($option) {
|
|
|
+ return [
|
|
|
+ 'id' => (int) $option->id,
|
|
|
+ 'label' => $option->label,
|
|
|
+ 'code' => $option->code,
|
|
|
+ 'type' => $option->type,
|
|
|
+ 'position' => (int) ($option->pivot->position ?? $option->position ?? 0),
|
|
|
+ 'is_required' => (bool) ($option->pivot->is_required ?? false),
|
|
|
+ 'meta' => $option->pivot->meta ?? $option->meta ?? null,
|
|
|
+ 'values' => ($option->values ?? collect())
|
|
|
+ ->map(fn ($v) => [
|
|
|
+ 'id' => (int) $v->id,
|
|
|
+ 'label' => $v->label,
|
|
|
+ 'code' => $v->code,
|
|
|
+ 'position' => (int) ($v->position ?? 0),
|
|
|
+ 'meta' => $v->meta ?? null,
|
|
|
+ ])
|
|
|
+ ->values()
|
|
|
+ ->all(),
|
|
|
+ ];
|
|
|
+ })
|
|
|
+ ->values()
|
|
|
+ ->all();
|
|
|
+
|
|
|
+ return empty($payload) ? null : json_encode($payload);
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
* Flexible variants — overrides the parent's flexibleVariants() so that
|