ongr/router-bundle
Symfony bundle that adds Elasticsearch-backed routing: store route definitions as documents, match incoming URLs through a custom router, and generate paths from indexed content. Useful for dynamic CMS pages and large route sets managed outside static Symfony routing.
The bundle replaces Symfony's file-based route collection with routes stored as Elasticsearch documents, so URLs can be created/edited at runtime (CMS pages, landing pages, per-tenant routes) without a deploy.
Install and register
composer require ongr/router-bundle
v2.0.1 is a composer-metadata-only release: it fixes a wrong ongr/elasticsearch-bundle version constraint in composer.json. Nothing in the PHP code changed, but the constraint fix means dependency resolution behaves differently than the 1.x/2.0.0 tag — before this patch the require could fail outright or resolve to an unexpected ES bundle line. So after installing, always confirm what you actually got:
composer show ongr/router-bundle ongr/elasticsearch-bundle elasticsearch/elasticsearch
The bundle still targets a Symfony 2.x/3.x stack and the old ES client; a corrected constraint does not mean Symfony 4+/ES 6+ support. Check the resolved versions before you start — it is not compatible with Symfony 4+ without patching.
// app/AppKernel.php — ONGR bundles must be registered after FrameworkBundle
$bundles = [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
// ...
new ONGR\ElasticsearchBundle\ONGRElasticsearchBundle(),
new ONGR\RouterBundle\ONGRRouterBundle(),
];
Configure the ES manager the router should read from
# app/config/config.yml
ongr_es:
managers:
default:
index:
index_name: app
hosts: [localhost:9200]
mappings:
- AppBundle:Route
ongr_router:
es_manager: default
Define the route document
namespace AppBundle\Document;
use ONGR\RouterBundle\Document\Route as BaseRoute;
use ONGR\ElasticsearchBundle\Annotation as ES;
/**
* @ES\Document(type="route")
*/
class Route extends BaseRoute
{
}
Create the index and seed one route
php app/console ongr:es:index:create
$route = new Route();
$route->setName('promo_show'); // ES document id == Symfony route name
$route->setPath('/promo/{slug}');
$route->setController('AppBundle:Promo:show');
$route->setDefaults(['_format' => 'html']);
$route->setRequirements(['slug' => '[a-z0-9-]+']);
$manager = $this->get('es.manager.default');
$manager->persist($route);
$manager->commit();
$manager->getConnection()->refresh(); // or you won't see it for ~1s
First use case to prove it works: hit /promo/spring-sale and then run php app/console debug:router promo_show. If the route shows up in debug:router you have correctly swapped the router service; if not, the stock router.default is still winning and you need to check bundle ordering / the ongr.router.router service alias.
Where to look first in the source
Routing/RouteProvider.php — how documents become a RouteCollectionRouting/RouteCompiler.php — array/document → Symfony\Component\Routing\RouteDocument/Route.php — the exact fields (path, controller, defaults, requirements, options, host, schemes, methods, condition)Resources/config/services.yml — service ids you will decorateTreat routes as content, not code. The idiomatic ONGR pattern is an admin CRUD screen that persists Route documents. Wrap creation in a small factory so the invariant "document id == route name" is enforced in one place:
class RouteFactory
{
public function create(string $name, string $path, string $controller, array $defaults = []): Route
{
$route = new Route();
$route->setName($name);
$route->setPath($path);
$route->setController($controller);
$route->setDefaults($defaults);
$route->setRequirements([]);
$route->setOptions([]);
$route->setSchemes([]);
$route->setMethods([]);
$route->setHost('');
return $route;
}
}
Fixture/import sync for static routes. For routes that are really code (homepage, sitemap), keep a YAML file in the repo and push it into ES during deploy, upserting by name. This gives you version control for the stable routes while still allowing runtime routes for the dynamic ones:
foreach ($yaml['routes'] as $name => $def) {
$route = $repository->find($name) ?: $this->factory->create($name, $def['path'], $def['controller']);
$route->setDefaults($def['defaults'] ?? []);
$manager->persist($route);
}
$manager->commit();
Generate URLs the normal way. Once the router is swapped, generate() and absolute_url() work unchanged, because the provider returns compiled Symfony\Component\Routing\Route objects:
return $this->redirectToRoute('promo_show', ['slug' => 'spring-sale']);
This is the main reason to use the bundle rather than hand-rolling a matcher — Twig's path() keeps working.
Localised routes. Emit one document per locale with the same path but different _locale defaults, and make the path itself prefixed (/de/promo/{slug}). Names must still be unique, so suffix them (promo_show.de). Because matching is first-match-wins over an unordered collection, put the stricter/prefix paths in requirements rather than relying on insertion order.
Integration points
ongr.router.route_provider if you need filtering (e.g. only routes for the current tenant/domain) instead of forking the bundle.ongr.router.route_compiler if you need extra document fields mapped onto the Symfony Route.kernel.request listener for anything the router can't express (redirects, 410s) rather than abusing condition.Warm it. Since RouteProvider builds the collection from ES, add a cache warmer or a kernel.cache_warmer-adjacent deploy step that serialises the collection, and have your provider read that on cold cache. Without this you pay an ES round-trip per request.
Pin the constraint yourself, don't trust v2.0.1's fix blindly. The release exists precisely because the ongr/elasticsearch-bundle requirement was wrong. That tells you the constraint had been shipping broken and untested for years. Commit composer.lock, and in CI assert the resolved ongr/elasticsearch-bundle and elasticsearch/elasticsearch versions match what your code was written against — a future constraint edit can silently swap the client underneath you.
Elasticsearch near-real-time refresh. The single most common "the route doesn't exist" bug. After `commit(
How can I help you explore Laravel packages today?