SearchableDataProvider plugins¶
How to implement a search provider that exposes your module's data to the unified search system (used by the CKEditor integration, autocomplete endpoints, and any future search consumers).
When to use this¶
- Your module stores a WoW entity type (items, creatures, quests, etc.) and you want it searchable from the editor modal or other search UIs.
- You want to provide local-first search with optional Blizzard Search API fallback for entities not yet imported.
The interface¶
Drupal\wow\SearchableDataProviderInterface
interface SearchableDataProviderInterface extends PluginInspectionInterface {
public function getSearchLabel(): string;
public function search(string $query, string $region, int $limit = 10): array;
public function getCanonicalUrl(int $blizzardId): ?Url;
public function importEntity(int $blizzardId, string $region): ?Url;
}
| Method | Returns | Purpose |
|---|---|---|
getSearchLabel() |
string |
Human-readable label for this category (e.g. "Items", "Creatures"). Shown in search UIs. |
search() |
SearchResult[] |
Query local storage first, optionally fall back to the Blizzard Search API if fewer than $limit results are found. |
getCanonicalUrl() |
Url\|null |
Canonical page URL for an entity by Blizzard ID. NULL if the type has no canonical page. |
importEntity() |
Url\|null |
Sync a remote entity from the Blizzard API, persist it locally, return the canonical URL. NULL if import failed or unsupported. |
The attribute¶
use Drupal\wow\Attribute\SearchableDataProvider;
#[SearchableDataProvider(
id: 'my_things',
label: new TranslatableMarkup('My things'),
weight: 15,
)]
class MyThingSearch extends SearchableDataProviderBase {
// ...
}
Plugins live in modules/<module>/src/Plugin/SearchableDataProvider/.
| Attribute property | Purpose |
|---|---|
id |
Plugin ID. Used as the type parameter in search queries. |
label |
Human-readable label for search UI dropdowns. |
weight |
Display order. Lower values appear first. Controls iteration order in multi-provider searches. |
The SearchResult value object¶
Drupal\wow\Search\SearchResult
Every search() call returns an array of these:
new SearchResult(
blizzardId: 19019,
name: 'Thunderfury, Blessed Blade of the Windseeker',
type: 'items',
isLocal: true,
iconUrl: 'https://render.worldofwarcraft.com/icons/56/inv_sword_39.jpg',
description: 'Unique legendary sword',
quality: 'LEGENDARY',
);
| Property | Type | Notes |
|---|---|---|
blizzardId |
int |
The Blizzard API ID. |
name |
string |
Display name. |
type |
string |
Plugin ID of the provider that returned this result. |
isLocal |
bool |
TRUE = from local database. FALSE = from Blizzard Search API (not yet imported). |
iconUrl |
?string |
Icon URL, or NULL. |
typeLabel |
string |
Stamped by the SearchOrchestrator — providers do not need to set this. |
description |
?string |
Short description or flavor text. |
quality |
?string |
Quality tier (e.g. "RARE", "EPIC"). Primarily for items; NULL for most types. |
Local vs remote search pattern¶
Most providers follow a two-phase search:
- Local search — query the Drupal entity storage (
CONTAINSon the name field). Fast, returns only imported entities. - Remote fallback — if the local search returned fewer than
$limitresults AND the data type supports the Blizzard Search API, query the API to fill the gap. Mark these results withisLocal: false.
Types with a full synced catalog (achievements, mounts, pets, toys, titles, quests) only need local search — the entire catalog is already in the database. Types with on-demand import (items, creatures) benefit from the remote fallback.
public function search(string $query, string $region, int $limit = 10): array {
// Phase 1: local.
$results = $this->searchLocal($query, $limit);
// Phase 2: remote fallback if under limit.
if (count($results) < $limit) {
$remaining = $limit - count($results);
$remoteResults = $this->searchRemote($query, $region, $remaining);
$results = array_merge($results, $remoteResults);
}
return $results;
}
importEntity()¶
Called when a consumer (the CKEditor plugin, for example) selects a remote search result and needs it persisted before linking to it.
- Fetch the entity from the Blizzard API.
- Create or update the local entity.
- Return the canonical URL so the consumer can build the link.
- Return NULL if import fails or the provider doesn't support remote import.
Local-only providers (achievements, mounts, etc.) return NULL from importEntity() — their catalogs are already fully synced.
The orchestrator¶
Drupal\wow\Search\SearchOrchestrator (service: wow.search_orchestrator)
Controllers and UI consumers should call the orchestrator rather than using the plugin manager directly. It handles:
- Multi-provider iteration — queries all enabled providers (or a single one if a
typefilter is passed). - Budget allocation — distributes the total result limit across providers with a per-provider cap to ensure result diversity.
- Failure isolation — one provider throwing an exception doesn't abort the search. The error is logged and remaining providers continue.
- Metadata stamping — stamps
typeLabelon each SearchResult from the provider'sgetSearchLabel(). - Minimum query length — queries shorter than 2 characters return empty immediately.
// Search all providers.
$results = \Drupal::service('wow.search_orchestrator')
->search('thunderfury', 'eu', limit: 20);
// Search a single provider.
$results = \Drupal::service('wow.search_orchestrator')
->search('thunderfury', 'eu', limit: 10, type: 'items');
// List available provider types (for building a dropdown).
$providers = \Drupal::service('wow.search_orchestrator')
->getAvailableProviders();
JSON endpoints¶
Search¶
| Path | Method | Parameters | Returns |
|---|---|---|---|
/wow/search |
GET | q (query), limit, type (optional) |
JSON array of SearchResult objects |
The search endpoint always uses the configured default region from wow.settings. There is no region query parameter.
Import¶
| Path | Method | Returns |
|---|---|---|
/wow/search/import |
POST | JSON with url (canonical) or error |
The import endpoint requires a CSRF token and a JSON request body:
POST /wow/search/import
Content-Type: application/json
X-CSRF-Token: {token}
{"type": "items", "id": 19019}
Response on success:
The CSRF token is obtained from /session/token.
Existing implementations¶
| Plugin ID | Module | Weight | Remote search | Import |
|---|---|---|---|---|
items |
wow_item |
0 | Yes (Blizzard Search API) | Yes |
creatures |
wow_creature |
0 | Yes (Blizzard Search API) | Yes |
achievements |
wow_achievement |
10 | No (full catalog synced) | No |
quests |
wow_quest |
10 | No (full catalog synced) | No |
mounts |
wow_mount |
20 | No (full catalog synced) | No |
pets |
wow_pet |
25 | No (full catalog synced) | No |
toys |
wow_toy |
30 | No (full catalog synced) | No |
titles |
wow_title |
35 | No (full catalog synced) | No |
See also¶
- Editor integration — the CKEditor 5 consumer that drives the search modal
- Adding submodules — the full submodule scaffold if your search provider accompanies a new entity type
- Plugin system — how SearchableDataProvider fits alongside DashboardSectionProvider and CharacterDataProvider