Skip to content

Feat/filter types - #20

Open
ericges wants to merge 96 commits into
mainfrom
feat/filter-types
Open

ericges wants to merge 96 commits into
mainfrom
feat/filter-types

Conversation

@ericges

@ericges ericges commented May 1, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces significant refactoring and cleanup to the codebase, focusing on backend configuration, dependency injection, and code quality. The main changes include the removal of several legacy or redundant contracts, the introduction of a new DCA (Data Container Array) configuration API, and updates to service definitions and code quality settings.

Backend configuration and DCA API:

  • Introduced a new DCA configuration API with DcaBuilder, DcaBuilderInterface, and DcaContext to standardize and streamline backend configuration for filter elements and list types. This includes the new DcaContract interface for elements that configure their Contao backend appearance. (src/DataContainer/Builder/DcaBuilder.php [1] src/DataContainer/Builder/DcaBuilderInterface.php [2] src/DataContainer/Builder/DcaContext.php [3] src/Contract/DcaContract.php [4]

  • Removed the legacy PaletteContract and related configuration classes, as well as several filter element contracts, in favor of the new DCA API. (src/Contract/PaletteContract.php [1] src/Contract/Config/PaletteConfig.php [2] src/Contract/Config/ListItemProviderConfig.php [3] src/Contract/FilterElement/FormDataContract.php [4] src/Contract/FilterElement/FormTypeOptionsContract.php [5] src/Contract/FilterElement/HydrateFormContract.php [6] src/Contract/FilterElement/IntrinsicValueContract.php [7] src/Contract/FilterElement/RuntimeValueContract.php [8]

Dependency injection and service configuration:

  • Updated services.yaml to exclude new directories (DataContainer/Builder, a filter element file) from autowiring and removed the now-unneeded FilterInvokerResolver service definition. (config/services.yaml [1] [2] [3]

Code quality and static analysis:

  • Updated mago.toml to restrict source paths to only src/, add an assertion style rule, and improve static analysis configuration. (mago.toml [1] [2]

Collection and utility classes:

  • Removed the now-unused AbstractCollection and FilterDefinitionCollection classes, likely as part of the refactoring and simplification. (src/Collection/AbstractCollection.php [1] src/Collection/FilterDefinitionCollection.php [2]

These changes collectively modernize the backend configuration approach, remove dead code, and improve maintainability.This pull request introduces a significant refactor of the filter system, focusing on renaming and replacing the FilterDefinition concept with ConfiguredFilter, and removing the custom filter invoker attribute/registration system. It also cleans up configuration and code style, and removes now-unnecessary files related to filter invoker registration.

Key changes:

Filter System Refactor

  • Replaced all uses of FilterDefinition with ConfiguredFilter throughout the codebase, including type hints, method signatures, and documentation. The collection class was renamed from FilterDefinitionCollection to ConfiguredFilterCollection, with corresponding updates to methods and type annotations. (src/Collection/ConfiguredFilterCollection.php, [1] [2] [3] [4] [5] [6]; src/Contract/FilterElement/HydrateFormContract.php, [7]; src/Contract/FilterElement/IntrinsicValueContract.php, [8] [9]; src/Contract/FilterElement/RuntimeValueContract.php, [10] [11]; src/DataContainer/FilterContainer.php, [12] [13] [14] [15]

  • Removed the AsFilterInvoker attribute and the compiler pass RegisterFilterInvokersPass, effectively eliminating custom filter invoker registration via attributes. Related service configuration and autoconfiguration were also cleaned up. (src/DependencyInjection/Attribute/AsFilterInvoker.php, [1]; src/DependencyInjection/Compiler/RegisterFilterInvokersPass.php, [2]; config/services.yaml, [3]; src/DependencyInjection/HeimrichHannotFlareExtension.php, [4] [5]

Configuration and Code Style

  • Updated mago.toml to only include the src/ directory in the source paths and added a rule for assertion style. (mago.toml, [1] [2]

  • Minor code cleanup, such as removing unnecessary comments and ignoring obsolete PHPStan directives. (src/DependencyInjection/Configuration.php, src/DependencyInjection/Configuration.phpL17)


These changes modernize and streamline the filter system, making it more consistent and easier to maintain by relying on the new ConfiguredFilter model and removing legacy invoker registration code.

@ericges
ericges force-pushed the feat/filter-types branch from bc68fe1 to 5122442 Compare May 1, 2026 18:00
@ericges
ericges force-pushed the feat/filter-types branch from 374d0e0 to 43c7da1 Compare May 28, 2026 14:14
@ericges
ericges force-pushed the feat/filter-types branch from 43c7da1 to bb30f1f Compare July 13, 2026 11:51
ericges added a commit that referenced this pull request Jul 13, 2026
Rewrites the developer documentation for the v0.2 filter subsystem rework:

- Delete pages for removed subsystems: DCA callbacks (callbacks.mdx),
  filter invokers (spec/invoker.md), and the six removed filter-element
  contracts (form-data, form-type-options, hydrate-form, intrinsic-value,
  runtime-value, palette).
- New pages: dev/filter-types (custom filter types guide), dev/dca-builder
  (backend DCA building via DcaContract/DcaBuilder/ElementDcaEvent),
  dev/contracts/dca-contract, spec/filtering (filter pipeline architecture),
  reference/filter-types (built-in type catalog), migrating-from-v0.1,
  removed-in-v0.2, and an unlisted preflight-v0.2 release checklist.
- Rewrite dev/filter-elements around the buildForm/buildFilter/buildDca/
  configureOptions lifecycle, dev/events for the new event set and named
  aliases (.building/.built/.form_built/.dca), spec/specifications around
  the immutable Filter value object and the keyed addFilter() API, and
  dev/contracts/index for the new contract layout.
- Update attribute docs (#[AsFilterElement] without palette/formType/method,
  #[AsListType] without palette), engine-mods examples to
  SimpleEquationFilterElement::define() + addFilter(), Twig docs for the
  new flare_make_filter() helper, and reference tables for the new
  Filter\Element namespace and per-element form controls.
- flare_enclosure_files is documented as removed (code removal tracked on
  the preflight page).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ericges
ericges force-pushed the feat/filter-types branch 3 times, most recently from ae0b057 to 5940ad6 Compare July 19, 2026 13:11
@ericges
ericges force-pushed the feat/filter-types branch 2 times, most recently from b86320a to fdb7d89 Compare September 2, 2026 13:15
ericges and others added 21 commits September 7, 2026 17:05
…pleEquationFilterType` for extensible filter handling in FlareBundle
Filter elements now own their entire lifecycle: form building on native
Symfony FormBuilderInterface sub-builders, config schema + DCA-row
translation via ConfigContract (OptionsResolver), data-to-options
transformation in buildFilter(builder, context, data), and backend DCA
configuration via DcaContract::configureDca() on both tl_flare_filter
and tl_flare_list.

- Replace ConfiguredFilter/ConfiguredFilterCollection with immutable
  Filter DTO held as plain keyed array on ListSpecification
- Remove FilterInvocation, FilterFormBuilder, Hydrate/FormData/
  Intrinsic/RuntimeValue/FormTypeOptions contracts, and the entire
  AsFlareCallback/palette machinery (PaletteContract, PaletteEvent,
  callback registry, MethodInjector)
- Add standalone filter channels: Filter::fromCallback()/fromType()
  and flare_make_filter() Twig function
- Nest filter GET params as ?list[alias][value]=x (BC break)
- Add unit tests for Filter, FilterConfigResolver, ListSpecification
Restructure the filter subsystem under the Filter\ namespace and align
class/interface names with their roles. No behavior changes.

- Move filter elements to Filter\Element\ and collectors to
  Filter\Collector\; suffix element classes consistently
  (PublishedFilterElement, SearchKeywordsFilterElement, ...)
- Rename ConfigContract -> FilterElementOptionsInterface
  (configureConfig -> configureOptions) and FilterConfigResolver ->
  Filter\OptionsResolver\FilterOptionsResolver
- Rename DcaContract::configureDca -> buildDca; introduce
  DcaBuilderInterface and DcaFieldBuilderInterface
- Rename Form\Type\DateRangeFilterType -> DateRangeFormType, resolving
  the name collision with the query-side filter type
- Drop unused ListItemProviderConfig; update translations
- Add unit tests for Filter, FilterOptionsResolver, and
  ListSpecification; restore FilterFormListener named dispatch
… and ListSpecification classes

<!-- This aligns with the existing commit style from the history while mentioning the main action and specific additions. -->
…ent` and update references

Revised all occurrences of `AbstractFilterFilterElement` to streamline naming conventions and align with the filter subsystem structure.
…d update references

Aligned filter element naming with established conventions (`ArchiveElement` -> `ArchiveFilterElement`, etc.) and adjusted translations accordingly.
…Test` and update method/exception references
Moved `FilterOptionsResolver` and `FilterElementResolver` to `Filter\Resolver\` namespace, adjusted imports and references accordingly.
Introduce `ConfigBuilder` (fluent canonical-config accumulator), `TransformerBuilder`
(source-class to transformer map), the `TransformerContract` (`configureTransformers()`),
and `FilterTransformerResolver` (per-element-class memoized transformer execution).
Transformer maps are extensible via `FilterTransformerEvent`, re-dispatched as
`flare.filter_element.{type}.transformers`.
…rogrammatic sugar

- Elements now implement `configureTransformers()` (via `AbstractFilterElement`, which
  registers `transformFilterModel(FilterModel, ConfigBuilder)` for the FilterModel source);
  `FilterElementOptionsInterface` and `configFromRow()` are gone. `FilterOptionsResolver`
  checks the generic `OptionsInterface`.
- `ListModelFilterCollector` translates via `FilterTransformerResolver`; elements without
  a matching transformer keep the raw-row passthrough.
- New `FilterContextFactory` dedupes the identical `FilterContext` construction in
  `FilterFormFactory` and `FilterExecutor`.
- Removed programmatic sugar: all static `define()` factories, `Filter::fromType()`,
  `Filter::fromCallback()`, `CallbackFilterElement`, and the `flare_make_filter` Twig
  function. Internal call sites construct `new Filter(element:, config:)` directly.
  A proper engine-extending API is a follow-up.
… options

- `Lists\ListSpec`: immutable list DTO (type, dc, filters, canonical config, source)
  with `with*()` modifiers, `getAutoItemField()` (validated), and a config-based `hash()`.
- `ListBuilder` owns the list build lifecycle: type's `BuildListContract::buildList()`
  hook, `ListBuildEvent` (named dispatch `flare.list.{type}.build`), base + type
  transformer config assembly, schema resolution via `ListOptionsResolver`.
- `BaseListOptions`: framework-owned base schema/translation for tl_flare_list columns,
  applied unconditionally; `genericPageMeta` replaces the dynamic `eval_generic_page_meta`.
- `AbstractListType` now implements `ListTypeInterface`, `OptionsInterface`, and
  `TransformerContract` (`transformListModel()` override point).
- `ListBuilderFactory` replaces `ListSpecificationFactory` (old path still in place
  until the consumer sweep).
…ryCache` utility

Made `PaginatorConfig` non-nullable in `InteractiveContext` for improved null safety. Introduced a new `EntryCache` utility for handling cached entries in a more structured manner. Replaced positional entry lookups with ID-based indexing. Updated `ValidationLoader` and removed stale `entryCache` logic from `ValidationContextFactory`. Adjusted `composer.json` to require `doctrine/dbal` `^3.6 || ^4.0`. Refined `count()` in `PaginatorConfig` to return `1` by default if `totalItems` is unknown.
…olution mechanics

Implemented detection for duplicate filter aliases in `ListModel` and introduced backend error messages for better user feedback. Refactored `ListDriver` resolution with a dedicated `ListDriverResolver` class to centralize logic. Updated `ListSpecBuilder`, `ListSpecFactory`, and related classes to use the resolver. Enhanced backend filter info template to display alias conflicts. Added corresponding translations and adjusted `ElementDcaListener` for alias check logic.
…codebase

Aligned all `FlareException` instantiations to use the `method: __METHOD__` parameter for consistency. Updated exception messages and adjusted formatting where necessary to ensure compliance with the revised standard.
…rTransformerResolver` readability

Added `has()` and `unset()` methods in `ConfigBuilder` for better configuration management. Refactored `FilterFactory::resolveElement()` and `FilterTransformerResolver` for improved code clarity and structure.
…in contexts

Introduced the `LazyPage` utility for lazy-loading `PageModel` instances, replacing closure-based logic for jump-to pages. Updated `InteractiveContext`, `ValidationContext`, and `ReaderUrlConfigCreatorTrait` to use `LazyPage` for improved readability and consistency.
…elated tests

Updated `ListSpecFactory` and its test suite to use `ListDriverResolver` for resolving drivers. Adjusted `InteractiveContext` return type to `self` for consistency. Introduced new dependency injection in `ListSpecBuilderTest` for improved driver resolution handling. Added initializations in `ElementDcaListener` for clearer variable declarations.
…LazyPage`

Replaced `formActionPage` with `LazyPage` for improved lazy-loading capabilities. Updated `FilterFormFactory` and associated interfaces to use `createFormActionUrl()`. Removed redundant `resolveFormAction()` method for cleaner code. Adjusted tests to reflect the changes.
Moved `FormContextInterface`, `PaginatedContextInterface`, and `SortableContextInterface` from the `Interface` namespace to `Context` for consistency. Updated all associated imports and references across the codebase.
The fix from #35 added these arms to `SimpleEquationElement::__invoke()` on
`main`. This branch had already moved that `match` into
`SimpleEquationFilterType::buildQuery()`, so the rebase dropped them.
…ntime data handling

Added `FilterData` to encapsulate filter runtime data as an immutable object, replacing array-based values. Updated `buildFilter()` across filter elements and contexts to require `FilterData` instead of raw arrays. Refactored methods in `InteractiveProjector`, `AggregationContext`, and `ValidationContext` to align with the new filter data model. Enhanced tests for compatibility with `FilterData` and ensured backward compatibility where appropriate.
Deleted combined audit files as they are no longer relevant or actively maintained. This cleanup reduces clutter and ensures the repository contains only actionable and current resources.
…y-based form handling

Implemented a registry system to split filter elements' matching logic from their presentation logic. Added `FilterFormInterface` and revised `FilterElementInterface` to enable independent, per-instance form handling. Migrated intrinsic filters and related configuration to the new `formVariant` model, replacing the `intrinsic` boolean. Updated value handling with type-safe value objects and enhanced DCA palette composition for clearer backend structure.
…hip rules

Refined documentation with a clearer `ChoiceSourceContract` design, introducing a second method (`valueFromChoiceKeys()`) to handle element-defined choice keys. Documented `ArchiveFilterElement` adaptations, config re-ownership details, and resolved decisions for intrinsic branching, shared translations, and value object usage. Added sections on decode divisions, module-specific behaviors, and open design questions.
Resolve the `FilterForm` name collision: the per-filter presentation strategy
takes the name, and the whole-form machinery becomes `FilterSet`. The naming
axis is multiplicity within *Filter*, not filter-versus-list — a form belongs
to filtering, the list is exclusively output.

- §2.1 defines the four layers (`FilterSet`, `FilterForm`, `FilterFormBuilder`,
  mount) and the `src/Filter/Form/` vs. `src/Form/` split
- §2.2 carries the rename table and the rejected names with their reasons
- §12 gains Phase 0: rename plus the new `FilterSet` object, no behaviour or
  schema change, so step 1 contains only design
- §8 moves the decode loop from `InteractiveProjector::collectFilterData()` to
  `FilterSet::decode()`, which is why the aggregate is an object
- `$field` becomes `$mount` throughout (§1.2, §3.1, §3.4, §7.2) — the mounted
  node is a group, not a field, in the compound case
- §14.4 records the unresolved tension between `FilterSet` and `ListSpec::$filters`
…e FilterMount

Implements step 0 of SPEC_FILTER_FORMS.md §12 (§2.1/§2.2). `FilterForm` is the name the
upcoming per-filter presentation strategy needs, but it was occupied by the whole-form
machinery. This frees it and gives the decode loop of step 1 a home to move into.

Renames per §2.2:
- Filter\Factory\FilterFormFactory        -> Filter\Factory\FilterSetFactory
- Event\FilterFormBuildEvent              -> Event\FilterSetBuildEvent
- Event\FilterElementFormBuiltEvent       -> Event\FilterFormBuiltEvent
- NamedDispatch\FilterFormListener        -> NamedDispatch\FilterSetListener
- flare.form.{name}.build                 -> flare.filter_set.{name}.build
- flare.filter_element.{type}.form_built  -> flare.filter_form.{type}.built

The second alias is not in §2.2's table (the spec renames the event class but is silent on
its alias); it is recorded there now. Its handler moves out of FilterElementListener into a
new FilterFormListener, occupying the path the FilterSetListener rename frees. `{type}` there
is still the element type — the alias names the concern, not the key.

FilterSetFactory::create() now returns a FilterSet holding the root form plus a mount<->filter
map of FilterMount (filter, alias, FilterContext), keyed by the filter's key within
ListSpec::$filters. Only filters that actually mount get an entry. Mounts resolve lazily
against the root form, because form children may legally be added or removed by a PRE_SUBMIT
listener while the request is handled. Local variables follow §12.0: root builder `$root`,
per-filter collector `$builder`, mounted node `$mount`.

FilterMount::$filter and $context overlap deliberately; only $alias carries information the
context does not — the valid-form-name invariant established before mounting.

InteractiveProjector gains a protected createFilterSet() that hands the request to the root
form; createForm() keeps its signature and delegates. collectFilterData() is untouched — step
1 replaces it with FilterSet::decode(). InteractiveView still takes a FormInterface, so the
list-view template is unaffected.

No behaviour change beyond the two dispatch aliases; no schema change; no new contract.

BREAKING CHANGE: the service id of the renamed factory, the four event/listener class names,
and the `flare.form.{name}.build` and `flare.filter_element.{type}.form_built` dispatch
aliases. The `flare.form.date_range.*` validator translation keys are unrelated and unchanged.
Answers SPEC_FILTER_FORMS.md §14.1, which asked for §9's "value objects need no hashing
interface" claim to be measured against ListSpec::hash() before step 1 commits to it.
Reaches the real hashing path with no new production code, via Filter::$data.

§9's core claim holds: equal-but-distinct readonly value objects of scalars, arrays, enums
and nested value objects hash identically and survive a serialize() round trip. Every hazard
§9 names is confirmed (DateTimeImmutable timezone representation, array order, model-like
mutation state, closures throwing).

One finding §9 does not anticipate: serialize() is not a pure value function over an object
graph. A repeated object is emitted as a back-reference (r:N;), so the hash differs depending
on whether two filters share one value instance or hold two equal ones. Today's code is immune
only because Filter::fingerprint() flattens through FilterData::toArray(); §8's plan to move
the hashing role onto the value object removes that flattening. Recorded in §14.1.

Committed separately so deleting the probe after step 1 is a clean revert. The finding in
§14.1 is meant to outlive it.
…handling

Introduced `FilterFormInterface` for registry-based form management, decoupling filter elements from their forms. Added immutable filter value objects (`BoolValue`, `ChoiceValue`, `DateRangeValue`, etc.) to ensure consistency and enforce type safety. Enhanced palette configuration with the new `formVariant` model for better backend clarity. Implemented comprehensive tests to validate flattening, normalization, and runtime compliance of value objects.
Replaces all occurrences of `FilterType` with `FilterLogic` to better reflect its purpose of encapsulating logical filtering operations rather than type definitions. Updates dependencies, registry mappings, tests, and documentation to adhere to the new naming convention. Introduced `FilterConditionsBuilder` and `FilterConditions` to replace `FilterQueryBuilder` and `FilterQuery`, aligning with the broader terminology shift. Includes minor adjustments to exception handling and logging strings for clarity.
…c` to `Logic`

Replaced all occurrences of `FilterBuilder` with `LogicSequencer` to improve conceptual clarity, aligning it with its purpose of sequencing logical operations. Similarly, renamed `FilterLogic` and related classes to `Logic` with corresponding updates in tests, events, registry mappings, and documentation. Includes necessary refactors in method and property names to reflect this change across the codebase.
Updated terminology to improve alignment with evolving functional purposes. Renamed `FilterSet` to `FormHarness` across the codebase, including factories, events, namespaces, and documentation. Adjusted method signatures and type declarations where relevant. Ensured all references to `FilterSetFactory`, `FilterSetBuildEvent`, and `FilterMount` now reflect the updated `FormHarness` naming convention. Includes updates to specs, schema references, and tests.
…erFormInterface` doc comments

Added `AbstractFilterForm` as a base class for filter forms aligning with `FilterFormInterface`. Refined `FilterFormInterface` PHPDoc for clarity and simplified method parameters (`$mount` to `$form`). Adjusted tests to reflect changes in method signatures (`decode`).
…cate `LogicStep` and `FilterExecutor`

Rebranded all `Logic` classes and interfaces as `Predicate` to improve naming consistency and clarity across the codebase. Updated dependencies, tests, events, and registry mappings accordingly. Removed `LogicStep` and `FilterExecutor` entirely, along with their usages, as part of a broader simplification. Adjusted `ListSpec` filters to omit alias generation, enforcing cleaner filter registration logic. Includes fixes and updates to relevant PHPDoc, method signatures, and event dispatch aliasing (`LogicSequencerInterface` → `FormulaBuilderInterface`).
…ormalization

Removed outdated attributes and validations (`default`, redundant PHPDoc) from filter forms, improving clarity. Updated constructors and methods of value classes (`BoolValue`, `DateRangeValue`, etc.) for consistency, immutability, and type safety. Introduced `normalizeTimestamp()` in `CalendarCurrentPredicate` and improved `DateTimeImmutable` usage in `DateRangeValue`. Adjusted references and registry mappings for streamlined behavior across filter forms.
…line config handling

Decoupled alias generation, moved config resolution logic from FilterContextBuilderFactory and FilterContext into FilterFactory using FilterOptionsResolver. Updated constructors and methods across affected classes for improved consistency. Enhanced PHPDoc with templates and refined method signatures for type safety and clarity.
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.

1 participant