|
|
@@ -6,8 +6,8 @@ use ApiPlatform\Laravel\Eloquent\Paginator;
|
|
|
use ApiPlatform\Metadata\Operation;
|
|
|
use ApiPlatform\State\Pagination\Pagination;
|
|
|
use ApiPlatform\State\ProviderInterface;
|
|
|
-use Webkul\BagistoApi\Models\Product;
|
|
|
use Illuminate\Pagination\LengthAwarePaginator as LaravelPaginator;
|
|
|
+use Webkul\BagistoApi\Models\Product;
|
|
|
|
|
|
class ProductGraphQLProvider implements ProviderInterface
|
|
|
{
|
|
|
@@ -17,25 +17,32 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
|
|
|
private ?array $attributeTypeCache = null;
|
|
|
|
|
|
+ private ?array $attributeScopeCache = null;
|
|
|
+
|
|
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
|
|
{
|
|
|
- // Cache attribute types once for the entire request
|
|
|
+ // Cache attribute types and scope flags once for the entire request
|
|
|
$this->attributeTypeCache = \DB::table('attributes')
|
|
|
->pluck('type', 'code')
|
|
|
->toArray();
|
|
|
|
|
|
+ $this->attributeScopeCache = \DB::table('attributes')
|
|
|
+ ->get(['code', 'value_per_locale', 'value_per_channel'])
|
|
|
+ ->keyBy('code')
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
$args = $context['args'] ?? [];
|
|
|
|
|
|
$query = Product::query();
|
|
|
|
|
|
$query->whereHas('attribute_values', function ($q) {
|
|
|
$q->where('attribute_id', 8)
|
|
|
- ->where('boolean_value', 1);
|
|
|
+ ->where('boolean_value', 1);
|
|
|
});
|
|
|
|
|
|
$query->whereHas('attribute_values', function ($q) {
|
|
|
$q->where('attribute_id', 7)
|
|
|
- ->where('boolean_value', 1);
|
|
|
+ ->where('boolean_value', 1);
|
|
|
});
|
|
|
|
|
|
if (! empty($args['query'])) {
|
|
|
@@ -43,46 +50,59 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
|
|
|
$query->where(function ($q) use ($searchTerm) {
|
|
|
$q->where('sku', 'like', "%{$searchTerm}%")
|
|
|
- ->orWhereHas('attribute_values', function ($attr) use ($searchTerm) {
|
|
|
- $attr->where('text_value', 'like', "%{$searchTerm}%");
|
|
|
- });
|
|
|
+ ->orWhereHas('attribute_values', function ($attr) use ($searchTerm) {
|
|
|
+ $attr->where('attribute_id', 2)
|
|
|
+ ->where('text_value', 'like', "%{$searchTerm}%");
|
|
|
+ });
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- $sortKey = strtoupper($args['sortKey'] ?? 'ID');
|
|
|
- $reverse = (bool) ($args['reverse'] ?? false);
|
|
|
+ $sortKey = strtoupper($args['sortKey'] ?? 'ID');
|
|
|
+ $reverse = (bool) ($args['reverse'] ?? false);
|
|
|
$direction = $reverse ? 'desc' : 'asc';
|
|
|
- $locale = $args['locale'] ?? request()->attributes->get('bagisto_locale');
|
|
|
- $channel = $args['channel'] ?? request()->attributes->get('bagisto_channel');
|
|
|
+ $locale = $args['locale'] ?? request()->attributes->get('bagisto_locale');
|
|
|
+ $channel = $args['channel'] ?? request()->attributes->get('bagisto_channel');
|
|
|
|
|
|
switch ($sortKey) {
|
|
|
case 'TITLE':
|
|
|
case 'NAME':
|
|
|
- $query->leftJoin('product_attribute_values as pav_name', function ($join) {
|
|
|
- $join->on('products.id', '=', 'pav_name.product_id')
|
|
|
- ->where('pav_name.attribute_id', 2);
|
|
|
- })
|
|
|
- ->orderBy('pav_name.text_value', $direction)
|
|
|
- ->orderBy('products.id', $direction)
|
|
|
- ->select('products.*');
|
|
|
+ $prefix = \DB::getTablePrefix();
|
|
|
|
|
|
- if ($locale) {
|
|
|
- $query->where('pav_name.locale', $locale);
|
|
|
- }
|
|
|
+ // Join for requested locale/channel
|
|
|
+ $query->leftJoin('product_attribute_values as pav_name_locale', function ($join) use ($locale, $channel) {
|
|
|
+ $join->on('products.id', '=', 'pav_name_locale.product_id')
|
|
|
+ ->where('pav_name_locale.attribute_id', 2);
|
|
|
|
|
|
- if ($channel) {
|
|
|
- $query->where('pav_name.channel', $channel);
|
|
|
- }
|
|
|
+ if ($locale) {
|
|
|
+ $join->where('pav_name_locale.locale', $locale);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($channel) {
|
|
|
+ $join->where('pav_name_locale.channel', $channel);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // Fallback join for null locale/channel (default values)
|
|
|
+ $query->leftJoin('product_attribute_values as pav_name_fallback', function ($join) {
|
|
|
+ $join->on('products.id', '=', 'pav_name_fallback.product_id')
|
|
|
+ ->where('pav_name_fallback.attribute_id', 2)
|
|
|
+ ->whereNull('pav_name_fallback.locale')
|
|
|
+ ->whereNull('pav_name_fallback.channel');
|
|
|
+ });
|
|
|
+
|
|
|
+ $query->orderBy(\DB::raw("COALESCE({$prefix}pav_name_locale.text_value, {$prefix}pav_name_fallback.text_value)"), $direction)
|
|
|
+ ->orderBy('products.id', $direction)
|
|
|
+ ->select('products.*');
|
|
|
break;
|
|
|
|
|
|
case 'CREATED_AT':
|
|
|
$query->orderBy('products.created_at', $direction)
|
|
|
- ->orderBy('products.id', $direction);
|
|
|
+ ->orderBy('products.id', $direction);
|
|
|
break;
|
|
|
|
|
|
case 'UPDATED_AT':
|
|
|
$query->orderBy('products.updated_at', $direction)
|
|
|
- ->orderBy('products.id', $direction);
|
|
|
+ ->orderBy('products.id', $direction);
|
|
|
break;
|
|
|
|
|
|
case 'PRICE':
|
|
|
@@ -93,11 +113,11 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
|
|
|
$query->leftJoin('product_price_indices', function ($join) use ($customerGroup) {
|
|
|
$join->on('products.id', '=', 'product_price_indices.product_id')
|
|
|
- ->where('product_price_indices.customer_group_id', $customerGroup->id);
|
|
|
+ ->where('product_price_indices.customer_group_id', $customerGroup->id);
|
|
|
})
|
|
|
- ->orderBy('product_price_indices.min_price', $direction)
|
|
|
- ->orderBy('products.id', $direction)
|
|
|
- ->select('products.*');
|
|
|
+ ->orderBy('product_price_indices.min_price', $direction)
|
|
|
+ ->orderBy('products.id', $direction)
|
|
|
+ ->select('products.*');
|
|
|
break;
|
|
|
|
|
|
case 'ID':
|
|
|
@@ -130,17 +150,17 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
|
|
|
if (isset($filters['new'])) {
|
|
|
$query->leftJoin('product_flat', 'products.id', '=', 'product_flat.product_id')
|
|
|
- ->where('product_flat.new', filter_var($filters['new'], FILTER_VALIDATE_BOOLEAN))
|
|
|
- ->select('products.*')
|
|
|
- ->distinct('products.id');
|
|
|
+ ->where('product_flat.new', filter_var($filters['new'], FILTER_VALIDATE_BOOLEAN))
|
|
|
+ ->select('products.*')
|
|
|
+ ->distinct('products.id');
|
|
|
unset($filters['new']);
|
|
|
}
|
|
|
|
|
|
if (isset($filters['featured'])) {
|
|
|
$query->leftJoin('product_flat', 'products.id', '=', 'product_flat.product_id')
|
|
|
- ->where('product_flat.featured', filter_var($filters['featured'], FILTER_VALIDATE_BOOLEAN))
|
|
|
- ->select('products.*')
|
|
|
- ->distinct('products.id');
|
|
|
+ ->where('product_flat.featured', filter_var($filters['featured'], FILTER_VALIDATE_BOOLEAN))
|
|
|
+ ->select('products.*')
|
|
|
+ ->distinct('products.id');
|
|
|
unset($filters['featured']);
|
|
|
}
|
|
|
|
|
|
@@ -154,7 +174,7 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
|
|
|
if (! empty($filters['price_from']) || ! empty($filters['price_to'])) {
|
|
|
$from = isset($filters['price_from']) ? (float) $filters['price_from'] : null;
|
|
|
- $to = isset($filters['price_to']) ? (float) $filters['price_to'] : null;
|
|
|
+ $to = isset($filters['price_to']) ? (float) $filters['price_to'] : null;
|
|
|
|
|
|
$query->whereHas('attribute_values', function ($q) use ($from, $to) {
|
|
|
$q->where('attribute_id', 11);
|
|
|
@@ -182,7 +202,7 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
}
|
|
|
|
|
|
$attributeFilters[$attrCode] = [
|
|
|
- 'term' => $spec['match'] ?? $spec,
|
|
|
+ 'term' => $spec['match'] ?? $spec,
|
|
|
'matchType' => strtoupper($spec['match_type'] ?? ''),
|
|
|
];
|
|
|
}
|
|
|
@@ -194,23 +214,23 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
|
|
|
// Join all attribute tables needed for both products and variants
|
|
|
foreach ($attributeFilters as $attrCode => $filterData) {
|
|
|
- $productAlias = 'pav_' . $attrCode . '_product';
|
|
|
- $variantAlias = 'pav_' . $attrCode . '_variant';
|
|
|
+ $productAlias = 'pav_'.$attrCode.'_product';
|
|
|
+ $variantAlias = 'pav_'.$attrCode.'_variant';
|
|
|
|
|
|
// Join product attribute values
|
|
|
- $query->leftJoin('product_attribute_values as ' . $productAlias, function ($join) use ($productAlias, $attrCode) {
|
|
|
- $join->on('products.id', '=', $productAlias . '.product_id')
|
|
|
- ->whereIn($productAlias . '.attribute_id', function ($sub) use ($attrCode) {
|
|
|
- $sub->select('id')->from('attributes')->where('code', $attrCode);
|
|
|
- });
|
|
|
+ $query->leftJoin('product_attribute_values as '.$productAlias, function ($join) use ($productAlias, $attrCode) {
|
|
|
+ $join->on('products.id', '=', $productAlias.'.product_id')
|
|
|
+ ->whereIn($productAlias.'.attribute_id', function ($sub) use ($attrCode) {
|
|
|
+ $sub->select('id')->from('attributes')->where('code', $attrCode);
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
// Join variant attribute values
|
|
|
- $query->leftJoin('product_attribute_values as ' . $variantAlias, function ($join) use ($variantAlias, $attrCode) {
|
|
|
- $join->on('variants.id', '=', $variantAlias . '.product_id')
|
|
|
- ->whereIn($variantAlias . '.attribute_id', function ($sub) use ($attrCode) {
|
|
|
- $sub->select('id')->from('attributes')->where('code', $attrCode);
|
|
|
- });
|
|
|
+ $query->leftJoin('product_attribute_values as '.$variantAlias, function ($join) use ($variantAlias, $attrCode) {
|
|
|
+ $join->on('variants.id', '=', $variantAlias.'.product_id')
|
|
|
+ ->whereIn($variantAlias.'.attribute_id', function ($sub) use ($attrCode) {
|
|
|
+ $sub->select('id')->from('attributes')->where('code', $attrCode);
|
|
|
+ });
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -219,13 +239,13 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
// Check all filters against product attributes
|
|
|
$filterQuery->where(function ($productFilterQuery) use ($attributeFilters, $locale, $channel) {
|
|
|
foreach ($attributeFilters as $attrCode => $filterData) {
|
|
|
- $productAlias = 'pav_' . $attrCode . '_product';
|
|
|
+ $productAlias = 'pav_'.$attrCode.'_product';
|
|
|
$term = $filterData['term'];
|
|
|
$matchType = $filterData['matchType'];
|
|
|
$attributeType = $this->attributeTypeCache[$attrCode] ?? 'text';
|
|
|
|
|
|
- $productFilterQuery->where(function ($q) use ($term, $matchType, $locale, $channel, $productAlias, $attributeType) {
|
|
|
- $this->applyAttributeFilter($q, $term, $matchType, $locale, $channel, $productAlias, $attributeType);
|
|
|
+ $productFilterQuery->where(function ($q) use ($term, $matchType, $locale, $channel, $productAlias, $attributeType, $attrCode) {
|
|
|
+ $this->applyAttributeFilter($q, $term, $matchType, $locale, $channel, $productAlias, $attributeType, $attrCode);
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
@@ -233,13 +253,13 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
// OR check all filters against variant attributes
|
|
|
$filterQuery->orWhere(function ($variantFilterQuery) use ($attributeFilters, $locale, $channel) {
|
|
|
foreach ($attributeFilters as $attrCode => $filterData) {
|
|
|
- $variantAlias = 'pav_' . $attrCode . '_variant';
|
|
|
+ $variantAlias = 'pav_'.$attrCode.'_variant';
|
|
|
$term = $filterData['term'];
|
|
|
$matchType = $filterData['matchType'];
|
|
|
$attributeType = $this->attributeTypeCache[$attrCode] ?? 'text';
|
|
|
|
|
|
- $variantFilterQuery->where(function ($q) use ($term, $matchType, $locale, $channel, $variantAlias, $attributeType) {
|
|
|
- $this->applyAttributeFilter($q, $term, $matchType, $locale, $channel, $variantAlias, $attributeType);
|
|
|
+ $variantFilterQuery->where(function ($q) use ($term, $matchType, $locale, $channel, $variantAlias, $attributeType, $attrCode) {
|
|
|
+ $this->applyAttributeFilter($q, $term, $matchType, $locale, $channel, $variantAlias, $attributeType, $attrCode);
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
@@ -253,27 +273,26 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
'images',
|
|
|
'attribute_values',
|
|
|
'super_attributes',
|
|
|
- 'variants' => fn ($q) =>
|
|
|
- $q->without(['variants', 'super_attributes', 'attribute_values', 'attribute_family']),
|
|
|
+ 'variants' => fn ($q) => $q->without(['variants', 'super_attributes', 'attribute_values', 'attribute_family']),
|
|
|
]);
|
|
|
|
|
|
- $first = isset($args['first']) ? (int) $args['first'] : null;
|
|
|
- $last = isset($args['last']) ? (int) $args['last'] : null;
|
|
|
- $after = $args['after'] ?? null;
|
|
|
+ $first = isset($args['first']) ? (int) $args['first'] : null;
|
|
|
+ $last = isset($args['last']) ? (int) $args['last'] : null;
|
|
|
+ $after = $args['after'] ?? null;
|
|
|
$before = $args['before'] ?? null;
|
|
|
|
|
|
- $limit = $first ?? $last ?? 30;
|
|
|
+ $limit = $first ?? $last ?? 30;
|
|
|
$offset = 0;
|
|
|
|
|
|
if ($after) {
|
|
|
$decoded = base64_decode($after, true);
|
|
|
- $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
|
|
|
+ $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
|
|
|
}
|
|
|
|
|
|
if ($before) {
|
|
|
$decoded = base64_decode($before, true);
|
|
|
- $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
|
|
|
- $offset = max(0, $cursor - $limit);
|
|
|
+ $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
|
|
|
+ $offset = max(0, $cursor - $limit);
|
|
|
}
|
|
|
|
|
|
$total = (clone $query)
|
|
|
@@ -315,25 +334,27 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-
|
|
|
/**
|
|
|
* Apply attribute filter logic to a query based on attribute type
|
|
|
*/
|
|
|
- private function applyAttributeFilter($q, $term, $matchType, $locale, $channel, $alias, $attributeType = null)
|
|
|
+ private function applyAttributeFilter($q, $term, $matchType, $locale, $channel, $alias, $attributeType = null, $attrCode = null)
|
|
|
{
|
|
|
// Fallback to text_value if attribute type not provided
|
|
|
- if (!$attributeType) {
|
|
|
+ if (! $attributeType) {
|
|
|
$attributeType = 'text';
|
|
|
}
|
|
|
|
|
|
- // Apply locale filter if provided
|
|
|
- if ($locale) {
|
|
|
- $q->where($alias . '.locale', $locale);
|
|
|
+ // Only constrain locale/channel when the attribute is actually scoped —
|
|
|
+ // non-scoped attributes store NULL in locale/channel columns, so adding
|
|
|
+ // a WHERE locale = 'en' would exclude every match.
|
|
|
+ $scope = $attrCode ? ($this->attributeScopeCache[$attrCode] ?? null) : null;
|
|
|
+
|
|
|
+ if ($locale && ($scope->value_per_locale ?? false)) {
|
|
|
+ $q->where($alias.'.locale', $locale);
|
|
|
}
|
|
|
|
|
|
- // Apply channel filter if provided
|
|
|
- if ($channel) {
|
|
|
- $q->where($alias . '.channel', $channel);
|
|
|
+ if ($channel && ($scope->value_per_channel ?? false)) {
|
|
|
+ $q->where($alias.'.channel', $channel);
|
|
|
}
|
|
|
|
|
|
if ($matchType === 'PARTIAL') {
|
|
|
@@ -352,12 +373,12 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
'text', 'textarea' => 'text_value',
|
|
|
'select','multiselect','dropdown' => 'integer_value',
|
|
|
'decimal', 'price' => 'float_value',
|
|
|
- 'integer' => 'integer_value',
|
|
|
- 'boolean' => 'boolean_value',
|
|
|
+ 'integer' => 'integer_value',
|
|
|
+ 'boolean' => 'boolean_value',
|
|
|
'datetime' => 'datetime_value',
|
|
|
- 'date' => 'date_value',
|
|
|
- 'json' => 'json_value',
|
|
|
- default => 'text_value',
|
|
|
+ 'date' => 'date_value',
|
|
|
+ 'json' => 'json_value',
|
|
|
+ default => 'text_value',
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -367,7 +388,7 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
private function applyPartialFilter($q, $term, $alias, $attributeType)
|
|
|
{
|
|
|
$column = $this->getColumnForType($attributeType);
|
|
|
- $q->where($alias . '.' . $column, 'like', "%{$term}%");
|
|
|
+ $q->where($alias.'.'.$column, 'like', "%{$term}%");
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -376,18 +397,18 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
private function applyExactFilter($q, $term, $alias, $attributeType)
|
|
|
{
|
|
|
$column = $this->getColumnForType($attributeType);
|
|
|
-
|
|
|
+
|
|
|
if (is_string($term) && str_contains($term, ',')) {
|
|
|
$values = array_filter(array_map('trim', explode(',', $term)));
|
|
|
-
|
|
|
+
|
|
|
// Convert values based on attribute type
|
|
|
$convertedValues = $this->convertValuesForType($values, $attributeType);
|
|
|
-
|
|
|
- $q->whereIn($alias . '.' . $column, $convertedValues);
|
|
|
+
|
|
|
+ $q->whereIn($alias.'.'.$column, $convertedValues);
|
|
|
} else {
|
|
|
$convertedValue = $this->convertValueForType($term, $attributeType);
|
|
|
-
|
|
|
- $q->where($alias . '.' . $column, $convertedValue);
|
|
|
+
|
|
|
+ $q->where($alias.'.'.$column, $convertedValue);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -400,7 +421,7 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
'decimal', 'price' => (float) $value,
|
|
|
'integer' => (int) $value,
|
|
|
'boolean' => $value ? '1' : '0',
|
|
|
- default => $value,
|
|
|
+ default => $value,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -412,8 +433,8 @@ class ProductGraphQLProvider implements ProviderInterface
|
|
|
return match ($attributeType) {
|
|
|
'decimal', 'price' => array_map('floatval', $values),
|
|
|
'integer' => array_map('intval', $values),
|
|
|
- 'boolean' => array_map(fn($v) => (int) filter_var($v, FILTER_VALIDATE_BOOLEAN), $values),
|
|
|
- default => $values,
|
|
|
+ 'boolean' => array_map(fn ($v) => (int) filter_var($v, FILTER_VALIDATE_BOOLEAN), $values),
|
|
|
+ default => $values,
|
|
|
};
|
|
|
}
|
|
|
}
|