Skip to content

fix: coerce string CLI inputs for Boolean params#52

Merged
ChiragAgg5k merged 2 commits intoutopia-php:masterfrom
ChiragAgg5k:fix/loose-bool-string-coercion
Apr 27, 2026
Merged

fix: coerce string CLI inputs for Boolean params#52
ChiragAgg5k merged 2 commits intoutopia-php:masterfrom
ChiragAgg5k:fix/loose-bool-string-coercion

Conversation

@ChiragAgg5k
Copy link
Copy Markdown
Member

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 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.

This bit a real production task: a CLI script ran with --commit=false to dry-run, but the write committed anyway because (bool) "false" === true in PHP.

What changed

CLI::getParams() now passes each validated value through a small coerce() helper before storing it in the params array. If the param's validator (unwrapping Nullable) is Boolean and the incoming value is still a string, it's normalised via filter_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/validators validators only validate — they never mutate. Keeping that contract intact, the only place to bridge stringbool for CLI dispatch is here.

Behaviour

Validator CLI input Before After
new Boolean(true) --c=false true ⚠️ false
new Boolean(true) --c=true true true
new Boolean(true) --c=0 false false
new Boolean(true) --c=1 true true
new Boolean(true) omitted default default
new Nullable(new Boolean(true)) --c=false true ⚠️ false
new Text(64) --n=false "false" "false"

Non-Boolean validators are completely untouched.

Test plan

  • Existing 18 tests still pass
  • New tests cover string "false", "true", "0", "1" inputs to Boolean(true) params (via data provider)
  • New test covers Nullable(Boolean(true)) unwrapping
  • New test covers default-value path when param omitted
  • New test covers non-Boolean validator (Text) where string "false" must remain the string "false"
  • composer lint passes
  • composer 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`.

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-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR fixes a silent string→bool miscast for CLI params backed by a Boolean validator: getopt always delivers strings, so --commit=false became (bool) "false" === true at the action callback boundary. A new coerce() helper in getParams() normalises validated string values to native PHP bools using filter_var(..., FILTER_NULL_ON_FAILURE), with proper unwrapping of Nullable wrappers, pass-through of non-string defaults, and preservation of the empty-string sentinel. The change is well-tested and non-breaking.

Confidence Score: 5/5

Safe 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

Filename Overview
src/CLI/CLI.php Adds a coerce() helper called from getParams() that converts validated string inputs to native PHP bools when the param's validator is Boolean; handles callable validators, unwraps nested Nullable wrappers, preserves empty strings, and uses FILTER_NULL_ON_FAILURE for safe pass-through of unrecognized strings.
tests/CLI/CLITest.php Adds 7 new tests covering string→bool coercion (data provider for "false"/"true"/"0"/"1"), default-value pass-through, Nullable unwrapping, empty-string sentinel preservation, and non-Boolean validator pass-through.

Reviews (2): Last reviewed commit: "fix: preserve empty-string sentinel in c..." | Re-trigger Greptile

Comment thread src/CLI/CLI.php Outdated
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.
@ChiragAgg5k ChiragAgg5k merged commit 145b91f into utopia-php:master Apr 27, 2026
7 checks passed
@ChiragAgg5k ChiragAgg5k deleted the fix/loose-bool-string-coercion branch April 27, 2026 09:19
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