Skip to content

Add pluggable escaper resolver for the Like filter#375

Merged
dereuromark merged 1 commit into
fixes-extraparams-callback-likefrom
feature-escaper-resolver-v2
May 11, 2026
Merged

Add pluggable escaper resolver for the Like filter#375
dereuromark merged 1 commit into
fixes-extraparams-callback-likefrom
feature-escaper-resolver-v2

Conversation

@dereuromark
Copy link
Copy Markdown
Member

Summary

Exposes the Like filter's driver-to-escaper mapping as a config option (escapers), so apps register custom escapers for custom or subclassed drivers without subclassing the filter. Builds on the per-query driver resolution from #374.

API

use App\Database\Driver\MyMariaDb;

$searchManager->like('title', [
    'escapers' => [
        MyMariaDb::class => 'App.MyMariaDb',
    ],
]);

The shipped defaults (Sqlserver::class => 'Search.Sqlserver', Postgres::class => 'Search.Postgres') are merged in via Cake's normal _defaultConfig behavior, so apps only specify their additions. Match is instanceof-based, so a subclassed driver still resolves correctly. Entries are evaluated in iteration order; the first match wins. When nothing matches, Search.Default is used.

Implementation

  • Adds 'escapers' => [Sqlserver::class => 'Search.Sqlserver', Postgres::class => 'Search.Postgres'] to Like::$_defaultConfig.
  • Replaces the in-line driver match in _setEscaper() with a new protected hook _resolveEscaperClass(Driver $driver) so subclasses can also override the entire resolution policy.
  • The escaper option still works as before: if set, it pins a specific escaper regardless of the active driver. The new escapers option only kicks in when escaper is null (the default).

Tests

Two new cases:

  • testCustomEscaperViaEscapersMap — a driver subclass listed in escapers is resolved before the shipped Sqlserver/Postgres defaults, demonstrating the override path.
  • testEscaperFallsBackToDefaultWhenNoMatch — when the map is explicitly empty and no shipped entry matches the active driver, Search.Default is used.

All gates green locally: phpunit (158 tests, 340 assertions), phpstan level 8 (clean), phpcs (clean).

Docs

docs/filters-and-examples.md updated alongside the code: the escaper option's description was simplified and a new escapers entry was added with a worked example, ordering note, and pointer to the EscaperInterface.

Dependency

This PR is based on the branch from #374 (extraParams leak, Callback null contract, per-query Like escaper resolution). It will not merge cleanly until #374 is merged or this is rebased onto master after #374 lands.

Recommended order: land #374 first, then rebase this on master and merge.

Builds on the per-query driver resolution from the previous commit
in this branch by exposing the driver-to-escaper mapping as a
config option. Apps register a custom escaper for a custom or
subclassed driver by extending the map at filter setup, without
having to subclass the filter:

    $searchManager->like('title', [
        'escapers' => [
            App\Database\Driver\MyMariaDb::class => 'App.MyMariaDb',
        ],
    ]);

The shipped defaults (Sqlserver -> Search.Sqlserver, Postgres ->
Search.Postgres) are merged in via Cake's normal `_defaultConfig`
behavior, so users only specify their additions. Entries are
evaluated in iteration order; the first instanceof match wins.
When nothing matches, `Search.Default` is used.

The previous in-line driver match in `_setEscaper()` is replaced
by a new protected hook `_resolveEscaperClass(Driver $driver)`,
which subclasses can also override for custom resolution logic.

Adds two tests: custom escaper picked via map override, and the
fallback to Search.Default when the map is explicitly empty.

Updates docs/filters-and-examples.md to document `escapers` next
to `escaper`, including a worked example and a note about
ordering and instanceof matching.
@dereuromark dereuromark force-pushed the feature-escaper-resolver-v2 branch from 9c5f0fd to fc32a71 Compare May 11, 2026 17:32
@dereuromark dereuromark merged commit 5cdddae into fixes-extraparams-callback-like May 11, 2026
7 checks passed
@dereuromark dereuromark deleted the feature-escaper-resolver-v2 branch May 11, 2026 20:19
dereuromark added a commit that referenced this pull request May 11, 2026
…374)

* Prevent extraParams from leaking between filters.

Processor::process() mutated the shared $filterParams map in place
when merging in extraParams for a given filter, which meant filters
declared later in the chain still saw those values even though they
hadn't requested them via their own extraParams config. Build a
per-filter view (`$currentParams`) instead so each filter only sees
its own merge.

Adds a regression test asserting that a filter without extraParams
does not see another filter's extra value.

* Deprecate null returns from Callback::process().

The Callback filter's process() method returned `?? true`, which
silently coerced a missing/void return from the user's callback
into isSearch=true. The documented contract is that callbacks must
return bool to drive isSearch(). That mismatch caused subtle bugs:
a callback that forgets the return statement is reported as having
filtered when it may not have.

Preserve the historical behaviour for one minor (null still maps
to true) but emit a deprecation pointing at the offending callback
so users can fix it. The next minor can hard-fail.

Updates the existing testProcess case to explicitly return true
and adds a new test asserting the deprecation fires when a callback
returns null.

* Resolve Like escaper per-query and add a Postgres branch.

Driver detection in Like::_setEscaper() was a substring suffix
match against get_class($driver), which mis-detects custom or
subclassed drivers, and the resolved escaper class was written
back into the filter's config so a reused filter instance kept
the first detected escaper even when the next query ran against
a different connection.

Switch to `instanceof` against the driver instance, add a
Postgres branch (new PostgresEscaper class that currently
inherits from DefaultEscaper but lets driver-specific wildcard
rules diverge later), and stop caching the result so each call
to _setEscaper() resolves afresh.

Adds tests for:
- Sqlserver detection against a sub-classed driver,
- Postgres detection,
- the same filter resolving different escapers across two
  queries backed by different drivers.

* Force E_USER_DEPRECATED in callback test for lowest-deps matrix.

* Update src/Model/Filter/Callback.php

Co-authored-by: ADmad <admad.coder@gmail.com>

* Add pluggable escaper resolver for the Like filter. (#375)

Builds on the per-query driver resolution from the previous commit
in this branch by exposing the driver-to-escaper mapping as a
config option. Apps register a custom escaper for a custom or
subclassed driver by extending the map at filter setup, without
having to subclass the filter:

    $searchManager->like('title', [
        'escapers' => [
            App\Database\Driver\MyMariaDb::class => 'App.MyMariaDb',
        ],
    ]);

The shipped defaults (Sqlserver -> Search.Sqlserver, Postgres ->
Search.Postgres) are merged in via Cake's normal `_defaultConfig`
behavior, so users only specify their additions. Entries are
evaluated in iteration order; the first instanceof match wins.
When nothing matches, `Search.Default` is used.

The previous in-line driver match in `_setEscaper()` is replaced
by a new protected hook `_resolveEscaperClass(Driver $driver)`,
which subclasses can also override for custom resolution logic.

Adds two tests: custom escaper picked via map override, and the
fallback to Search.Default when the map is explicitly empty.

Updates docs/filters-and-examples.md to document `escapers` next
to `escaper`, including a worked example and a note about
ordering and instanceof matching.

---------

Co-authored-by: ADmad <admad.coder@gmail.com>
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.

2 participants