Skip to content

quick search - #260

Merged
jonasdekeukelaere merged 8 commits into
masterfrom
quick-search
Oct 16, 2026
Merged

jonasdekeukelaere merged 8 commits into
masterfrom
quick-search

Conversation

@jonasdekeukelaere

@jonasdekeukelaere jonasdekeukelaere commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Cmd+K / Ctrl+K opens a search modal on every page that extends the bundle's base.html.twig. Results come from providers: the bundle ships one for the main menu, applications add their own. Everything is shown in one list ranked on relevance, each result with a badge naming its source. Ported from the LA Systems app.

  • Requires symfony/ux-live-component.
  • QuickSearch service merges the results of all tagged QuickSearchProviders, sorted on relevanceScore (0–1) × the provider's multiplier, ties alphabetical. Every autoconfigured implementation is tagged by the bundle extension (registerForAutoconfiguration).
  • Scorers: AlphabeticalScorer and GivenOrderScorer (1, 0.9, 0.81, …).
  • MenuQuickSearchProvider: translated menu labels, submenus as "Parent › Child", hidden items skipped.
  • SumoCodersFrameworkCore:QuickSearch live component, search-modal Stimulus controller, modal in a {% block quick_search %} in base.html.twig (override it empty to switch off).
  • Docs: docs/quick-search.md, stimulus.md, index, UPGRADE.md ("To 19.2": register the controller, check the live-component route).

Tested with unit tests (scorers, QuickSearch, menu provider) and in the LA Systems app via a path repository: menu and app-provider results, re-rendering through /_components, one modal per page.

Skeleton follow-up: sumocoders/application-skeleton#177 (sumocoders/application-skeleton#177) registers the search-modal controller in new projects and raises the bundle constraint to ^19.2; merge it after this is released.

Summary by Sourcery

Add an extensible, ranked quick-search experience to bundle-based application pages.

New Features:

  • Add a Cmd+K/Ctrl+K quick-search modal to bundle layouts, combining menu and application-provided results with relevance ranking and source badges.
  • Provide an extensible provider API with automatic service tagging, menu search support, configurable scoring, and provider weighting.
  • Add a live component and Stimulus interactions for debounced searching, keyboard navigation, and modal lifecycle handling.

Enhancements:

  • Support translated, nested, and visibility-aware menu search results with Bootstrap icon fallbacks.

Build:

  • Add Symfony UX Live Component as a runtime dependency.

Documentation:

  • Document quick-search usage, provider registration, ranking, configuration, troubleshooting, and upgrade steps.

Tests:

  • Add unit coverage for search ranking, scoring strategies, and menu-provider behavior.

Chores:

  • Add English and Dutch translations for the quick-search interface.

@sourcery-ai

sourcery-ai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a provider-extensible Cmd+K/Ctrl+K search experience across bundle base pages, combining translated menu and application results through relevance-weighted ranking, a Symfony UX Live Component, and Stimulus modal controls, with installation documentation and unit tests.

Sequence diagram for quick search query and ranked results

sequenceDiagram
    actor User
    participant Stimulus as search-modal
    participant LiveComponent as QuickSearchComponent
    participant Service as QuickSearch
    participant Menu as MenuQuickSearchProvider
    participant App as ApplicationQuickSearchProvider

    User->>Stimulus: open()
    Stimulus->>LiveComponent: update query
    LiveComponent->>Service: search(query, limit)
    Service->>Menu: search(query, limit)
    Service->>App: search(query, limit)
    Menu-->>Service: QuickSearchResult list
    App-->>Service: QuickSearchResult list
    Service->>Service: getMultiplier()
    Service-->>LiveComponent: ranked results
    LiveComponent-->>User: render results and source badges
Loading

File-Level Changes

Change Details Files
Adds the provider-based quick-search domain and relevance ranking.
  • Introduces the provider contract and result value object.
  • Merges provider results using score × multiplier, with alphabetical tie-breaking.
  • Adds alphabetical and given-order scoring utilities.
  • Registers bundle and application providers through autoconfiguration and tagged iteration.
src/Service/QuickSearch/Provider/QuickSearchProvider.php
src/Service/QuickSearch/QuickSearch.php
src/Service/QuickSearch/Scorer/AlphabeticalScorer.php
src/Service/QuickSearch/Scorer/GivenOrderScorer.php
src/ValueObject/QuickSearchResult.php
src/DependencyInjection/SumoCodersFrameworkCoreExtension.php
config/services.php
Implements main-menu search with translated, hierarchical navigation results.
  • Traverses the main menu recursively, skips hidden items and non-link submenu parents.
  • Matches translated labels and emits parent › child paths with icons and navigation badges.
  • Applies alphabetical relevance scoring and a menu multiplier.
src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php
Adds the live search modal and keyboard interaction to the bundle base layout.
  • Creates the live component with a two-character threshold and 300 ms debounced query updates.
  • Renders ranked results with links, icons, and source badges inside an overridable quick-search block.
  • Adds Cmd+K/Ctrl+K opening, focus management, arrow-key navigation, Escape/Turbo cleanup, and Bootstrap modal integration.
src/Twig/Components/QuickSearch.php
templates/components/QuickSearch.html.twig
templates/quick_search.html.twig
templates/base.html.twig
assets-public/controllers/search_modal_controller.js
Documents and validates installation, extension, and ranking behavior.
  • Requires Symfony UX Live Component and documents its route and Stimulus controller setup.
  • Adds usage, provider, ranking, troubleshooting, and upgrade guidance plus translations.
  • Adds unit coverage for scorers, aggregation, and menu-provider behavior.
composer.json
UPGRADE.md
docs/quick-search.md
docs/index.md
docs/stimulus.md
docs/memory/testing.md
translations/messages.en.yml
translations/messages.nl.yml
tests/Service/QuickSearch/Provider/FakeQuickSearchProvider.php
tests/Service/QuickSearch/Provider/MenuQuickSearchProviderTest.php
tests/Service/QuickSearch/QuickSearchTest.php
tests/Service/QuickSearch/Scorer/ScorerTest.php
CLAUDE.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 7 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php" line_range="63-65" />
<code_context>
+            }
+
+            $path = [...$parents, $this->translateLabel($item)];
+            $url = $item->getUri();
+            // Parents of a submenu link to '#'; only their children lead somewhere.
+            if ($url !== null && $url !== '#' && mb_stripos(implode(' ', $path), $query) !== false) {
+                $icon = $item->getLabelAttribute('icon');
+                $matches[] = [
</code_context>
<issue_to_address>
**Route-based menu links disappear**

When a menu item is configured with a Symfony `route` instead of a literal `uri`, `getUri()` does not resolve a menu item's configured Symfony route, so `$url` is null and the condition skips that item; route-based links shown in the menu therefore do not appear in quick-search results.

Resolve route-configured menu items to their generated URLs before filtering and returning results.
</issue_to_address>

### Comment 2
<location path="src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php" line_range="32" />
<code_context>
+        $matches = [];
+        $this->collect($this->menuBuilder->createMainMenu(), [], $query, $matches);
+
+        // All matches, not capped at $limit: the menu is short and every hit is useful.
+        $scores = AlphabeticalScorer::score(array_column($matches, 'text'));
+        $source = $this->translator->trans('core.quick_search.source.navigation');
</code_context>
<issue_to_address>
**Search returns too many results**

When more menu entries match than the requested limit, `MenuQuickSearchProvider::search()` scores every match and returns them all, so `QuickSearch::search()` exceeds the per-provider limit and live searches do unnecessary scoring work.

Apply the requested limit to menu matches and score only the results that will be returned.

Also at `src/Service/QuickSearch/Scorer/AlphabeticalScorer.php:23`.
</issue_to_address>

### Comment 3
<location path="src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php" line_range="91" />
<code_context>
+        return ucfirst($this->translator->trans(
+            $item->getLabel(),
+            is_array($parameters) ? $parameters : [],
+            is_string($domain) ? $domain : 'messages',
+        ));
+    }
</code_context>
<issue_to_address>
**Search labels differ from navigation**

When a menu item sets `translation_domain` to `false`, `translateLabel()` replaces `false` with `messages`, so the provider translates a label that the menu renders literally; search results can differ from the navigation and fail to match its text.

Preserve the `false` translation domain and return the label without translating it.

Also at `src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php:92`.
</issue_to_address>

### Comment 4
<location path="src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php" line_range="70" />
<code_context>
+                $matches[] = [
+                    'text' => implode(' › ', $path),
+                    'url' => $url,
+                    'icon' => is_string($icon) ? str_replace('bi bi-', '', $icon) : '',
+                ];
+            }
</code_context>
<issue_to_address>
**Search results show broken icons**

When a menu item omits its icon or uses a documented Font Awesome icon, the provider stores an empty string for a missing icon and passes Font Awesome classes through unchanged, while the results template always prefixes the value with `bi bi-`; search results therefore show a broken icon instead of the navigation icon.

Provide the navigation fallback for missing icons and render Font Awesome classes without the Bootstrap icon prefix.

Also at `templates/components/QuickSearch.html.twig:16`.
</issue_to_address>

### Comment 5
<location path="src/Service/QuickSearch/QuickSearch.php" line_range="32" />
<code_context>
+        $results = [];
+        $scores = [];
+        foreach ($this->providers as $provider) {
+            foreach ($provider->search($query, $limit) as $result) {
+                $results[] = $result;
+                $scores[] = $result->relevanceScore * $provider->getMultiplier();
</code_context>
<issue_to_address>
**One provider blocks all results**

When a registered provider's dependency is slow or throws during a search, the provider call runs synchronously inside `QuickSearch::search()` with no timeout or per-provider error handling, so a slow or failing provider prevents results from other sources from being returned and the user sees a stalled or failed update.

Require providers to bound dependency calls and isolate/log provider failures so one source cannot abort the whole search.
</issue_to_address>

### Comment 6
<location path="src/ValueObject/QuickSearchResult.php" line_range="22" />
<code_context>
+        public float $relevanceScore = 0.5,
+    ) {
+        assert(
+            $relevanceScore >= 0.0 && $relevanceScore <= 1.0,
+            'The relevance score of a quick search result must be between 0 and 1.',
+        );
</code_context>
<issue_to_address>
**Invalid scores distort result ranking**

When production runs with assertions disabled and a provider supplies a score outside 0–1, `QuickSearchResult` accepts the out-of-range score, and `QuickSearch::search()` uses it directly for sorting, so the provider's result ranks incorrectly.

Validate the relevance score at runtime and reject values outside 0–1.
</issue_to_address>

### Comment 7
<location path="src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php" line_range="88-92" />
<code_context>
+        // @mago-expect analysis:mixed-assignment
+        $domain = $item->getExtra('translation_domain', 'messages');
+
+        return ucfirst($this->translator->trans(
+            $item->getLabel(),
+            is_array($parameters) ? $parameters : [],
+            is_string($domain) ? $domain : 'messages',
+        ));
+    }
+}
</code_context>
<issue_to_address>
**Accented menu labels stay lowercase**

When a displayed menu label begins with a lowercase non-ASCII character, `ucfirst()` only uppercases ASCII, so translated labels beginning with a lowercase accented character are not capitalized in search results, unlike the menu rendering; callers see inconsistent labels.

Use Unicode-aware capitalization matching the Twig `ucfirst` behavior.
</issue_to_address>

Sourcery assessment

Approval pending. 6 findings to address first.

Blocking findings: src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php:65, src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php:32, src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php:91, src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php:70, src/Service/QuickSearch/QuickSearch.php:32, and 1 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php
Comment thread src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php
Comment thread src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php
Comment thread src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php Outdated
Comment thread src/Service/QuickSearch/QuickSearch.php
Comment thread src/ValueObject/QuickSearchResult.php
Comment thread src/Service/QuickSearch/Provider/MenuQuickSearchProvider.php
import { Controller } from '@hotwired/stimulus'
import { Modal } from 'bootstrap'

// Opens the quick search modal (see docs/quick-search.md) on Cmd+K / Ctrl+K and moves focus through its results

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

De Cmd+K / Ctrl+K wordt hier niet gedefinieerd. Dus vind het verwarrend om dat hier te vermelden.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Klopt, de shortcut wordt in templates/quick_search.html.twig gebonden. De comment in de controller en de uitleg in docs/stimulus.md zeggen dat nu, zie abc3f37.

@jonasdekeukelaere
jonasdekeukelaere merged commit a4bf0e5 into master Oct 16, 2026
13 of 14 checks passed
@jonasdekeukelaere
jonasdekeukelaere deleted the quick-search branch October 16, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants