From f4176017fd99ec0812c0cb2318c2d145320dd8e5 Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Fri, 17 Jul 2026 17:40:50 +0200 Subject: [PATCH 1/2] chore: update bundle code and docs --- AGENTS.md | 59 +++++++++++++++++++ spec/AGENTS.md | 45 ++++++++++++++ src/Knp/DictionaryBundle/AGENTS.md | 57 ++++++++++++++++++ src/Knp/DictionaryBundle/Dictionary/AGENTS.md | 44 ++++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 AGENTS.md create mode 100644 spec/AGENTS.md create mode 100644 src/Knp/DictionaryBundle/AGENTS.md create mode 100644 src/Knp/DictionaryBundle/Dictionary/AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..4cf59632 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,59 @@ +# PROJECT KNOWLEDGE BASE + +## OVERVIEW + +Reusable Symfony bundle for declaring named dictionaries in application configuration and consuming them through DI, forms, validation, Twig, Faker, and the web profiler. PHP 8.1+; Symfony 5.4, 6.4, and 7.x; Twig 2.15/3.x. + +## STRUCTURE + +```text +DictionaryBundle/ +├── src/Knp/DictionaryBundle/ # PSR-4 runtime package; legacy nested bundle layout +├── spec/Knp/DictionaryBundle/ # PHPSpec tree mirroring runtime namespaces +├── spec/PHPSpec/ # Local extension and shouldBeOneOf matcher +├── bin/lint-twig # Standalone Twig syntax check +└── .github/workflows/test.yaml # Compatibility matrix and quality gates +``` + +## WHERE TO LOOK + +| Task | Location | Notes | +|------|----------|-------| +| Bundle bootstrap | `src/Knp/DictionaryBundle/KnpDictionaryBundle.php` | Registers compiler passes in significant order | +| User configuration | `src/Knp/DictionaryBundle/DependencyInjection/Configuration.php` | Normalizes `knp_dictionary.dictionaries` | +| Service composition | `src/Knp/DictionaryBundle/Resources/config/services.yaml` | Imports explicit service fragments | +| Dictionary behavior | `src/Knp/DictionaryBundle/Dictionary/` | Implementations, collection, wrappers, factories | +| Framework adapters | `Form/`, `Validator/`, `Templating/`, `Faker/`, `DataCollector/` | All consume the shared collection | +| Behavioral specs | `spec/Knp/DictionaryBundle/` | Mirrors source paths; no PHPUnit suite | + +## CONVENTIONS + +- Runtime namespace maps to `src/Knp/DictionaryBundle`; specs map `spec\Knp\` to `spec/Knp`. +- PHP files use strict types, final concrete classes, and ordered imports/interfaces. +- DI YAML uses explicit FQCN service IDs, arguments, and tags. No broad autowire/autoconfigure resource block. +- Services are private except public API entry points: `Dictionary\Collection` and the `Dictionary\Factory` alias. +- Behavior changes require a matching PHPSpec example; specs keep data inline and use Prophecy collaborators. + +## ANTI-PATTERNS (THIS PROJECT) + +- Do not eagerly evaluate callable or iterator-backed dictionaries. Their documented contract is lazy until first value access. +- Do not treat the README's `knp_dictionary.value_transformer` tag as implemented. No compiler pass or autoconfiguration consumes it. +- Do not reorder compiler passes or tagged factory definitions without tracing construction order and running the relevant specs. +- Do not copy the README's PHPStan or Rector commands; both sections are stale. Use the commands below. + +## COMMANDS + +```bash +composer install +vendor/bin/phpspec run -v --config=phpspec.no-coverage.yml +vendor/bin/phpstan analyse --no-progress --memory-limit=-1 +bin/lint-twig src/ +PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --diff --dry-run -vvv +vendor/bin/rector process --dry-run +``` + +## NOTES + +- CI rewrites `composer.json` to test Symfony minors with lowest and current dependencies; avoid relying on a single local dependency set. +- PHPStan level 8 covers `src` plus `spec/PHPSpec`, not the mirrored behavior specs. +- CI currently excludes PHP 8.4 pending PHPSpec compatibility. diff --git a/spec/AGENTS.md b/spec/AGENTS.md new file mode 100644 index 00000000..27f4af0e --- /dev/null +++ b/spec/AGENTS.md @@ -0,0 +1,45 @@ +# PHPSPEC GUIDE + +## OVERVIEW + +Behavior suite mirroring the runtime namespace with source-aligned specifications. + +## STRUCTURE + +```text +spec/ +├── Knp/DictionaryBundle/ # Source-mirrored ObjectBehavior specs +└── PHPSpec/ # Local extension and shouldBeOneOf matcher +``` + +## WHERE TO LOOK + +| Task | Location | Notes | +|------|----------|-------| +| Domain/factory behavior | `Knp/DictionaryBundle/Dictionary/` | Largest coverage area; inline data and helper callables/iterators | +| Compiler wiring | `Knp/DictionaryBundle/DependencyInjection/Compiler/` | Assert definitions, tags, arguments, and method calls exactly | +| Shared matcher | `PHPSpec/OneOfMatcher.php` | Strict membership matcher used by Faker specs | +| Extension registration | `PHPSpec/Extension.php` | Loaded by both PHPSpec config files | + +## CONVENTIONS + +- Mirror the source path and name classes `final class FooSpec extends ObjectBehavior`. +- Name examples `it_*`/`its_*`; use `let()` for common setup and constructor injection. +- Use Prophecy collaborators for interfaces; pass wrapped collaborators into real `Collection` instances when collection behavior matters. +- Keep fixtures inline; the suite has no fixture, mock, or data directories. +- Test both `supports()` dispatch and `create()` output/error behavior for factories. + +## COMMANDS + +```bash +vendor/bin/phpspec run +vendor/bin/phpspec run spec/Knp/DictionaryBundle/Dictionary/SimpleSpec.php +``` + +The default config writes `coverage.xml`; use the parent guide's no-coverage command for routine full-suite checks. + +## GAPS TO REMEMBER + +- `DictionaryTracePass::process()` has no direct spec; bundle registration alone is covered. +- `KnpDictionaryExtensionSpec` only checks initialization, not service/config loading behavior. +- Interfaces and `Dictionary\Wrapper` lack direct specs and are exercised through implementations. diff --git a/src/Knp/DictionaryBundle/AGENTS.md b/src/Knp/DictionaryBundle/AGENTS.md new file mode 100644 index 00000000..a21917c5 --- /dev/null +++ b/src/Knp/DictionaryBundle/AGENTS.md @@ -0,0 +1,57 @@ +# RUNTIME PACKAGE GUIDE + +## OVERVIEW + +The bundle turns normalized `knp_dictionary` configuration into tagged dictionary services, aggregates them in a public collection, and exposes that collection through Symfony adapters. + +## STRUCTURE + +```text +DictionaryBundle/ +├── DependencyInjection/ # Config tree, extension, compiler passes +├── Dictionary/ # Core contracts, implementations, factories +├── Resources/ # Service fragments and profiler template +├── ValueTransformer/ # Ordered value transformation aggregate +└── {Form,Validator,Templating,Faker,DataCollector}/ # Thin adapters +``` + +## RUNTIME FLOW + +```text +knp_dictionary config + -> KnpDictionaryExtension + -> knp_dictionary.configuration parameter + -> DictionaryBuildingPass + -> Factory\Aggregate::create() + -> tagged Dictionary service + -> DictionaryRegistrationPass + -> Dictionary\Collection + -> framework adapters +``` + +`services/debug.yaml` always defines the collector; the final pass therefore decorates every tagged dictionary with `Traceable` in the normal bundle load path. + +## WHERE TO LOOK + +| Change | Files that move together | +|--------|--------------------------| +| Add a config key | `DependencyInjection/Configuration.php`, consuming factory/pass, matching specs | +| Add a built-in dictionary type | Factory implementation, `Resources/config/services/factories.yaml`, configuration schema, specs | +| Add a framework adapter | Adapter class, dedicated service YAML fragment, import in `services.yaml`, mirrored spec | +| Change registration | `KnpDictionaryExtension.php`, compiler pass constants/processors, bundle pass order | +| Change profiler behavior | `DictionaryTracePass`, `Dictionary/Traceable.php`, collector, Twig layout | + +## CONVENTIONS + +- `KnpDictionaryExtension` autoconfigures implementations of `Dictionary` and `Dictionary\Factory`; manual tags are `knp_dictionary.dictionary` and `knp_dictionary.factory`. +- Compiler pass order is build dictionaries, collect factories, register dictionaries, then add trace decorators. +- `Configuration` normalizes bare maps and `content`-only entries to `type: value`; `normalizeKeys(false)` preserves dictionary keys. +- Generated definitions defer construction to `Factory\Aggregate::create()` through a container factory reference. +- Registration and tracing deliberately discover the same dictionary tag; tracing runs after collection method calls are recorded. + +## ANTI-PATTERNS + +- Do not add accepted factory input only in `supports()` or `create()`; the configuration tree rejects unknown keys first. +- Do not replace generated definitions with compiler-time instances; container references preserve service lookup and lazy sources. +- Do not remove dictionary tags after building; both collection registration and profiler decoration depend on them. +- Do not make optional adapters public merely to retrieve them from the container; their framework tags are the integration API. diff --git a/src/Knp/DictionaryBundle/Dictionary/AGENTS.md b/src/Knp/DictionaryBundle/Dictionary/AGENTS.md new file mode 100644 index 00000000..dd07cff4 --- /dev/null +++ b/src/Knp/DictionaryBundle/Dictionary/AGENTS.md @@ -0,0 +1,44 @@ +# DICTIONARY DOMAIN GUIDE + +## OVERVIEW + +Core dictionary contracts, eager and lazy implementations, the name-indexed collection, decorators, and the ordered factory chain. + +## WHERE TO LOOK + +| Concern | Location | Constraint | +|---------|----------|------------| +| Public value contract | `../Dictionary.php` | ArrayAccess + Countable + IteratorAggregate + name/keys/values | +| Registry | `Collection.php` | Add through `add()`; ArrayAccess mutation is rejected | +| Basic storage | `Simple.php` | Backing implementation for value-based factories | +| Lazy callable/iterator | `Invokable.php`, `Iterator.php` | Source must remain untouched until first data access | +| Composition | `Combined.php`, `Factory/Combined.php` | Resolves referenced names through Collection | +| Inheritance | `Factory/Extended.php` | Base dictionary precedes new values in merge order | +| Debug decoration | `Traceable.php` | Delegates behavior and records collector access | +| Factory dispatch | `Factory.php`, `Factory/Aggregate.php` | First factory whose `supports()` returns true wins | + +## FACTORY ORDER + +`../Resources/config/services/factories.yaml` declares `Extended` before type factories because any config containing `extends` must be intercepted first. `../DependencyInjection/Compiler/DictionaryFactoryBuildingPass.php` preserves tagged-service discovery order when calling `Aggregate::addFactory()`. + +Built-in factory triggers are: + +- `value`, `value_as_key`, `key_value`: content-backed dictionaries with value transformation. +- `callable`: container service plus optional method, wrapped lazily by `Dictionary\Invokable`. +- `iterator`: Traversable container service, wrapped lazily by `Dictionary\Iterator`. +- `combined`: ordered names resolved from `Collection`. +- `extends`: wrapper factory applied before the underlying type factory. + +## CONVENTIONS + +- Each concrete factory validates its required config at `create()` and keeps `supports()` limited to dispatch. +- `Combined` and `Extended` resolve dependencies by dictionary name; declaration order matters because referenced dictionaries must already exist when instantiated. +- Preserve generic PHPDoc on `Dictionary`, implementations, collections, and factory return types; PHPStan runs at level 8. +- Mirror behavior changes under `spec/Knp/DictionaryBundle/Dictionary`, including factory `supports()` cases and invalid configuration. + +## ANTI-PATTERNS + +- Do not reorder factories casually; overlapping `supports()` predicates change which implementation is constructed. +- Do not call or traverse lazy sources in constructors, factory validation, or `getName()`. +- Do not mutate `Collection` through array offsets; use `add()` so names remain the sole keys. +- Do not silently reverse merge precedence for extended or combined dictionaries. From c9ef1deed97df5d96820a2398c680a8540f940f0 Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Fri, 17 Jul 2026 20:45:41 +0200 Subject: [PATCH 2/2] ci: update Symfony compatibility and modernize README --- .github/workflows/test.yaml | 2 +- README.md | 115 +++++++++++++++++++++--------------- 2 files changed, 70 insertions(+), 47 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index be07d76f..fd277044 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,7 +14,7 @@ jobs: symfony: - '5.4' - '6.4' - - '7.2' + - '7.4' minimumStability: - 'stable' composerOptions: diff --git a/README.md b/README.md index d8bb774a..fd901b1c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -DictionaryBundle -================ +# DictionaryBundle + [![CircleCI](https://circleci.com/gh/KnpLabs/DictionaryBundle.svg?style=svg)](https://circleci.com/gh/KnpLabs/DictionaryBundle) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/KnpLabs/DictionaryBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/KnpLabs/DictionaryBundle/?branch=master) @@ -8,15 +8,17 @@ Are you often tired to repeat static choices like gender or civility in your app ## Requirements - PHP >= 8.1 -- Symfony 5.4, 6.4 or 7.* +- Symfony 5.4, 6.4 or 7.\* ## Installation Run the following command: + ```bash composer require knplabs/dictionary-bundle ``` -Register the bundle in ``config/bundles.php`` + +Register the bundle in `config/bundles.php` ```php $bundles = array( @@ -29,21 +31,24 @@ $bundles = array( You can ping us if need some reviews/comments/help: - - [@AntoineLelaisant](https://github.com/AntoineLelaisant) - - [@PedroTroller](https://github.com/PedroTroller) +- [@AntoineLelaisant](https://github.com/AntoineLelaisant) +- [@PedroTroller](https://github.com/PedroTroller) ## Basic usage Define dictionaries in your config.yml file: + ```yaml knp_dictionary: dictionaries: my_dictionary: # your dictionary name - - Foo # your dictionary content + - Foo # your dictionary content - Bar - Baz ``` -You will be able to retreive it by injecting the Collection service and accessing the dictionary by its key + +You will be able to retrieve it by injecting the Collection service and accessing the dictionary by its key + ```php private Dictionary $myDictionary; @@ -53,7 +58,8 @@ You will be able to retreive it by injecting the Collection service and accessin $this->myDictionary = $dictionaries['my_dictionary']; } ``` -### Dictionary form type + +## Dictionary form type Now, use them in your forms: @@ -69,9 +75,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) ; } ``` + The dictionary form type extends the [symfony's choice type](http://symfony.com/fr/doc/current/reference/forms/types/choice.html) and its options. -### Validation constraint +## Validation constraint You can also use the constraint for validation. The `value` has to be set. @@ -80,60 +87,69 @@ use Knp\DictionaryBundle\Validator\Constraints\Dictionary; class User { - /** - * @ORM\Column - * @Dictionary(name="my_dictionary") - */ + #[ORM\Column] + #[Dictionary(name: 'my_dictionary')] private $civility; } ``` ## Advanced usage + You can specify the indexation mode of each dictionary + ```yaml knp_dictionary: dictionaries: - my_dictionary: # your dictionary name - type: 'key_value' # your dictionary type - content: # your dictionary content + my_dictionary: # your dictionary name + type: "key_value" # your dictionary type + content: # your dictionary content "foo": "foo_value" "bar": "bar_value" "baz": "baz_value" ``` -### Available types + +## Available types + - `value` (default) : Natural indexation - `value_as_key`: Keys are defined from their value - `key_value`: Define your own keys - `callable`: Build a dictionary from a callable -### Callable dictionary +## Callable dictionary + You can create a callable dictionary: + ```yaml knp_dictionary: dictionaries: - my_callable_dictionary: # your dictionary name - type: 'callable' # your dictionary type - service: 'app.service.id' # a valid service from your application - method: 'getSomething' # the method name to execute + my_callable_dictionary: # your dictionary name + type: "callable" # your dictionary type + service: "app.service.id" # a valid service from your application + method: "getSomething" # the method name to execute ``` + Callable dictionaries are loaded with a lazy strategy. It means that the callable will not be called if you do not use the dictionary. -### Iterator based dictionary +## Iterator based dictionary + You can create a dictionary from an iterator: + ```yaml knp_dictionary: dictionaries: - my_iterator_dictionary: # your dictionary name - type: 'iterator' # your dictionary type - service: 'app.service.id' # a valid service from your application + my_iterator_dictionary: # your dictionary name + type: "iterator" # your dictionary type + service: "app.service.id" # a valid service from your application ``` -Iterator based dictionaries are loaded with a lazy strategy. It means that the + +Iterator based dictionaries are loaded with a lazy strategy. It means that the iterator will not be fetched if you do not use the dictionary. -### Combined dictionary +## Combined dictionary You can combine multiple dictionaries into a single one: + ```yaml knp_dictionary: dictionaries: @@ -155,34 +171,38 @@ knp_dictionary: - payment_mode - extra_payment_mode ``` -Now you have 3 dictionaries, `payment_mode` and `extra_payment_mode` contain + +Now you have 3 dictionaries, `payment_mode` and `extra_payment_mode` contain their own values but `combined_payment_mode` contains all the values of the previous ones. -### Extended dictionary +## Extended dictionary You can create an extended dictionary: + ```yaml knp_dictionary: dictionaries: europe: - type: 'key_value' + type: "key_value" content: fr: France de: Germany world: - type: 'key_value' + type: "key_value" extends: europe content: us: USA ca: Canada ``` + The dictionary `world` will now contain its own values in addition to the `europe` values. **Note**: You must define the initial dictionary **BEFORE** the extended one. ## Transformers + For now, this bundle is only able to resolve your **class constants**: ```yaml @@ -191,13 +211,15 @@ my_dictionary: - Foo - Bar ``` + You want to add other kinds of transformations for your dictionary values ? Feel free to create your own transformer ! -### Add your own transformers +## Add your own transformers Create your class that implements [TransformerInterface](src/Knp/DictionaryBundle/Dictionary/ValueTransformer/TransformerInterface.php). Load your transformer and tag it as `knp_dictionary.value_transformer`. + ```yaml services: App\My\Transformer: @@ -225,7 +247,7 @@ But you can also access directly to a value by using the same function (or filte The KnpDictionaryBundle comes with a [faker provider](https://github.com/FakerPHP/Faker) that can be used to provide a random entry from a dictionary. -### Alice +## Alice To register the provider in [nelmio/alice](https://github.com/nelmio/alice), you can follow the [official documentation](https://github.com/nelmio/alice/blob/master/doc/customizing-data-generation.md#add-a-custom-faker-provider-class) @@ -239,13 +261,13 @@ App\Entity\User: ## Create your own dictionary implementation -### Dictionary +## Dictionary Your dictionary implementation must implements the interface [Dictionary](src/Knp/DictionaryBundle/Dictionary.php). -It is automaticaly registered with the `autoconfigure: true` DIC feature. +It is automatically registered with the `autoconfigure: true` DIC feature. -Else you can register it by your self: +Else you can register it by your self: ```yaml services: @@ -254,13 +276,13 @@ services: - knp_dictionary.dictionary ``` -### Dictionary Factory +## Dictionary Factory -You must create a dictionary factory that will be responsible to instanciate your dictionary. +You must create a dictionary factory that will be responsible to instantiate your dictionary. -It is automaticaly registered with the `autoconfigure: true` DIC feature. +It is automatically registered with the `autoconfigure: true` DIC feature. -Else you can register it by your self: +Else you can register it by your self: ```yaml services: @@ -268,23 +290,24 @@ services: tags: - knp_dictionary.factory ``` + ## Tests -### phpspec +## phpspec ```bash composer install vendor/bin/phpspec run ``` -### php-cs-fixer +## php-cs-fixer ```bash composer install vendor/bin/php-cs-fixer fix ``` -### phpstan +## phpstan First [install phive](https://github.com/phar-io/phive#getting-phive). @@ -295,7 +318,7 @@ phive install tools/phpstan process ``` -### rector (*optional*) +## rector (_optional_) ```bash rector process --set php70 --set php71 --set php72 --set code-quality --set coding-style --set symfony34 --set twig240 --set psr-4 --set solid src/ spec/