jonbaldie / explicitness-checker
A tool to analyze explicitness of PHP code (implicit inputs/outputs).
Package info
github.com/jonbaldie/explicitness-checker
Type:phpstan-extension
pkg:composer/jonbaldie/explicitness-checker
Requires
- php: >=8.0
- nikic/php-parser: ^5.1
- phpstan/phpstan: ^2.1
Requires (Dev)
- phpmd/phpmd: ^2.15
- phpunit/phpunit: ^9.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v1.1.0
- v1.0.0
- dev-release/1.1.0
- dev-fleet/worktree-explicitness-checker-73
- dev-feat/72-normand-implicit-io
- dev-fleet/worktree-explicitness-checker-48
- dev-fleet/worktree-explicitness-checker-42
- dev-fleet/worktree-explicitness-checker-27
- dev-fleet/ci-b9f9f6d98d7c41fea026cdf8ac57468a-1-a3de9a87d70d
This package is auto-updated.
Last update: 2026-09-24 02:12:38 UTC
README
This is an explicitness parsing tool that you can run on your PHP projects to analyze the explicitness of your code.
What is explicitness?
Simply put, explicitness refers to the clarity of data flow through your functions. Ideally all of your inputs should be provided through function arguments, and all of your outputs should be returned through function return values.
function add($a, $b) { return $a + $b; // explicit input and output: all values are provided through function arguments and returned through function return values }
If your function accesses global variables not provided through its arguments, that is an implicit input. If your function changes global state without returning a value, that is an implicit output.
$some_global_number = 10; function add($a, $b) { global $some_global_number; echo "Calling add on $a and $b...\n"; // implicit output: printing to stdout --$some_global_number; // implicit output: decrementing global variable $some_global_number return $a + $b + $some_global_number; // implicit input: reading from global variable $some_global_number }
How does the tool work?
The tool uses nikic's PHP Parser library to parse PHP code and analyze the explicitness of your functions and class methods.
Which functions are checked, and how they're named
Every function-like with a body is checked on its own, wherever it's declared, and whether or not the file declares a namespace:
| Declared as | Reported as |
|---|---|
Function (including inside if blocks or other functions) |
Fully qualified name, e.g. App\Sub\send_mail |
| Method of a named class, trait or enum | Fully qualified class, e.g. App\Sub\Mailer::send |
Method of an anonymous class (new class { ... }) |
class@anonymous::send, or App\Service::class@anonymous::send when nested in a named class |
Property hook (PHP 8.4 get/set), including on a promoted constructor parameter |
Fully qualified class, property and hook, e.g. App\Sub\Temperature::$celsius::get, or class@anonymous::$celsius::get for an anonymous class; App\Service::class@anonymous::$celsius::get when nested in a named class |
| Closure or arrow function | {closure} |
Abstract and interface methods, and hooks declared without a body, aren't checked. Hooks are reported even when the tool itself runs on an older PHP version. Rows are listed in source order.
A function's analysis stops at any closure, arrow function, nested function or class declared inside it: code in there, including global declarations, belongs to that inner function-like, which gets its own row. So a closure that declares global $x doesn't make the enclosing function's local $x look like a global.
Behaviour change: earlier versions skipped closures, arrow functions, anonymous-class methods and conditionally declared functions in files without a namespace, dropped the namespace from method names, and let a closure's global declarations leak into the enclosing function. Upgrading can therefore add rows, remove false global findings from enclosing functions, rename methods, and change the exit code.
Why should I care about explicitness?
Implicit inputs and outputs fundamentally limit the modularity and reusability of your code.
Functions with implicit dependencies are like electronic components that are hardwired to other components - they can't be easily detached and used elsewhere. When a function reads from global variables (implicit inputs) or writes to global state or performs side effects like DOM manipulation (implicit outputs), it becomes tightly coupled to its environment. This means you can only use that function in very specific contexts where those global dependencies are available and properly configured. In contrast, functions with explicit inputs (arguments) and outputs (return values) are like modular connectors that can be plugged into any compatible system.
The practical consequences of implicit inputs and outputs make your code significantly harder to test, debug, and reason about.
Implicit inputs limit when you can call a function because you must ensure all the global state is properly set up beforehand, and you have to worry about other code potentially interfering with those shared variables. Implicit outputs similarly constrain when you can call a function - you can only call it when you actually want those side effects to occur. This makes testing particularly challenging because you must set up all the implicit inputs, run the function, and then verify all the implicit outputs, which becomes exponentially more complex as the number of implicit dependencies grows. Functions with only explicit inputs and outputs are much easier to test because you simply pass in arguments and check the return value, with no external setup or cleanup required.
Installation
Install via Composer:
composer require jonbaldie/explicitness-checker --dev
Laravel
Run the checker on app:
./vendor/bin/explicitness-checker app
-
Blind spot. Helpers (
env(),config(),request(),now(), ...) and facade calls that take arguments (DB::table('orders'),Cache::get($key),Log::info($message), ...) are ordinary calls to the checker, so I/O through them reports as explicit. Facade calls with no arguments, such asAuth::user(), are reported as static method calls. A clean result covers only superglobals,global,$GLOBALS, static method calls with no arguments, static properties, writes through arguments,staticvariables, by-reference closure captures, and with--strictbuilt-ins such asgetenv,time,file_get_contents,curl_exec,execandmysqli_query. -
--propsalso reports constructor-injected services ($this->orders), so expect it to flag most controllers and services. -
PHPStan with Larastan. Add this package's config next to the Larastan include from Larastan's docs:
includes: - vendor/larastan/larastan/extension.neon - vendor/jonbaldie/explicitness-checker/extension.neon
With
phpstan/extension-installer, delete both lines instead. The installer loads both extensions, and a file included twice makes PHPStan abort with "This file is included multiple times".
How do I run the tool?
./vendor/bin/explicitness-checker [--verbose] [--strict] [--props] [--exclude=dir] [--include-pattern=pattern] [--exclude-pattern=pattern] [--min-explicitness=percent] ./path/to/your/project
Flags
--verboseor-v: Enable verbose output showing detailed analysis progress--strict: Enable strict mode which detects additional implicit I/O patterns:- Standard output (
echo,print,printf,ob_*, etc.).print_randvar_exportwithreturn: truearen't reported - File I/O (
file_get_contents,fwrite,fgets, etc.) and file system checks and writes (file_exists,unlink,mkdir, etc.) - Environment variables, HTTP headers, sessions and error logging (
getenv,header,session_start,error_log,syslog, etc.) - System time (
time,hrtime,new DateTime(), etc.).date,mktime,date_createand the rest of the date family are reported only when they read the clock, not when they're given a timestamp or date - Randomness (
rand,shuffle,uniqid,new Random\Randomizer()without an engine, etc.) - Network (
curl_exec,fsockopen, etc.), databases (mysqli_*,pg_*), external processes (exec, backticks, etc.), email (mail),include/require, and runtime configuration (ini_set,set_error_handler,define, etc.) - Superglobals read through
filter_inputandfilter_input_array
- Standard output (
--props: Also report instance property access ($this->property). Static properties are reported without it.
Any other argument starting with - is an unknown option. It stops the run before anything is analysed: Unknown option: <argument> and the usage line go to stderr and the exit code is 2, so a mistyped flag such as --stict fails the build instead of quietly turning a check off.
Directory and File Filtering
--exclude=directoryor--exclude directory: Exclude specific directories from analysis- Can be used multiple times to exclude multiple directories
- By default,
vendor/is excluded - Example:
--exclude=tests --exclude=cache
--include-pattern=patternor--include-pattern pattern: Only analyze files matching the regex pattern- Can be used multiple times; a file is analyzed if it matches any of the include patterns
- Example:
--include-pattern="src/.*\.php$"to only analyze PHP files in src/
--exclude-pattern=patternor--exclude-pattern pattern: Exclude files matching the regex pattern- Can be used multiple times; a file is excluded if it matches any of the exclude patterns
- Example:
--exclude-pattern="test.*\.php$"to exclude test files
- Every pattern given is checked before anything is analysed. A pattern that isn't a valid regular expression stops the run: the reason and the usage line go to stderr and the exit code is 2, so a mistyped filter fails the build instead of quietly analysing the wrong files.
- Example:
--include-pattern="src/("printsInvalid --include-pattern: Compilation failed: missing closing parenthesis at offset 6
- Example:
Filtering Examples
# Exclude vendor and tests directories ./vendor/bin/explicitness-checker --exclude=vendor --exclude=tests ./project # Only analyze files in src/ directory ./vendor/bin/explicitness-checker --include-pattern="src/" ./project # Exclude all test files ./vendor/bin/explicitness-checker --exclude-pattern="test.*\.php$" ./project # Combine multiple filters ./vendor/bin/explicitness-checker --exclude=vendor --exclude-pattern=".*Test\.php$" ./project # Repeat a pattern flag to exclude several things at once ./vendor/bin/explicitness-checker --exclude-pattern=".*Test\.php$" --exclude-pattern="/generated/" ./project
Functional core, imperative shell. To exempt console commands, controllers or other shells while still checking the core logic they call, in either the CLI or PHPStan, see docs/functional-core-imperative-shell.md.
Severity Levels and Exit Codes
The tool categorizes violations into three severity levels:
Items marked strict are reported only with --strict (the PHPStan strict parameter); the rest are always on.
- Minor (Exit code 1): Simple output operations
echo,print,var_dump,print_r(strict)
- Serious (Exit code 2): Shared state access
- Global variables (
global,$GLOBALS) - Superglobals (
$_GET,$_POST,$_SESSION, etc.) - Static method calls with no arguments (
ClassName::method()) - Static properties (
ClassName::$property) - Writes through arguments (
$cart[] = $itemorsort($cart)witharray &$cart,$product->price = 1) staticvariables and by-reference closure captures (use (&$x))- Instance properties (
$this->property) when--propsis enabled
- Global variables (
- Critical (Exit code 3): System-level implicit I/O
- Environment access:
$_ENV, andgetenv(strict) - File operations (
file_get_contents,fwrite,unlink, etc.) (strict) - Time functions (
time,date,microtime,new DateTime()) (strict) - Random functions (
rand,random_int,shuffle) (strict) - HTTP headers (
header,setcookie) (strict) - Session functions (
session_start,session_id) (strict) - Error logging (
error_log,trigger_error,syslog) (strict) - Network, databases, external processes, email,
include/requireand runtime configuration (strict)
- Environment access:
By-reference built-ins. A built-in that takes an argument by reference, such as sort, array_push or preg_match's $matches, writes to it. Passing it a by-reference parameter, a global, a superglobal, a static variable or a by-reference capture is reported as a write. Iterating by reference (foreach ($cart as &$line)) and binding a reference ($r = &$cart, [&$first] = $cart) count the same way. The checker asks the PHP that runs it which parameters are by reference, so the result depends on the extensions loaded there: a call to a function from an extension that isn't loaded isn't reported, and two machines with different extensions can report different results for the same code. User-defined functions, and calls that unpack their arguments (sort(...$lists)), are never treated as by-reference.
Behaviour change: $_ENV access used to be Serious, like the other superglobals. It is now Critical, the same as getenv, so a run whose worst finding is $_ENV access now exits with 3 instead of 2.
Exit Codes for CI Integration
- 0: No violations found
- 1: Only minor violations found
- 2: Serious violations found (may include minor)
- 3: Critical violations found (may include serious and minor)
The tool exits with the highest severity level found, making it easy to integrate into CI pipelines with appropriate failure thresholds.
If a PHP file cannot be parsed, the tool reports the parse error, continues checking the other files, and exits with at least code 2. A critical violation in another file still raises the exit code to 3.
Explicitness Percentage Gate
--min-explicitness=percent or --min-explicitness percent lets a codebase pass while it still has some implicit function-likes, as long as enough of them are explicit. It's meant for adopting the tool gradually: set the minimum to where the codebase is today and raise it over time.
- A function-like is explicit when it has no findings. The percentage is explicit function-likes ÷ checked function-likes × 100, in the current mode, so
--strictand--propscan lower it. - When the percentage is at or above the minimum, violations no longer set the exit code, so the run exits 0. A file that fails to parse still makes it exit at least 2: the gate waives violations, not files it couldn't check. Below the minimum, the run exits with the usual severity code. The
Exit code:line in the summary shows the code the run actually exits with. - The comparison is exact: 7 of 8 is 87.5%, which meets
87.5but not87.51. The displayed percentage is rounded down to one decimal place, so 2 of 3 shows as 66.6% and still meets66.66. - A run that checks no function-likes is 100% explicit, so it meets any minimum. Files that fail to read or parse count towards neither total.
- The minimum must be a plain decimal number from 0 to 100, such as
80or87.5. Anything else, including-5,101,1e2,.5and an empty value, stops the run:Invalid --min-explicitness: <value>and the usage line go to stderr and the exit code is 2. - If the flag is repeated, the last value wins. Given without a value, it's ignored.
With a minimum set, the summary ends with one more line:
Exit code: 0
Explicit function-likes: 2 of 3 (66.6%, minimum 50%)
On a clean run it follows No implicit inputs or outputs found., without the indent.
Example Output
$ ./vendor/bin/explicitness-checker ./path/to/your/project
Analyzing...
Results:
+---------------------------------------+------+----------+-----------------------------------------------+----------------------------------------------+----------+
| File | Line | Function | Implicit Inputs | Implicit Outputs | Severity |
+---------------------------------------+------+----------+-----------------------------------------------+----------------------------------------------+----------+
| ./path/to/your/project/Calculator.php | 4 | add | read from global variable $some_global_number | wrote to global variable $some_global_number | Serious |
+---------------------------------------+------+----------+-----------------------------------------------+----------------------------------------------+----------+
Summary:
Critical violations: 0 (exit code 3)
Serious violations: 1 (exit code 2)
Minor violations: 0 (exit code 1)
Exit code: 2
PHPStan extension
This package also ships a PHPStan rule that reports the same implicit inputs and outputs as the CLI, as PHPStan errors. It finds and names function-likes the same way the CLI does (see "Which functions are checked, and how they're named" above): a function-like the CLI reports is reported by the rule under the same name, wherever it's declared. The one thing the extension doesn't inherit from the CLI is file selection: the CLI walks the path you give it, filtered by --exclude/--include-pattern/--exclude-pattern; the rule analyses whatever paths (and excludePaths) your own PHPStan configuration already includes. See #12 for the history here.
Installation
With phpstan/extension-installer, the extension is picked up automatically once both packages are required — no further configuration needed:
composer require --dev phpstan/extension-installer jonbaldie/explicitness-checker
Without it, require the package and include its config yourself:
composer require --dev jonbaldie/explicitness-checker
# phpstan.neon includes: - vendor/jonbaldie/explicitness-checker/extension.neon
Either way, run vendor/bin/phpstan analyse as usual; violations appear alongside your other PHPStan errors.
strict and props parameters
explicitness.strict and explicitness.props mirror the CLI's --strict and --props flags and both default to false:
parameters: explicitness: strict: true props: true
Categories and identifiers
Every error the rule reports carries one of these explicitness.<category> identifiers. Categories marked strict or props only appear once the matching parameter above is true; the rest are always on.
| Category | Identifier | Enabled by |
|---|---|---|
Global variable (global $x) |
explicitness.globalVariable |
default |
Superglobal ($_GET, $_ENV, etc.; filter_input with strict) |
explicitness.superglobal |
default |
$GLOBALS array |
explicitness.globalsArray |
default |
Static method call with no arguments (ClassName::method(), not self::, parent:: or static::) |
explicitness.staticCall |
default |
Static property (ClassName::$property) |
explicitness.staticProperty |
default |
| Write through a by-reference parameter or an object argument's property | explicitness.argumentMutation |
default |
static variable (static $x) |
explicitness.staticVariable |
default |
By-reference closure capture (use (&$x)) |
explicitness.capturedReference |
default |
Standard output (echo, print, var_dump, ob_start, ...) |
explicitness.standardOutput |
strict |
File I/O (file_get_contents, fwrite, fgets, ...) |
explicitness.file |
strict |
File system checks and writes (file_exists, is_dir, unlink, mkdir, ...) |
explicitness.fileSystem |
strict |
Environment variables (getenv, putenv) |
explicitness.environment |
strict |
System time (time, date, microtime, new DateTime(), ...) |
explicitness.time |
strict |
Random number generator (rand, random_int, shuffle, uniqid, ...) |
explicitness.random |
strict |
HTTP headers (header, setcookie, ...) |
explicitness.httpHeaders |
strict |
Error log (error_log, trigger_error, syslog, ...) |
explicitness.errorLog |
strict |
Session (session_start, session_id, ...) |
explicitness.session |
strict |
Network (curl_exec, fsockopen, gethostbyname, ...) |
explicitness.network |
strict |
Database (mysqli_*, pg_*) |
explicitness.database |
strict |
External process (exec, shell_exec, backticks, proc_open, ...) |
explicitness.process |
strict |
Email (mail, mb_send_mail) |
explicitness.mail |
strict |
Included file (include, require, include_once, require_once) |
explicitness.include |
strict |
Runtime configuration (ini_set, ini_get, set_error_handler, define, ...) |
explicitness.runtimeConfig |
strict |
Object property ($this->property) |
explicitness.objectProperty |
props |
Ignoring or baselining a category
To ignore a category everywhere, add it to ignoreErrors by identifier:
parameters: ignoreErrors: - identifier: explicitness.standardOutput
To grandfather in existing violations instead, run vendor/bin/phpstan analyse --generate-baseline. Each generated baseline entry keeps its identifier, so you can hand-edit phpstan-baseline.neon afterwards to drop the entries for a category you'd rather start enforcing straight away.
Overlap with jonbaldie/phpstan-extension-accessing-globals
jonbaldie/phpstan-extension-accessing-globals also reports on global and superglobal access, under its own identifiers. If you install both, the same line can be reported twice:
explicitness.globalVariableandexplicitness.globalsArrayoverlap with that extension's defaultaccess.globalandmodify.globalidentifiers (global $xand$GLOBALS[...]access and modification).explicitness.superglobaloverlaps with its defaultaccess.superglobal.nestedandmodify.superglobal.nestedidentifiers, since this extension only ever checks function bodies, which that extension always treats as a nested scope.explicitness.staticPropertyoverlaps with its opinionatedproperty.staticidentifier, if you've enabled that extension's opinionated rule set.- With
explicitness.strict: true,explicitness.time,explicitness.random,explicitness.environment,explicitness.file,explicitness.fileSystem,explicitness.httpHeaders,explicitness.errorLog,explicitness.session,explicitness.network,explicitness.process,explicitness.mailandexplicitness.runtimeConfigoverlap with its opinionatedfunction.impureidentifier, which reports calls to a fixed list of impure built-in functions. The lists only partly match:function.impurealso covers functions this extension doesn't (strtotime,getcwd,realpath,php_uname, ...) and reportsdateandmktimeeven when they're given a timestamp, and it doesn't coversrand,setrawcookie,trigger_error,curl_exec,popen,defineorinclude, among others.explicitness.standardOutput,explicitness.databaseandexplicitness.includedon't overlap.
Pick one extension as the source of truth for each pair and ignore the other's identifier for it, e.g. to keep jonbaldie/phpstan-extension-accessing-globals as the source of truth for global and superglobal access:
parameters: ignoreErrors: - identifier: explicitness.globalVariable - identifier: explicitness.globalsArray - identifier: explicitness.superglobal
For function.impure, neither side is a superset of the other, so ignoring either one loses some coverage. If you run both extensions strictly, one option is to keep this extension's per-category identifiers and ignore function.impure:
parameters: ignoreErrors: - identifier: function.impure