
Fast static analyzer for PHP, written in Rust. Finds maintainability and runtime risks with zero configuration required.
PHP codebases grow. As they grow, they collect technical debt: classes that do too much, methods that are hard to follow, and hidden risks that only appear under real traffic. Most tools check syntax, style, or types. Phanalist checks whether the code will stay maintainable and safe as it grows.
It looks at two core things:
In plain terms, Phanalist helps you answer questions like:
Each finding explains what is wrong, why it matters, and how to fix it. You do not need to be a static-analysis expert to use it.
eval(), unsafe deserialization (unserialize), weak randomness, and insecure cookies.phanalist.yaml.text, json, sarif for GitHub code scanning, and codeclimate for code-quality platforms.Run the installation script in your terminal:
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/denzyldick/phanalist/main/bin/init.sh | sh
Navigate to your PHP project root and run:
~/phanalist
(On the first run, phanalist.yaml is automatically created with sensible defaults).
Phanalist outputs actionable findings with file paths, line numbers, and clear explanations. To see violations per rule summary only, run:
~/phanalist --summary-only
| Tool category | What it usually checks | What Phanalist adds | Run together? |
|---|---|---|---|
| Type analysis | Types, nullability, and contracts | Architecture, runtime/resource, and security findings | Yes |
| Style and formatting | Coding standard and layout | Design and runtime risk | Yes |
| Architecture and metrics | Coupling, complexity, and dependency rules | Actionable findings with explanations and fix guidance | Yes, or use instead |
| Security scanning | Broad source-to-sink analysis | Targeted PHP-specific hazards | Yes |
| Quality dashboard | History, trends, and team reporting | SARIF and Code Climate output for your existing dashboard | Yes |
Add inline SARIF annotations to your PRs using the Phanalist GitHub Action:
name: Phanalist
on: [pull_request]
permissions:
security-events: write
jobs:
phanalist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denzyldick/phanalist-action@v1
By default, the action appends each ruleβs full documentation to inline pull request annotations.
Additional CLI flags:
| Flag | Description | Default |
|---|---|---|
--config, -c |
Path to configuration file | ./phanalist.yaml |
--src, -s |
Path(s) to project sources (repeatable, e.g. -s src -s tests) |
./src |
--rules, -r |
Only run these rules (overrides config) | from config |
--output-format, -o |
Output format: text, json, sarif, codeclimate |
text |
--summary-only |
Show only violation counts per rule | β |
--quiet, -q |
Suppress all output | β |
--verbose, -v |
Increase verbosity; repeat for more (-v main pass, -vv parsing, -vvv indexing) |
β |
--debug-rule-timing |
Print per-rule per-file timing | β |
--debug-rule-stats |
Print per-rule cost/coverage stats | β |
--use-baseline |
Filter results against a baseline file, reporting only new violations | β |
--update-baseline |
Regenerate the baseline from the current scan | β |
--blame |
Attribute violations to engineers via git blame and show a quality report | β |
--since |
Only count violations from commits after this date (e.g. "30 days", "2025-01-01") |
β |
--until |
Only count violations from commits before this date | β |
--export-chart |
Export engineer chart as PNG/SVG image (requires --blame) |
β |
--exclude-author |
Exclude authors from the report (repeatable, e.g. --exclude-author dependabot) |
β |
--min-violations |
Minimum total violations to include an engineer in the report | 0 |
--lsp |
Start as a Language Server (LSP) for editor integrations | β |
Use the --blame flag to see who is introducing and fixing violations in your codebase:
# Who has violations in their code currently
~/phanalist --blame
# See what changed in the last 30 days (Net column shows who is improving vs degrading)
~/phanalist --blame --since "30 days"
# Exclude bots and sort by net improvements
~/phanalist --blame --since "30 days" --exclude-author dependabot --sort net
| Column | Meaning |
|---|---|
| Engineer | Git author name |
| Fixed (β) | Violations that disappeared compared to the --since snapshot |
| Introduced (β) | Violations that appeared compared to the --since snapshot |
| Net | Fixed - Introduced β positive means improving, negative means degrading |
A baseline lets you adopt Phanalist on an existing codebase without fixing every finding at once. It freezes current violations; later runs report only new ones so CI stays green on known debt but fails on regressions.
# Generate baseline
~/phanalist --use-baseline phanalist-baseline.json --update-baseline
# Run against baseline in CI
~/phanalist --use-baseline phanalist-baseline.json
Phanalist organizes rules into four internal analysis sections. You can configure thresholds or disable rules in phanalist.yaml:
ast): Checks syntax structure, formatting conventions, naming rules, method complexity metrics, and class design.verification): Analyzes control flow to prove runtime safety (such as preventing division by zero, null errors, and array bounds violations).resource): Measures time complexity, memory allocation growth, and untrusted loop cardinality.security): Detects security vulnerabilities, unclosed file/stream resources, and dangerous execution sinks.enabled_rules: [] # empty = all rules active
disable_rules: []
exclude_paths:
- vendor
- node_modules
rules:
E0007:
max_parameters: 5
E0009:
max_complexity: 10
E0031:
php_version: "8.4"
timeout_ms: 1000
E0044:
runtime_threshold: quadratic
New to the terminology? Check the Rule Glossary. Every rule links to detailed, junior-friendly documentation with bad/good examples and junior rules of thumb.
Type: Syntax, metrics, structure, and design checks.
| Code | Name | Primary Option / Threshold |
|---|---|---|
| E0000 | Example rule | β |
| E0001 | Opening tag position | β |
| E0002 | Empty catch block | β |
| E0003 | Method visibility modifiers | β |
| E0004 | Uppercase constants | β |
| E0005 | Capitalized class name | β |
| E0006 | Property visibility modifiers | β |
| E0007 | Method parameters count | max_parameters: 5 |
| E0008 | Return type signature | β |
| E0009 | Cyclomatic complexity | max_complexity: 10 |
| E0010 | Npath complexity | max_paths: 200 |
| E0011 | Error suppression symbol (@) |
β |
| E0012 | Service Shared Memory Model compatibility | include_namespaces, reset_interfaces |
| E0013 | Unused private method | β |
| E0014 | Law of Demeter | β |
| E0015 | Method Cohesion (LCOM4) | threshold: 1 |
| E0016 | Cognitive complexity | max_complexity: 15 |
| E0017 | Coupling Between Objects (CBO) | max_coupling: 10 |
| E0018 | Weighted Methods per Class (WMC) | max_wmc: 50 |
| E0019 | Response For a Class (RFC) | max_rfc: 50 |
| E0020 | Depth of Inheritance Tree (DIT) | max_depth: 4 |
| E0021 | Number of Children (NOC) | max_children: 15 |
| E0022 | Afferent/Efferent Coupling (Ca/Ce) | max_ca: 20, max_ce: 20 |
| E0023 | Instability, Abstractness, Distance | max_distance: 0.5 |
| E0024 | Lines of Code per Method | max_loc: 30 |
| E0025 | Lines of Code per File | max_loc: 500 |
| E0026 | Comment Ratio | min_ratio: 0.1, max_ratio: 0.5 |
| E0027 | God Class (Brain Class) | max_methods: 15, max_fields: 10 |
| E0028 | Data Class | min_methods: 3 |
| E0029 | Fan-in / Fan-out | max_fan_out: 10, max_fan_in: 20 |
| E0030 | Cyclomatic Complexity Density | max_density: 0.3 |
Type: Path analysis and runtime safety checks.
| Code | Name | What it Proves / Checks |
|---|---|---|
| E0031 | Division by Zero | Proves that divisors are never zero |
| E0032 | Modulo by Zero | Proves that modulo divisors are never zero |
| E0033 | intdiv() by Zero |
Proves integer division divisors are valid |
| E0034 | intdiv() Overflow |
Prevents integer overflow during division |
| E0035 | Missing Array Key | Proves that required array keys exist before access |
| E0036 | Array Index Out of Bounds | Proves array index access stays within bounds |
| E0037 | String Offset Out of Bounds | Proves string character offsets are valid |
| E0038 | Method Call on Null | Proves objects are not null before calling methods |
| E0039 | Property Access on Null | Proves objects are not null before accessing properties |
| E0040 | Return Contract Violation | Verifies return types match documented contracts |
| E0041 | Unreachable Code | Detects dead statements after returns or throws |
| E0042 | Impossible Assertion | Detects assertions that can never hold true |
| E0043 | Stream Resource Used After Close | Detects reading/writing streams after closing them |
Type: Scalability, memory growth, and time complexity checks.
| Code | Name | What it Analyzes |
|---|---|---|
| E0044 | Input-Dependent Runtime Growth | Flags loops whose time complexity scales poorly with input size |
| E0045 | Input-Dependent Live-Heap Growth | Flags memory allocation patterns that grow with input size |
| E0046 | Input-Controlled Cardinality | Flags collections sized directly from untrusted user input |
Type: Security hazards and resource leak checks.
| Code | Name | Security / Lifecycle Risk |
|---|---|---|
| E0047 | Resource Lifecycle Leak | Unclosed file handles or database connections |
| E0048 | Unbounded Direct Recursion | Infinite recursion without base condition changes |
| E0049 | Builtin Argument Validation | Invalid literal arguments passed to built-in functions |
| E0050 | Position Check Used as Boolean | Using strpos() without strict comparison (!== false) |
| E0051 | Count of Non-Countable Value | Calling count() on non-array scalars |
| E0052 | Unchecked json_decode Result |
Missing validation after JSON decoding |
| E0053 | Unserialize Without allowed_classes |
PHP object injection risk |
| E0054 | Dynamic Variable Creation | Variable variables ($$x) or extract() usage |
| E0055 | Dynamic Include or Require | Unsafe dynamic file inclusion paths |
| E0056 | Weak Randomness for Security | Using rand() or mt_rand() for security tokens |
| E0057 | MD5 or SHA1 for Passwords | Insecure hashing algorithms for credentials |
| E0058 | Cookie Missing Secure or HttpOnly | Cookies missing security flags |
| E0059 | Dynamic Eval or Assert | Dangerous code execution sinks (eval()) |
Adding a new rule is straightforward β this tutorial explains how.
Phanalist includes built-in LSP support to provide real-time, as-you-type diagnostics directly in your editor (phanalist --lsp).
Read a series of chapters on https://dev.to/denzyldick to understand the projectβs internals.