fix: coerce string CLI inputs for Boolean params#52
fix: coerce string CLI inputs for Boolean params#52ChiragAgg5k merged 2 commits intoutopia-php:masterfrom
Conversation
CLI args arrive as strings via getopt. When a param's validator is `Boolean` (loose mode), strings like "true"/"false"/"1"/"0" pass validation but remain strings. If the action callback declares a `bool` parameter, PHP's implicit string-to-bool cast turns any non-empty string except "0" into `true` -- so `--commit=false` silently becomes `true` and the action runs as if `--commit=true` had been passed. Validators in utopia-php are pure (validate only, never mutate), so the coercion has to happen here at the dispatch boundary in `CLI::getParams()`. After validation, if the param's validator (unwrapping `Nullable`) is `Boolean` and the value is still a string, we run it through `filter_var(..., FILTER_VALIDATE_BOOLEAN)` before handing it to the callback. Bool defaults and non-string inputs pass through untouched, so this is a safe, additive change for existing callers.
Greptile SummaryThis PR fixes a silent Confidence Score: 5/5Safe to merge — the fix is additive, well-scoped, and comprehensively tested. No P0 or P1 issues found. The coerce() logic correctly guards against non-string values, empty strings, callable validators, nested Nullable wrappers, and unrecognized strings (via FILTER_NULL_ON_FAILURE). Tests cover all documented cases and edge paths. Existing 18 tests are unchanged. No files require special attention. Important Files Changed
Reviews (2): Last reviewed commit: "fix: preserve empty-string sentinel in c..." | Re-trigger Greptile |
Optional params with `--flag=` (or default `''`) bypass `validate()` entirely, so they reach `coerce()` un-validated. The previous version ran them through `filter_var($value, FILTER_VALIDATE_BOOLEAN)`, which silently returns `false` for empty strings -- a behaviour change for callers (e.g. Cloud's `Patch.php`) that use `''` as a three-state "not set" sentinel before resolving to `null`. Now coerce returns early for empty strings, and uses `FILTER_NULL_ON_FAILURE` for everything else so any unrecognised input falls back to the original value rather than getting silently downgraded to `false`. Spotted by Greptile review on PR utopia-php#52.
Summary
CLI args arrive as strings via getopt. When a param's validator is
Boolean(true)(loose mode), strings like"true"/"false"/"1"/"0"pass validation but remain strings. If the action callback declares aboolparameter, PHP's implicit string-to-bool cast turns any non-empty string except"0"intotrue— so--commit=falsesilently becomestrue, and the action runs as if--commit=truehad been passed.This bit a real production task: a CLI script ran with
--commit=falseto dry-run, but the write committed anyway because(bool) "false" === truein PHP.What changed
CLI::getParams()now passes each validated value through a smallcoerce()helper before storing it in the params array. If the param's validator (unwrappingNullable) isBooleanand the incoming value is still a string, it's normalised viafilter_var(..., FILTER_VALIDATE_BOOLEAN). Bool defaults and non-string inputs pass through untouched.Validators in utopia-php are pure (validate only, never mutate), so the coercion has to live at the dispatch boundary on the consumer side. CLI is the right place because every CLI arg starts life as a string.
Why not at the validator layer
utopia-php/validatorsvalidators only validate — they never mutate. Keeping that contract intact, the only place to bridgestring→boolfor CLI dispatch is here.Behaviour
new Boolean(true)--c=falsetruefalse✅new Boolean(true)--c=truetruetruenew Boolean(true)--c=0falsefalsenew Boolean(true)--c=1truetruenew Boolean(true)new Nullable(new Boolean(true))--c=falsetruefalse✅new Text(64)--n=false"false""false"Non-Boolean validators are completely untouched.
Test plan
"false","true","0","1"inputs toBoolean(true)params (via data provider)Nullable(Boolean(true))unwrappingText) where string"false"must remain the string"false"composer lintpassescomposer check(phpstan) passes```
$ composer test
OK (25 tests, 41 assertions)
```
Compatibility
This is additive. Existing callers that already pass real `bool` values continue to work. Existing callers that passed `"false"` and accidentally got `true` were already buggy — this fix gives them the behaviour they presumably intended. Anyone who needs the literal string `"false"` should be using `Text`, not `Boolean`.