feat: support starts_with and ends_with operators in local evaluation - #209
Conversation
Adds local evaluation for the starts_with, not_starts_with, ends_with, and not_ends_with property filter operators (PostHog/posthog#72992). Matching is case-insensitive and mirrors icontains: both sides are stringified and lowercased, and the not_ variants are exact negations. Generated-By: PostHog Code Task-Id: ef980bb5-ff81-4191-a7df-796e932b8251
posthog-php Compliance ReportDate: 2026-08-05 15:32:17 UTC ✅ All Tests Passed!46/46 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
The changeset previously claimed these operators fell back to remote evaluation; unrecognized operators actually evaluated to false locally. Tests now cover the missing-key inconclusive path and explicit null property values for all four operators. Generated-By: PostHog Code Task-Id: bb15888a-04c4-4dc0-916e-793a0540ece3
Prompt To Fix All With AI### Issue 1
test/FeatureFlagLocalEvaluationTest.php:336
**Duplicate operator test matrices**
The starts-with and ends-with tests manually repeat the same positive, negated, case-insensitive, numeric, null, and nonmatching scenarios. A parameterised matrix would keep the shared contract in one place and prevent sibling operators from receiving inconsistent coverage as these cases change.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "Fix changeset wording and add missing-ke..." | Re-trigger Greptile |
marandaneto
left a comment
There was a problem hiding this comment.
One non-blocking parity question from the cross-SDK review.
| } | ||
|
|
||
| if ($operator == "starts_with") { | ||
| return str_starts_with(strtolower(FeatureFlag::valueToString($overrideValue)), strtolower(FeatureFlag::valueToString($value))); |
There was a problem hiding this comment.
strtolower() performs ASCII-only case conversion, so international values can differ from the Python SDK’s Unicode casefold() behavior (for example, Äpfel does not start with ä here). The current implementation does match PHP’s existing icontains convention and the authoritative flags service, which uses ASCII-only lowering. Would you prefer server/PHP consistency as implemented, or literal Python SDK parity for Unicode values? Either choice seems defensible, but a non-ASCII test or documentation note would make the intended contract explicit.
There was a problem hiding this comment.
I decided to match existing PHP conventions. In all the SDKs, I matched local conventions.
I think we may want to try and match what happens on the server as closely as possible. I opened a separate issue on that: PostHog/posthog#78019
I'd rather do that as an all or nothing follow-up.
marandaneto
left a comment
There was a problem hiding this comment.
One forward-compatibility concern from the cross-SDK review.
| "posthog-php": minor | ||
| --- | ||
|
|
||
| Support the `starts_with`, `not_starts_with`, `ends_with`, and `not_ends_with` property filter operators in feature flag local evaluation. Matching is case-insensitive and mirrors `icontains`. Previously, local evaluation treated these operators as unrecognized and silently evaluated their conditions to `false`; they now match correctly. |
There was a problem hiding this comment.
The previous silent-false behavior still applies to any future operator the server adds: matchProperty() reaches its final return false, so the condition can produce an incorrect local result without triggering remote fallback. The other SDK implementations generally treat an unknown operator as inconclusive and fall back to /flags. Could we change the unknown-operator path to throw InconclusiveMatchException and add a regression test using something like future_operator? This is pre-existing behavior, but this PR demonstrates the forward-compatibility risk.
Unknown operators now throw InconclusiveMatchException so the flag defers to the /flags endpoint instead of silently evaluating to false, matching the other SDKs. Non-ASCII tests pin the intentional ASCII-only strtolower contract, which mirrors the flags service. Generated-By: PostHog Code Task-Id: bb15888a-04c4-4dc0-916e-793a0540ece3

💡 Motivation and Context
PostHog/posthog#72992 added four case-insensitive property filter operators —
starts_with,not_starts_with,ends_with,not_ends_with— across HogQL, the flags service, and the UI. The UI surface is gated behind a feature flag until server-side SDKs can evaluate these operators locally; on SDK versions without support, flags using them fall back to remote evaluation.This adds local evaluation support for the four operators, mirroring
icontains: both sides are stringified and lowercased, and thenot_variants are exact negations. Filter values are single strings — server-side validation rejects list values for these operators, matching the flags service behavior.💚 How did you test it?
New unit tests mirror the flags service's own test matrix for these operators: case-insensitive anchored matching, mid-string non-matches (
prevaluedoes not matchstarts_with: "Val"), numeric stringification (323matchesstarts_with: "3",123does not), negation inverses, and missing-key inconclusive behavior.vendor/bin/phpunit test/FeatureFlagLocalEvaluationTest.phppasses locally (109 tests, 2531 assertions), andcomposer run api:checkconfirms the public API snapshot is unchanged.📝 Checklist
If releasing new changes
Created with PostHog Code