1616
1717use Gally \SyliusPlugin \Event \GridFilterUpdateEvent ;
1818use Gally \SyliusPlugin \Grid \Filter \Type \SelectFilterType ;
19+ use Gally \SyliusPlugin \Search \ActiveFilterResolver ;
1920use Gally \SyliusPlugin \Search \Aggregation \Aggregation ;
2021use Gally \SyliusPlugin \Search \Aggregation \AggregationOption ;
2122use Sylius \Bundle \GridBundle \Form \Type \Filter \BooleanFilterType ;
2425use Sylius \Component \Locale \Context \LocaleContextInterface ;
2526use Sylius \Component \Taxonomy \Model \TaxonInterface ;
2627use Symfony \Component \Form \AbstractType ;
28+ use Symfony \Component \Form \Extension \Core \Type \HiddenType ;
2729use Symfony \Component \Form \Extension \Core \Type \RangeType ;
2830use Symfony \Component \Form \FormBuilderInterface ;
2931use Symfony \Component \HttpFoundation \RequestStack ;
@@ -49,6 +51,18 @@ public function __construct(
4951
5052 public function buildForm (FormBuilderInterface $ builder , array $ options ): void
5153 {
54+ $ isTaxonPage = $ this ->isTaxonPage ();
55+ $ categoryFieldAdded = false ;
56+
57+ if ($ isTaxonPage && !$ this ->hasCategoryAggregation ()) {
58+ // Gally doesn't always return a "category" aggregation on taxon listing pages (e.g. a
59+ // leaf taxon with no sub-taxons), so there's no signal telling us where it would
60+ // normally sit in the facet order. Placed first, right below the active filters, in
61+ // that case.
62+ $ this ->addCategoryTaxonomyAnchor ($ builder );
63+ $ categoryFieldAdded = true ;
64+ }
65+
5266 foreach ($ this ->aggregations as $ aggregation ) {
5367 switch ($ aggregation ->getType ()) {
5468 case 'slider ' :
@@ -95,32 +109,122 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
95109 /* @var AggregationOption $option */
96110 $ choices [$ option ->getLabel ()] = $ option ->getId ();
97111 }
98- $ options = [
99- 'block_prefix ' => 'sylius_gally_filter_checkbox ' ,
100- 'label ' => $ aggregation ->getLabel (),
101- 'choices ' => $ choices ,
102- 'expanded ' => true ,
103- 'multiple ' => true ,
104- 'search_url ' => $ this ->buildSearchUrl ($ aggregation ->getField ()),
105- 'has_more ' => $ aggregation ->hasMore (),
106- ];
107112 $ builder ->add (
108113 $ aggregation ->getField (),
109114 SelectFilterType::class,
110- $ options
115+ [
116+ 'block_prefix ' => 'sylius_gally_filter_checkbox ' ,
117+ 'label ' => $ aggregation ->getLabel (),
118+ 'choices ' => $ choices ,
119+ 'expanded ' => true ,
120+ 'multiple ' => true ,
121+ 'search_url ' => $ this ->buildSearchUrl ($ aggregation ->getField ()),
122+ 'has_more ' => $ aggregation ->hasMore (),
123+ ]
111124 );
112125 break ;
126+ case 'category ' :
127+ // A taxon-scoped request is still a category browsing context even when
128+ // Gally does return this aggregation (e.g. a parent taxon whose products
129+ // span several sub-taxons): direct taxon navigation (native component) is
130+ // used either way, never the Gally query-string filter, which would produce
131+ // links "filtering" the current taxon page instead of going to the sub-taxon
132+ // page directly.
133+ if ($ isTaxonPage ) {
134+ $ this ->addCategoryTaxonomyAnchor ($ builder );
135+ } else {
136+ $ this ->addCategoryField ($ builder , $ aggregation );
137+ }
138+ $ categoryFieldAdded = true ;
139+ break ;
113140 default :
114141 break ;
115142 }
116143 }
144+
145+ // Gally excludes the category aggregation from the response as soon as a category filter is
146+ // active, so once selected the field above never reappears. Without this, submitting any
147+ // other filter would silently drop the category criteria.
148+ if (!$ categoryFieldAdded ) {
149+ $ categoryId = $ this ->getCurrentCategoryId ();
150+ if (null !== $ categoryId ) {
151+ $ builder ->add (ActiveFilterResolver::CATEGORY_FIELD , HiddenType::class, ['data ' => $ categoryId ]);
152+ }
153+ }
117154 }
118155
119156 public function onFilterUpdate (GridFilterUpdateEvent $ event ): void
120157 {
121158 $ this ->aggregations = $ event ->getAggregations ();
122159 }
123160
161+ private function isTaxonPage (): bool
162+ {
163+ $ slug = $ this ->requestStack ->getCurrentRequest ()?->attributes->get ('slug ' );
164+
165+ return \is_string ($ slug ) && '' !== $ slug ;
166+ }
167+
168+ private function hasCategoryAggregation (): bool
169+ {
170+ foreach ($ this ->aggregations as $ aggregation ) {
171+ if ('category ' === $ aggregation ->getType ()) {
172+ return true ;
173+ }
174+ }
175+
176+ return false ;
177+ }
178+
179+ /**
180+ * On taxon listing pages Gally never returns a "category" aggregation at all (the taxon
181+ * already scopes the search), so browsing there is handled by the native Sylius taxonomy
182+ * component instead (proper direct links, plus a "go level up" link), inserted as a plain
183+ * form field so it renders through the same form_widget(form) call and keeps its position
184+ * in the facet order (see buildForm()). The block rendering it lives in checkbox.html.twig
185+ * (block "sylius_gally_filter_category_taxonomy_row").
186+ */
187+ private function addCategoryTaxonomyAnchor (FormBuilderInterface $ builder ): void
188+ {
189+ $ builder ->add (ActiveFilterResolver::CATEGORY_FIELD , HiddenType::class, [
190+ 'block_prefix ' => 'sylius_gally_filter_category_taxonomy ' ,
191+ ]);
192+ }
193+
194+ /**
195+ * Rendered through the same SelectFilterType/autosubmit mechanism as the other facets (real
196+ * radio inputs, native browser change event), just themed to look like plain links via CSS
197+ * (see the "sylius_gally_filter_category" block and #searchbar .category-links rule in
198+ * filters.css) since a category is single-select, not a combinable criterion.
199+ */
200+ private function addCategoryField (FormBuilderInterface $ builder , Aggregation $ aggregation ): void
201+ {
202+ $ choices = [];
203+ foreach ($ aggregation ->getOptions () as $ option ) {
204+ /* @var AggregationOption $option */
205+ $ choices [$ option ->getLabel ()] = $ option ->getId ();
206+ }
207+
208+ $ builder ->add (ActiveFilterResolver::CATEGORY_FIELD , SelectFilterType::class, [
209+ 'block_prefix ' => 'sylius_gally_filter_category ' ,
210+ 'label ' => $ aggregation ->getLabel (),
211+ 'choices ' => $ choices ,
212+ 'expanded ' => true ,
213+ 'multiple ' => false ,
214+ 'data ' => $ this ->getCurrentCategoryId (),
215+ ]);
216+ }
217+
218+ private function getCurrentCategoryId (): ?string
219+ {
220+ $ queryParameters = $ this ->requestStack ->getCurrentRequest ()?->query->all () ?? [];
221+ $ criteria = \is_array ($ queryParameters ['criteria ' ] ?? null ) ? $ queryParameters ['criteria ' ] : [];
222+ $ gallyCriteria = \is_array ($ criteria ['gally ' ] ?? null ) ? $ criteria ['gally ' ] : [];
223+ $ categoryId = $ gallyCriteria [ActiveFilterResolver::CATEGORY_FIELD ] ?? null ;
224+
225+ return \is_string ($ categoryId ) && '' !== $ categoryId ? $ categoryId : null ;
226+ }
227+
124228 private function buildSearchUrl (string $ field ): string
125229 {
126230 $ request = $ this ->requestStack ->getCurrentRequest ();
0 commit comments