knplabs/dictionary-bundle
KnpDictionaryBundle lets you group tagged services into named dictionaries and fetch them by key at runtime, instead of wiring each dependency individually. Great for plugin-style architectures, handler registries, and any collection of services you want to look up dynamically without boilerplate co...
Architecture fit. Low, unchanged. knplabs/dictionary-bundle remains a Symfony bundle, not a framework-agnostic library, and v3.6.0 makes that more true rather than less: the headline feature, named dictionary autowiring (PR #370), is delivered entirely through Symfony's DI autowiring/alias machinery — named service aliases resolved by parameter type + name. That is the single clearest illustration of the ceiling here. Laravel has no named-autowiring analog; its contextual binding ($this->app->when(...)->needs(...)->give(...)) covers a different problem shape, and you would not build a registry feature around it. The rest of the surface is unchanged: compiler passes and DI tags (DependencyInjection/), Form types (Form/), Validator constraints (Validator/), Twig extensions (Templating/), Faker providers (Faker/), and a web-profiler collector (DataCollector/). None have a Laravel consumer. The estimate holds: roughly 60–70% of the integration layer is inert in a Laravel application, and v3.6.0 grew the inert part, not the portable part.
What is portable is still the core: Dictionary/ — named dictionary implementations (callable-backed, iterator-backed, static), a Collection registry, wrappers, and a Factory alias. That remains a thin, in-memory value-object layer over configuration, reproducible in a service provider plus a config file in a few hundred lines, using native backed enums for the common case. One genuine improvement on this axis: PR #371 "docs: document custom dictionary implementation" now publishes the Dictionary interface as a supported extension point, which is exactly the seam an extraction needs and previously had to be inferred from source. That moves path 1 from "archaeology" toward "documented contract."
Integration feasibility. Feasible only as a reference architecture, not as an installed dependency — unchanged. The realistic paths remain:
Dictionary/ namespaces) and re-implement the adapters as a Laravel service provider, a Rule::in()-style validation rule, and a Blade directive/component. Slightly de-risked by the new custom-dictionary docs.Path 2 is still likely cheaper for the common case. The extraction friction is structural rather than documentary: the legacy nested PSR-4 layout (src/Knp/DictionaryBundle/ rather than src/) still forces namespace re-mapping and re-pointing of the PHPSpec tree, and specs still depend on a local Prophecy extension and a custom shouldBeOneOf matcher (spec/PHPSpec/).
Technical risk.
Key questions. The prior five stand, plus one new. (1) What does composer.json actually require — is Dictionary/ free of symfony/* imports, and what is the PHP floor? (2) Do the PHPSpec suites for Dictionary/ run standalone without the bundle kernel? (3) Is the Collection/Factory alias genuinely the only public API, or do adapters reach into internals? (4) Are any dictionaries in our domain DB- or API-backed, which the bundle does not address? (5) Who owns a fork if we extract, and what is the sync cadence? (6) Does the newly documented custom-dictionary contract cover the Dictionary interface alone, or does it assume the named-autowiring registration path — i.e. does the documentation describe a seam we can reuse, or a Symfony-shaped seam we must re-cut?
Stack fit. Do not install the bundle. Treat it as a specification for a Laravel-native dictionary registry. v3.6.0 does not change the mapping, and the one new feature has no row worth adding — which is itself the finding:
| Bundle surface | Laravel replacement |
|---|---|
knp_dictionary.dictionaries config |
config/dictionaries.php |
Dictionary\Collection (public) |
Singleton bound in a service provider, exposed via facade |
Dictionary\Factory alias |
Container binding / factory method |
| Named dictionary autowiring (v3.6.0) | No analog — omit. Contextual binding if ever genuinely needed |
Form/ types |
Blade component + old()/request validation |
Validator/ constraints |
Custom rule or Rule::in() |
Templating/ Twig |
Blade directive @dictionary('gender') or component |
Faker/ provider |
Faker addProvider() |
DataCollector/ |
Omit (Telescope watcher if genuinely needed) |
Migration path. Unchanged. (1) Freeze current static arrays/constants into config/dictionaries.php, preserving the bundle's {name: {values: [...], ...}} shape. (2) Register a DictionaryServiceProvider binding a collection singleton, with config:cache compatibility as a hard requirement. (3) Introduce a Dictionary facade and a dictionary() helper. (4) Add the validation rule and Blade surface. (5) Delete the hardcoded literals. Prefer PHP backed enums plus Rule::enum() for the common case; reserve the registry for dictionaries that are dynamic, localized, or shared across services. The named-autowiring feature is explicitly not part of the migration — if a developer asks for it during the build, that is the moment to re-read this assessment rather than extend scope.
Compatibility. Extracting the core still sidesteps the bundle's symfony/* constraint graph, which remains the main compatibility liability. v3.6.0 improves the upstream posture — Symfony 7.1 CI, PHP 8.4/8.5 support — but that improvement accrues to Symfony users, not to us; it does not lower the PHP floor that matters to a Laravel runtime. Verify PHP floor against our runtime, and check whether Dictionary/ implementations depend on symfony/options-resolver, symfony/config, or PropertyAccess. If they do, extraction is a rewrite, not a port, and should be scoped accordingly. Note that the dev-toolchain majors (PHPStan 2.x, Rector 2.x) are dev-only and carry no runtime constraint.
Sequencing.
composer.json, attempt standalone autoload of Dictionary/, run vendor/bin/phpspec run against the core specs only. Add: read the new custom-dictionary documentation (PR #371) and the AGENTS guidance (PR #368) as primary inputs — they are the most current description of the extension points and will shorten the spike. Output: go/no-go on extraction.Maintenance. The contributor process remains unusually disciplined — behavior changes require a matching PHPSpec example, and PHPSpec/PHPStan/PHP-CS-Fixer/Rector/Twig-lint gates run in CI. v3.6.0 adds a codified AGENTS.md covering bundle and spec guidance (#368), which is a meaningful maturity signal: agent-facing in-repo guidance is usually written by people who expect contributors to act without a maintainer in the loop. It also means a divergent fork is expensive to keep in sync — every upstream behavior change arrives with a spec and now with authoring conventions we must reconcile. That calculus is unchanged by this release and slightly reinforced by it. Net: still adopt neither the code nor the fork obligation. A clean-room Laravel implementation remains near-zero ongoing cost.
Support. Still no commercial support; issues and PRs are community-only. Two developments worth recording. First, a new contributor (@ErwannRousseau) authored the entire substantive half of this release — AGENTS docs, PHPStan/Symfony compatibility fix, named autowiring, custom-dictionary docs, and PHP 8.4/8.5 support. That is a bus-factor improvement versus the prior read, but it is one release deep; treat it as an encouraging trend, not an established second maintainer. Second, the documentation gap is narrowing: the README-plus-AGENTS situation is now closer to README-plus-AGENTS-plus-custom-dictionary-guide, which reduces onboarding cost for anyone reading the package as a reference. Internal support burden for the Laravel adapter remains on whoever writes it.
Scaling. Runtime cost is negligible and unchanged: dictionaries are configuration, resolved at container/config-cache time, held in memory, lazily materialized on first access. The lazy contract remains the scaling mechanism. The scaling risk remains entirely self-inflicted — any adapter that eagerly enumerates values converts a lazy dictionary into per-request I/O. Enforce with a code review rule and a boot-time timing assertion. Dictionaries backed by DB or external APIs remain out of scope for the bundle and require our own caching layer.
Failure modes.
src/Knp/DictionaryBundle layout makes careless extraction produce class-not-found errors at runtime rather than build time.config/) survive config:cache invisibly in some workers and not others; enforce config-only definitions.Ramp-up. A Symfony-fluent engineer needs roughly a day to read Dictionary/, Configuration.php, and the compiler passes and form an opinion; the new custom-dictionary docs and AGENTS file shave the better part of half a day off that. A Laravel-only engineer needs two to three days, most of which is spent learning Symfony idioms in order to justify discarding them — unchanged, and the named-autowiring feature adds a small amount of surface that must be understood and then explicitly rejected. Budget the spike as evaluation time, not implementation time — the expected outcome is a design document for a ~300-line Laravel registry, plus a decision to use native enums wherever they suffice. Against an opportunity score of 20 and an unverified dependent count, the correct TPM posture is unchanged: read it, don't depend on it. v3.6.0 is a healthy release for a package we should still not install — the modernization is real, the Symfony coupling is deeper, and neither fact moves the recommendation.
How can I help you explore Laravel packages today?