lychee-org/phpstan-sensitive-parameter-values
A PHPStan rule that flags sensitive values like passwords, tokens, and API keys passed directly as literal arguments to functions and methods, helping you catch hardcoded secrets before they reach your codebase.
Install it as a dev dependency alongside PHPStan:
composer require --dev lychee-org/phpstan-sensitive-parameter-values:^1.2
If phpstan/extension-installer is present, the extension registers automatically through its extra.phpstan.includes entry. Otherwise, add the registration file (typically extension.neon inside the package) to your phpstan.neon:
includes:
- vendor/lychee-org/phpstan-sensitive-parameter-values/extension.neon
The package targets PHP 8.2+ (#[\SensitiveParameter]) and 8.3+ (SensitiveParameterValue), so confirm your CI matrix and phpstan platform version before wiring it in.
Where to look first: the README for the supported PHPStan range, then the registration file to see exactly which services are declared, then the extension class in src/ — that class is the whole point of the package and reading it takes two minutes. Check the changelog too: 1.2.0 ("Remove false positives", #3) tightens what the extension reports, so if you tried an earlier 1.0.x build and got noisy diagnostics, that is the version worth retrying.
First use case: somewhere in your codebase you already have something like
final class Password
{
private SensitiveParameterValue $value;
public function __construct(#[\SensitiveParameter] string $value)
{
$this->value = new SensitiveParameterValue($value);
}
public function reveal(): string
{
return $this->value->getValue();
}
}
Run PHPStan before and after installing. Before, reveal() returns mixed (or whatever SensitiveParameterValue::getValue() is stubbed as) and every caller needs a cast or an @var. After, reveal(): string is satisfied natively and downstream code type-checks against string instead of mixed. That diff is the fastest way to confirm the extension is loaded and doing something.
Wrap at the trust boundary, unwrap at the sink. The useful shape is: controller/command receives the raw value → immediately wraps it → wrapper travels through service layers → getValue() is called exactly once, at the point where the value is hashed, compared, or sent over the wire. With the extension installed, PHPStan tracks the wrapped type across that whole journey, so intermediate layers don't need docblocks or assertions.
Pair the attribute with the wrapper. #[\SensitiveParameter] redacts the raw argument in stack traces; SensitiveParameterValue redacts it in var_dump/print_r output. Put the attribute on the constructor parameter that accepts the raw string, then store only the wrapper. Doing one without the other leaves one leak channel open and PHPStan won't tell you.
Lean on constructor-call-site inference. The extension infers the wrapped type from the new SensitiveParameterValue(...) expression. That means a factory with a precise signature gives you precise types everywhere:
/** @param non-empty-string $raw */
public static function fromString(#[\SensitiveParameter] string $raw): self
{
return new self(new SensitiveParameterValue($raw));
}
Add a narrow accessor and treat it as the review surface. A single reveal(): string (or ->getValue() calls) per sensitive type gives you a greppable list of every place a secret becomes plaintext. In code review, rg 'reveal\(|getValue\(\)' is a fast audit. The static analyser can't enforce that discipline, but the type inference makes the accessor cheap to write.
Keep it in the same PHPStan invocation. There's no separate command — the extension just enriches the existing analysis. In CI, run it in your normal phpstan analyse step; no extra pipeline leg needed.
Use it to clean up existing casts. If you already have SensitiveParameterValue in production code, installing this package usually lets you delete @var inline annotations, assert() calls, and mixed workarounds around getValue(). Do that in a dedicated PR so you can see the type-inference improvement in isolation.
Reuse the pattern for your own wrappers. If you have similar newtype/redaction objects (e.g. an API token wrapper), the dynamic return type extension in this package is a good template — mirror its registration for your own classes rather than casting at every call site.
SensitiveParameterValue hides values from var_dump, print_r, and debug_backtrace, but not from error_log, Monolog, or json_encode. The extension gives you types; it does not give you log redaction. Don't let installing it create a false sense of safety.SensitiveParameterValue is not serializable. Queued jobs, session payloads, and cache entries containing the wrapper will fail or produce unexpected output. Unwrap before serialization, or write a normalizer that stores an opaque token.mixed, from a container get(), or built in a file PHPStan can't fully resolve will degrade back to mixed. If reveal() suddenly widens, look at the constructor call that created the wrapper.reportUnmatchedIgnoredErrors (on by default) will flag them. Run phpstan analyse on a clean cache, delete the now-dead baseline entries, and review the remainder rather than blanket-regenerating.phpstan analyse --debug and check the extension list, then reorder the includes.vendor/bin/phpstan clear-result-cache after installing, upgrading (including 1.0.1 → 1.2.0), or reordering extensions. Stale cache is the single most common reason "it doesn't work."mixed-driven errors into different, more specific errors. Regenerate the baseline in the same commit that adds or upgrades the extension so the diff is readable — especially on the 1.x → 1.2.0 hop, where removed false positives shrink the baseline.lychee-org/phpstan-sensitive-organization-values-style deps with ^1.2 and pin phpstan/phpstan in composer.json so a minor bump doesn't silently disable inference in CI.phpstan.neon. If phpVersion is below 8.3, the SensitiveParameterValue class may not resolve and you'll get "class not found" noise instead of useful analysis.string $password that is later logged; only the attribute plus the wrapper together give you both static tracking and runtime redaction.assertType()-style PHPStan test (or a tiny fixture project) so a dependency bump that breaks inference shows up as a failing test rather than a silent mixed — and so a future false-positive cleanup doesn't quietly drop coverage you were depending on.How can I help you explore Laravel packages today?