Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Psalm Plugin Stringf Laravel Package

boesing/psalm-plugin-stringf

A Psalm plugin that statically validates printf and sprintf format strings, checking that placeholders match the number and types of arguments passed. Mismatches are caught during analysis instead of at runtime, using Psalm's type inference to verify format specifiers across your codebase.

Deep Wiki
Context7

Getting Started

Install it as a dev dependency alongside Psalm:

composer require --dev boesing/psalm-plugin-stringf
vendor/bin/psalm-plugin enable boesing/psalm-plugin-stringf

The enable command writes the registration into psalm.xml. If you configure manually, add the plugin class:

<plugins>
    <pluginClass class="Boesing\PsalmPluginStringf\Plugin"/>
</plugins>

Then clear Psalm's cache and run against a single file so the new diagnostics are visible immediately:

vendor/bin/psalm --no-cache src/Formatting/MessageFormatter.php

Where to look first: any file that builds strings with sprintf/printf, or parses with sscanf/fscanf. Logging, i18n, CLI output, and fixed-width parsing code are the highest-yield targets.

First use case: a message formatter with a missing or mistyped placeholder.

// Before: Psalm says nothing, runtime emits "ValueError: too few arguments"
return sprintf('User %s logged in from %s at %d', $name, $ip);

Once the plugin is enabled, the arity mismatch and the type mismatch on %d surface as Psalm issues at the call site, in the same report you already run in CI.

Implementation Patterns

Keep format strings inline and literal. The plugin resolves string literals and constants; that is the whole basis of its analysis. The most reliable pattern is a literal first argument with a fixed argument list:

sprintf('Processed %d of %d records in %.2fs', $done, $total, $elapsed);

Promote reusable formats to class constants rather than concatenating at the call site. Constants stay resolvable, concatenation usually does not:

final class Formats
{
    public const LOGIN = 'User %s logged in from %s at %d';
}

sprintf(Formats::LOGIN, $name, $ip, $timestamp);

Use it as a CI gate, not an editor-only nicety. Because the plugin only adds diagnostics to existing rules, it drops into an existing composer psalm / vendor/bin/psalm --threads=4 step with no new tooling. Adopt it behind a baseline first (see below) so the PR is about the plugin, not about hundreds of pre-existing format strings.

Pair printf and sprintf checks with sscanf/fscanf parsing code. If you parse log lines or fixed-format input, the same plugin validates the parse pattern against the assignment targets:

[$level, $message] = sscanf($line, '[%s] %[^$]');

This is where the plugin earns its keep on parsers: a format specifier that no longer matches the destructured variables is caught at analysis time instead of producing null fields in production.

Wrap translation deliberately, not accidentally. A helper like t('Hello %s', $name) hides the sprintf call, so the plugin can no longer see the format string at the call site. Two workable layouts:

  • Keep sprintf at the call site with a literal format and let a translator return the already-resolved format only where static checking is not worth it: sprintf($translator->get('login'), $name).
  • Accept the coverage loss for i18n and reserve the plugin for the non-translated paths (logs, CLI, parsers) where formats really are literals.

When adopting on a legacy codebase, baseline in two passes:

vendor/bin/psalm --set-baseline=psalm-baseline.xml
vendor/bin/psalm

Then delete baseline entries file-by-file as you fix real bugs. Do not leave the plugin unbaselined on day one — it will drown out unrelated findings.

Gotchas and Tips

  • Only statically resolvable formats are checked. A format built by concatenation, sprintf('%s ' . $suffix, $x), or returned from a function silently gets no analysis. It is not an error, so the absence of issues is not proof of correctness.
  • Variadic spreads are skipped. sprintf($format, ...$args) cannot be arity-checked. The same goes for call_user_func('sprintf', ...) and similar indirection.
  • PHP's sprintf is loose; the plugin is not. PHP happily coerces "42" into %d and an int into %s. Expect the plugin to flag some calls that "work fine at runtime", especially %s with non-string scalars or Stringable objects. Cast at the call site ((string) $value) or add a narrow @psalm-suppress rather than loosening your config globally.
  • Positional specifiers change the counting rules. %1$s / %2$d reorder arguments; verify your specific call against the plugin after enabling rather than assuming one-to-one positional matching. %% and width/precision flags (%05.2f) are parsed, but nested/combined specifiers are the first place to look when a real mismatch is missed.
  • Cache invalidation after enabling. If no new issues appear, run once with --no-cache and confirm the plugin actually loaded (Psalm warns about unresolved plugin classes).
  • Version compatibility is the main risk. The last release is from 2022-12, and Psalm's plugin API has moved since. Pin psalm/psalm to a version you have verified, or run the plugin's own tests against the Psalm version in your lockfile before rolling out. Do not assume it works against the newest Psalm major.
  • Check plugin ordering if you already use type-altering plugins. Plugins that rewrite inferred types or format strings can run before or after this one; if diagnostics vanish, put this plugin first in <plugins> and re-test.
  • Translation and logging helpers are invisible to it. __(), _e(), Log::info('... %s', …) and similar wrappers are ordinary functions. The plugin only knows the four native functions. If you need the same checks for your own helper, you will have to write a small Psalm plugin (a function-call analyzer or return-type provider) — this package exposes no user-facing extension point or config.
  • Licence is declared NOASSERTION. If you have compliance review, confirm the actual licence from the repository's LICENSE file before shipping it into a locked-down CI image.
  • Small project, low maintenance signal (12 stars, last release 2022). Treat it as a narrow, replaceable lint layer: keep it dev-only, keep the dependency pinned, and be ready to drop it if it breaks on a Psalm upgrade.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource