bianjunhui 3 dni temu
rodzic
commit
cfe25d6883

+ 0 - 1
config/api-platform.php

@@ -56,7 +56,6 @@ return [
         base_path('packages/Webkul/BagistoApi/src/Dto/ProductDetail/'),
         base_path('packages/Webkul/BagistoApi/src/Dto/CustomerOrder/'),
         base_path('packages/Webkul/BagistoApi/src/Dto/ProductSearch/'),
-        base_path('packages/Webkul/BagistoApi/src/Dto/CategoryProducts/'),
     ],
 
     'formats' => [

+ 0 - 38
packages/Webkul/BagistoApi/src/Dto/CategoryProducts/AppliedFilterDto.php

@@ -1,38 +0,0 @@
-<?php
-
-namespace Webkul\BagistoApi\Dto\CategoryProducts;
-
-use ApiPlatform\Metadata\ApiResource;
-
-#[ApiResource(
-    operations: [],
-    graphQlOperations: [],
-    paginationEnabled: false,
-)]
-class AppliedFilterDto
-{
-    /**
-     * Attribute code (e.g. "color", "size", "price").
-     */
-    public ?string $code = null;
-
-    /**
-     * Attribute display label (localized).
-     */
-    public ?string $label = null;
-
-    /**
-     * Selected option values. For select attributes this holds the option id;
-     * for price this holds the selected price range.
-     *
-     * @var list<string>
-     */
-    public array $values = [];
-
-    /**
-     * Human-readable labels for the selected options (when resolvable).
-     *
-     * @var list<string>
-     */
-    public array $value_labels = [];
-}

+ 0 - 20
packages/Webkul/BagistoApi/src/Dto/CategoryProducts/CategoryFilterEdgeDto.php

@@ -1,20 +0,0 @@
-<?php
-
-namespace Webkul\BagistoApi\Dto\CategoryProducts;
-
-use ApiPlatform\Metadata\ApiProperty;
-use ApiPlatform\Metadata\ApiResource;
-use Webkul\BagistoApi\Models\Filter\Attribute;
-
-#[ApiResource(
-    operations: [],
-    graphQlOperations: [],
-    paginationEnabled: false,
-)]
-class CategoryFilterEdgeDto
-{
-    public ?string $cursor = null;
-
-    #[ApiProperty(readableLink: true)]
-    public ?Attribute $node = null;
-}

+ 0 - 0
packages/Webkul/BagistoApi/src/Dto/CategoryProducts/CategoryProductsDto.php


+ 2 - 21
packages/Webkul/BagistoApi/src/Models/CategoryProducts.php

@@ -5,8 +5,6 @@ namespace Webkul\BagistoApi\Models;
 use ApiPlatform\Metadata\ApiProperty;
 use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\GraphQl\Query;
-use Webkul\BagistoApi\Dto\CategoryProducts\AppliedFilterDto;
-use Webkul\BagistoApi\Dto\CategoryProducts\CategoryFilterEdgeDto;
 use Webkul\BagistoApi\Dto\ProductSearch\ProductSearchEdgeDto;
 use Webkul\BagistoApi\Resolver\CategoryProductsResolver;
 
@@ -17,7 +15,7 @@ use Webkul\BagistoApi\Resolver\CategoryProductsResolver;
         new Query(
             resolver: CategoryProductsResolver::class,
             args: [
-                'categorySlug' => [
+                'slug' => [
                     'type'        => 'String!',
                     'description' => 'Category slug to list products for.',
                 ],
@@ -34,7 +32,7 @@ use Webkul\BagistoApi\Resolver\CategoryProductsResolver;
             ],
             read: false,
             paginationEnabled: false,
-            description: 'List products in a category with applied/available attribute filters.',
+            description: 'List products in a category with pagination and filters. Use categoryAttributeFilters for the filter/facet data.',
         ),
     ],
     normalizationContext: ['skip_null_values' => false],
@@ -53,21 +51,4 @@ class CategoryProducts
      */
     #[ApiProperty(readableLink: true)]
     public array $products = [];
-
-    /**
-     * Filters the client has currently applied (resolved from the `filter` arg).
-     *
-     * @var list<AppliedFilterDto>
-     */
-    #[ApiProperty(readableLink: true)]
-    public array $applied_filters = [];
-
-    /**
-     * All filterable attributes for the category, each with option-level
-     * product counts — equivalent to the `categoryAttributeFilters` query.
-     *
-     * @var list<CategoryFilterEdgeDto>
-     */
-    #[ApiProperty(readableLink: true)]
-    public array $available_filters = [];
 }

+ 1 - 1
packages/Webkul/BagistoApi/src/Models/Filter/Attribute.php

@@ -16,7 +16,7 @@ use Webkul\BagistoApi\State\FilterableAttributesProvider;
         new QueryCollection(
             provider: FilterableAttributesProvider::class,
             args: [
-                'categorySlug' => ['type' => 'String', 'required' => false],
+                'slug'         => ['type' => 'String', 'required' => false],
                 'filter'       => ['type' => 'String', 'description' => 'JSON attribute filters to recompute option product counts against (faceted search).'],
                 'first'        => ['type' => 'Int', 'description' => 'Number of items to return from the start'],
                 'last'         => ['type' => 'Int', 'description' => 'Number of items to return from the end'],

+ 5 - 172
packages/Webkul/BagistoApi/src/Resolver/CategoryProductsResolver.php

@@ -3,33 +3,28 @@
 namespace Webkul\BagistoApi\Resolver;
 
 use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface;
-use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\State\Pagination\PaginatorInterface;
 use Illuminate\Support\Facades\DB;
-use Webkul\BagistoApi\Dto\CategoryProducts\AppliedFilterDto;
-use Webkul\BagistoApi\Dto\CategoryProducts\CategoryFilterEdgeDto;
 use Webkul\BagistoApi\Dto\ProductSearch\ProductSearchEdgeDto;
 use Webkul\BagistoApi\Models\CategoryProducts;
-use Webkul\BagistoApi\State\FilterableAttributesProvider;
 use Webkul\BagistoApi\State\ProductGraphQLProvider;
 
 class CategoryProductsResolver implements QueryItemResolverInterface
 {
     public function __construct(
         private readonly ProductGraphQLProvider $productProvider,
-        private readonly FilterableAttributesProvider $filterProvider,
     ) {}
 
     public function __invoke(?object $item, array $context): object
     {
         $args = $context['args'] ?? [];
 
-        $categorySlug = $args['categorySlug'] ?? null;
+        $categorySlug = $args['slug'] ?? null;
 
-        // When a collection field (appliedFilters / availableFilters) is empty,
-        // API Platform's ResolverFactory treats the empty array as "not yet
-        // fetched" and re-invokes the item resolver for that sub-field. Guard
-        // against this by returning an empty result for non-root field names.
+        // When the "products" collection field is empty, API Platform's
+        // ResolverFactory treats the empty array as "not yet fetched" and
+        // re-invokes the item resolver for that sub-field. Guard against this by
+        // returning an empty result for non-root field names.
         $fieldName = $context['info']->fieldName ?? 'categoryProducts';
         if (! $categorySlug || $fieldName !== 'categoryProducts') {
             return new CategoryProducts;
@@ -43,8 +38,6 @@ class CategoryProductsResolver implements QueryItemResolverInterface
             throw new \RuntimeException("Category not found for slug: {$categorySlug}");
         }
 
-        // 1. Products — reuse the product provider with category + filters and
-        //    default price-ascending order.
         $filters = $this->parseFilter($args['filter'] ?? null);
         $filters['category_id'] = (int) $categoryId;
 
@@ -78,32 +71,6 @@ class CategoryProductsResolver implements QueryItemResolverInterface
             $result->products[] = $edge;
         }
 
-        // 2. Available filters — reuse the filter provider (returns attributes
-        //    with option-level product counts for the category). The client's
-        //    selected filters are passed through so facet counts reflect the
-        //    remaining result set (excluding each attribute's own selection).
-        $filterArgs = [
-            'categorySlug' => $categorySlug,
-            'filter'       => $args['filter'] ?? null,
-        ];
-
-        $filterPaginator = $this->filterProvider->provide(
-            new GetCollection,
-            [],
-            ['source' => null, 'args' => $filterArgs, 'info' => null]
-        );
-
-        foreach ($filterPaginator as $index => $attribute) {
-            $edge = new CategoryFilterEdgeDto;
-            $edge->cursor = base64_encode((string) $index);
-            $edge->node = $attribute;
-            $result->available_filters[] = $edge;
-        }
-
-        // 3. Applied filters — resolve the client's current selection into a
-        //    structured list (code + label + option ids + option labels).
-        $result->applied_filters = $this->buildAppliedFilters($filters);
-
         return $result;
     }
 
@@ -151,138 +118,4 @@ class CategoryProductsResolver implements QueryItemResolverInterface
 
         return is_array($filter) ? $filter : [];
     }
-
-    /**
-     * Build the structured "applied filters" list from the raw filter map.
-     *
-     * @param  array<string, mixed>  $filters
-     * @return list<AppliedFilterDto>
-     */
-    private function buildAppliedFilters(array $filters): array
-    {
-        if (empty($filters)) {
-            return [];
-        }
-
-        // Non-attribute keys handled specially (price range) or ignored
-        // (already folded into the product query / pagination).
-        $ignored = ['category_id', 'type', 'sku', 'new', 'featured', 'pageSize', 'first', 'last', 'after', 'before'];
-
-        $price = [];
-        if (isset($filters['price_from'])) {
-            $price['from'] = (float) $filters['price_from'];
-            unset($filters['price_from']);
-        }
-        if (isset($filters['price_to'])) {
-            $price['to'] = (float) $filters['price_to'];
-            unset($filters['price_to']);
-        }
-
-        $applied = [];
-
-        foreach ($filters as $code => $spec) {
-            if (in_array($code, $ignored, true)) {
-                continue;
-            }
-
-            $values = $this->extractValues($spec);
-            if (empty($values)) {
-                continue;
-            }
-
-            $applied[] = $this->makeFilterDto($code, $values);
-        }
-
-        if (! empty($price)) {
-            $dto = new AppliedFilterDto;
-            $dto->code = 'price';
-
-            $priceAttribute = DB::table('attributes')->where('code', 'price')->first();
-            $dto->label = $priceAttribute?->admin_name ?? 'Price';
-
-            $dto->values = [];
-            $dto->value_labels = [];
-
-            if (isset($price['from'])) {
-                $dto->values[] = (string) $price['from'];
-                $dto->value_labels[] = (string) $price['from'];
-            }
-
-            if (isset($price['to'])) {
-                $dto->values[] = (string) $price['to'];
-                $dto->value_labels[] = (string) $price['to'];
-            }
-
-            $applied[] = $dto;
-        }
-
-        return $applied;
-    }
-
-    /**
-     * Extract the selected option id(s) from a filter spec. Accepts:
-     *   "2", 2, ["2","3"], {"match": "2"}, {"match": "2,3"}
-     *
-     * @return list<string>
-     */
-    private function extractValues(mixed $spec): array
-    {
-        $raw = $spec;
-
-        if (is_array($spec)) {
-            if (isset($spec['match'])) {
-                $raw = $spec['match'];
-            } elseif (array_is_list($spec)) {
-                $raw = $spec;
-            }
-        }
-
-        if (is_array($raw)) {
-            $values = [];
-            foreach ($raw as $v) {
-                foreach (explode(',', (string) $v) as $part) {
-                    $part = trim($part);
-                    if ($part !== '') {
-                        $values[] = $part;
-                    }
-                }
-            }
-
-            return array_values(array_unique($values));
-        }
-
-        $values = array_filter(array_map('trim', explode(',', (string) $raw)));
-
-        return array_values(array_unique($values));
-    }
-
-    /**
-     * Build an AppliedFilterDto with the attribute label and option labels
-     * resolved from the database.
-     *
-     * @param  list<string>  $values
-     */
-    private function makeFilterDto(string $code, array $values): AppliedFilterDto
-    {
-        $dto = new AppliedFilterDto;
-        $dto->code = $code;
-        $dto->values = $values;
-
-        $attribute = DB::table('attributes')->where('code', $code)->first();
-        if ($attribute) {
-            $dto->label = $attribute->admin_name;
-
-            $optionLabels = DB::table('attribute_options')
-                ->where('attribute_id', $attribute->id)
-                ->whereIn('id', $values)
-                ->pluck('admin_name', 'id');
-
-            $dto->value_labels = [];
-            foreach ($values as $value) {
-                $dto->value_labels[] = $optionLabels[$value] ?? $value;
-            }
-        }
-
-        return $dto;
-    }
 }

+ 61 - 8
packages/Webkul/BagistoApi/src/State/FilterableAttributesProvider.php

@@ -25,7 +25,7 @@ class FilterableAttributesProvider implements ProviderInterface
         $args = $context['args'] ?? [];
         $info = $context['info'] ?? null;
 
-        $categorySlug = $args['categorySlug'] ?? null;
+        $categorySlug = $args['slug'] ?? $args['categorySlug'] ?? null;
 
         // Active attribute filters (faceted search). When a filter is already
         // selected for one attribute, the product counts of every other
@@ -244,10 +244,12 @@ class FilterableAttributesProvider implements ProviderInterface
     }
 
     /**
-     * Parse the `filter` arg (JSON string or array) into a map of attribute
-     * code => filter spec. Matches the format used by ProductGraphQLProvider:
-     *   {"color":{"match":"2","match_type":"exact"}}
-     *   {"color":"2"}
+     * Parse the `filter` arg (JSON string or array) into a map of active filters.
+     *
+     * Attribute filters are keyed by attribute code; the price range is stored
+     * under the special "__price__" key so it can be applied to facet counts too.
+     * Matches the format used by ProductGraphQLProvider:
+     *   {"color":{"match":"2"},"price_from":10,"price_to":200}
      *
      * @return array<string, array<string, mixed>>
      */
@@ -267,9 +269,24 @@ class FilterableAttributesProvider implements ProviderInterface
             return [];
         }
 
+        // Non-attribute keys handled elsewhere (category, pagination, etc.) must
+        // not be treated as attribute filters for facets.
+        $ignored = ['category_id', 'type', 'sku', 'new', 'featured', 'pageSize', 'first', 'last', 'after', 'before'];
+
         $active = [];
 
+        $priceFrom = isset($filter['price_from']) ? (float) $filter['price_from'] : null;
+        $priceTo = isset($filter['price_to']) ? (float) $filter['price_to'] : null;
+
+        if ($priceFrom !== null || $priceTo !== null) {
+            $active['__price__'] = ['from' => $priceFrom, 'to' => $priceTo];
+        }
+
         foreach ($filter as $code => $spec) {
+            if (in_array($code, $ignored, true) || $code === 'price_from' || $code === 'price_to') {
+                continue;
+            }
+
             if (is_array($spec)) {
                 if (isset($spec['match'])) {
                     $active[$code] = [
@@ -291,9 +308,9 @@ class FilterableAttributesProvider implements ProviderInterface
      * Apply the active filters of other attributes to a facet count query.
      *
      * For each active attribute filter (excluding the attribute currently being
-     * counted), add an EXISTS sub-query restricting to products whose attribute
-     * value matches the selection. This keeps the counts consistent with the
-     * product listing that used the same filters.
+     * counted) and the price range, add an EXISTS sub-query restricting to
+     * products that match. This keeps the counts consistent with the product
+     * listing that used the same filters.
      *
      * @param  array<string, array<string, mixed>>  $activeFilters
      */
@@ -305,7 +322,16 @@ class FilterableAttributesProvider implements ProviderInterface
 
         $attributeTypes = $this->getAttributeTypeCache();
 
+        // Price range (always applied, regardless of the attribute being counted).
+        if (isset($activeFilters['__price__'])) {
+            $this->applyPriceFilter($countQuery, $activeFilters['__price__']);
+        }
+
         foreach ($activeFilters as $code => $spec) {
+            if ($code === '__price__') {
+                continue;
+            }
+
             // Exclude the current attribute so its own options remain selectable.
             if ($code === $currentCode) {
                 continue;
@@ -335,6 +361,33 @@ class FilterableAttributesProvider implements ProviderInterface
         }
     }
 
+    /**
+     * Apply the price range to a facet count query. Mirrors the product listing
+     * price filter: products whose price attribute (id 11) float_value falls in
+     * the requested range.
+     *
+     * @param  array{from: ?float, to: ?float}  $price
+     */
+    private function applyPriceFilter($countQuery, array $price): void
+    {
+        $from = $price['from'] ?? null;
+        $to = $price['to'] ?? null;
+
+        $countQuery->whereIn('pav.product_id', function ($sub) use ($from, $to) {
+            $sub->select('product_id')
+                ->from('product_attribute_values as pav_price')
+                ->where('pav_price.attribute_id', 11);
+
+            if ($from !== null && $to !== null) {
+                $sub->whereBetween('pav_price.float_value', [$from, $to]);
+            } elseif ($from !== null) {
+                $sub->where('pav_price.float_value', '>=', $from);
+            } elseif ($to !== null) {
+                $sub->where('pav_price.float_value', '<=', $to);
+            }
+        });
+    }
+
     /**
      * Get the attribute code => type map, cached for the request.
      *