douglasgreen / php-linter
Code linter for PHP based on Nikic parser
Requires
- php: ^8.3
- justinrainbow/json-schema: >=5.3
- league/commonmark: ^2.8
- nikic/php-parser: ^5.7
- pdepend/pdepend: ^2.16.2
- phpstan/phpdoc-parser: ^2.3
- symfony/yaml: >=6.4
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94.2
- phpstan/phpstan: ^2.1.39
- phpunit/phpunit: ^12.5.14
- rector/rector: ^2.3.8
README
| title | Code linter | ||||||
|---|---|---|---|---|---|---|---|
| description | A static analysis and metrics tool for PHP based on the Nikic PHP parser and PDepend | ||||||
| tags |
|
||||||
| audience | Developers | ||||||
| last_updated | 2026-02-24 | ||||||
| reading_time | 8 min |
Code linter
Code linter is a static analysis and metrics evaluation tool for PHP codebases. It uses the
nikic/php-parser library to generate an Abstract Syntax Tree (AST) for deep stylistic analysis,
and it wraps pdepend/pdepend to evaluate software metrics and code complexity.
Why Code linter
This project is being developed as a potential replacement for PHP Mess Detector (PHPMD). Compared to PHPMD, this project:
- Offers more metrics: Utilizing PDepend, it tracks advanced metrics including Code Rank, Afferent/Efferent Coupling, Comment to Code Ratio, Halstead Effort, Maintainability Index, and Lines of Code per File.
- Provides predefined settings: Thresholds for each metric are predefined mostly by a study of PHP code. The limits are set at a level at which your code exceeds 99% of the metrics of similar open-source code.
- Promotes a better workflow: PHPMD allows you to trigger an error, suppress it, and ignore it forever. Code linter, instead, provides an advisory report every time without offering inline suppressions. The problem with PHPMD's approach is that once you suppress an error, your code can grow in complexity without limits without getting further warnings.
- Processes more code: PHPMD relies solely on the parse tree provided by PDepend, which completely ignores code outside of classes and functions. Code linter solves this by running the PDepend check and a separate linting check using the Nikic PHP Parser that analyzes your entire codebase, including standalone files.
Prerequisites
- PHP 8.3 or later
- Composer
- Git repository (the linter requires a Git context to verify the main branch and file index)
Install the linter
Add the package to your project as a development dependency using Composer:
composer require --dev douglasgreen/php-linter
Run the linter
Execute the script from the root directory of your repository.
vendor/bin/php-linter
IMPORTANT: You must run the script from the repository root. The linter relies on
git ls-files, parses thecomposer.jsonfile for PSR-4 namespace mappings, and utilizes avar/cache/pdependdirectory for caching metric analysis.
Sort JSON files
The linter can automatically sort composer.json and package.json files into conventional key
order. Use the --fix flag to sort these files:
vendor/bin/php-linter --fix
When the --fix flag is provided, the linter skips all other checks and only sorts the JSON files.
This is useful for maintaining consistent file structure across your project.
Note: The sorting follows the official schema documentation for each file type. Keys not in the standard order are appended at the end in their original relative order.
Add to Composer scripts
To integrate the linter into your continuous integration (CI) pipeline or daily workflow, add it to
the scripts section of your composer.json file:
{
"scripts": {
"lint": ["vendor/bin/php-linter"]
}
}
Run the configured script with:
composer lint
Checked metrics
The analyzer enforces limits on various software metrics. Exceeding these thresholds indicates code that may be overly complex, tightly coupled, or difficult to maintain.
Class-level metrics
- Class Size: Max 60 (Total number of methods and properties).
- Lines of Code (Class): Max 1,100.
- Code Rank: Max 2.0 (Measures class centrality and responsibility).
- Properties: Max 25 total properties, and Max 30 non-private properties.
- Public Methods: Max 40.
- Afferent Coupling: Max 45 (Incoming dependencies).
- Efferent Coupling: Max 24 (Outgoing dependencies).
- Object Coupling: Max 24 (Coupling between objects).
- Inheritance Depth: Max 5.
- Child Classes: Max 35.
Method and Function-level metrics
- Lines of Code (Method): Max 130.
- Cyclomatic Complexity: Max 25 (Extended cyclomatic complexity).
- NPath Complexity: Max 10,000 (Acyclic execution paths).
- Halstead Effort: Max 135,000.
- Maintainability Index: Min 25 (Code falls below this limit is considered hard to maintain).
File-level metrics
- Comment to Code Ratio: Min 0.05 (5% of code should be documented).
- Lines of Code (Standalone files): Max 200 (For lines executed outside of classes and functions).
Checked linting issues
The AST linter analyzes your code for the following stylistic and structural issues:
Naming conventions
- CamelCase usage: Classes, interfaces, and traits must use
UpperCamelCase. Methods, functions, and variables must uselowerCamelCase. - Constants: Must be in
ALL_CAPS. - Name length: Global names (classes/methods) should be 3–32 characters. Local variables should be 1–24 characters.
- PSR suffixes/prefixes: Abstract classes must be prefixed with
Abstract, traits must be suffixed withTrait, and interfaces must be suffixed withInterface. - Boolean naming: Boolean return functions should start with declarative verbs (e.g.,
is,has,can). - Verb-based naming: Non-boolean functions should start with an imperative verb.
Code structure & PSR standards
- PSR-1 compliance: Checks for constants and functions that should be moved from the top-level namespace to a class namespace.
- PSR-4 compliance: Verifies that the file path matches the class namespace and name based on
composer.json. - Visibility order: Properties and methods must be ordered by visibility:
public, thenprotected, thenprivate. - Member ordering: Properties must be defined before methods.
Best practices & Modernization
- DTO suggestions: Identifies arrays accessed with string keys as parameters or return types and suggests using Data Transfer Objects (DTOs) instead.
- PHP 4 Constructors: Flags old-style constructors (methods named after the class).
- Strict Loading: Suggests
require_onceoverincludeorrequire. - Superglobals: Flags use of superglobal variables outside of allowed contexts (like Controllers, Middleware, or global scope).
Potential bugs & Cleanup
- Unused code: Detects unused private properties, unused private methods, unused function parameters, unused classes, interfaces, or traits, and unused functions.
- Redundant variables: Flags variables that are assigned but only used once.
- Magic numbers: Detects duplicate numeric literals used across the code and suggests defining them as constants.
- Empty blocks: Flags empty
catchblocks that suppress errors.
Security & Forbidden syntax
- Dangerous functions: Flags the use of
eval(). - Global scope: Flags the use of the
globalkeyword. - Unstructured code: Flags the use of
gotostatements. - Error suppression: Flags the use of the
@operator. - Execution flow: Flags
exitordieinside functions/classes, suggesting exceptions instead. - Debug leftovers: Flags common debug calls like
var_dump(),print_r(), ordebug_backtrace().
Repository health
- Git branch: Verifies the default repository branch is named
main.
Composer.json standards
- Basic structure: Validates required fields:
name,description,type. - Package name: Ensures lowercase, hyphenated format matching
owner/packagepattern. - Package type: Validates against allowed types (library, project, composer-plugin, etc.).
- License: Checks for presence and optionally validates against expected license.
- Keywords: Validates presence, checks for duplicates and non-empty strings.
- PHP version: Ensures PHP version constraint is present and meets minimum (8.3+).
- Autoload: Validates PSR-4 autoloading configuration and namespace conventions.
- Config settings: Checks for
sort-packagesand warns about platform overrides. - Dependencies: Validates version constraints, warns about wildcards and dev branches.
- Scripts: Security checks for dangerous commands in post-install/post-update scripts.
- Binaries: Validates binary files exist and are executable.
- Support info: Checks for issues URL and valid source URLs.
- Public packages: Additional validation for homepage, authors, and keywords.
Documentation standards
- Required files: Checks for essential files like README.md, CHANGELOG.md, LICENSE, and documentation structure.
- File naming: Validates kebab-case naming convention for Markdown files.
- File encoding: Ensures UTF-8 encoding and Unix line endings (LF).
- Heading structure: Validates single H1, no skipped heading levels, and sentence case.
- Code blocks: Checks for language specification in fenced code blocks.
- Writing style: Flags fluff words, passive voice, and future tense.
- Links: Validates internal links, flags bare URLs and non-descriptive link text.
- Security: Detects exposed API keys, tokens, passwords, and other credentials.
- Frontmatter: Validates YAML frontmatter structure and recommended fields.
- Orphaned files: Identifies Markdown files not linked from other documentation.
Package.json standards
- Basic structure: Validates required fields:
name,version,description. - Package name: Ensures lowercase, hyphenated format matching
@scope/packagepattern for scoped packages. - Package type: Validates against allowed types (module, commonjs, esm, cjs).
- License: Checks for presence and cross-file consistency with composer.json.
- Keywords: Validates presence, checks for duplicates and forbidden generic terms.
- Engines: Ensures Node.js and npm version constraints are specified and meet minimums.
- Dependencies: Validates dev tools are in devDependencies, warns about wildcards and version inconsistencies.
- Scripts: Checks for standard scripts (lint, test, format) when tools are installed, and flags dangerous commands.
- Binaries: Validates binary files exist, are executable, and in the
bin/directory. - Exports: Recommends exports field for ES modules.
- Deprecated configs: Flags legacy config files (eslintrc, tslint, etc.) and suggests modern alternatives.
- File locations: Validates files are in appropriate directories based on type.
- Tooling configs: Checks Prettier, ESLint, and Stylelint configurations are present and plugins are properly configured.
- Cross-file consistency: Compares project name, description, and license with composer.json.
- Public packages: Additional validation for homepage, repository, author, and contributors.
PHPDoc Standards
- Missing Documentation: Public API elements (classes, interfaces, traits, enums, public methods, and public properties) must have a DocBlock.
- Summary Formatting: DocBlocks for non-magic functions must start with a summary line under 80 characters, starting with a capital letter and ending with a period.
- Mandatory Tags:
- Classes must have either
@apior@internal. - Properties without native types must have a
@vartag.
- Classes must have either
- Tag Ordering: Tags must follow a specific order (e.g.,
@apibefore@param). - Complex Types: Bare
arraytypes are forbidden; use typed generics syntax (e.g.,list<string>,array<string, int>).
Configure the ignore list
The linter supports an ignore list similar to .gitignore. Create a .phplintignore file in the
root of your repository to exclude specific paths or files from analysis.
Syntax rules
- Lines starting with
#act as comments. *matches any number of characters.?matches a single character.
Example configuration
# Ignore third-party and configuration files
config/*.php
# Ignore build artifacts
build/*.tmp.php
Configure error ignoring
Create a php-linter.json file in the root of your repository to configure the linter. This file
supports two main configuration options: ignoring specific issue types and customizing metric
limits.
Configuration structure
The configuration file contains two optional arrays:
ignoreIssues: An array of issue strings to ignore. The strings must match the exact issue text reported by the linter.metricLimits: An object mapping metric names to custom limit values. These override the default constants defined in the Analyzer class.
{
"ignoreIssues": [
"Remove unused private non-static method MyClass::unusedMethod() to reduce dead code.",
],
"metricLimits": {
"classSize": 80,
"classLoc": 1500,
"methodLoc": 200,
"cyclomaticComplexity": 30,
"npathComplexity": 15000,
"halsteadEffort": 200000,
"maintainabilityIndex": 20,
"commentRatio": 0.03,
"properties": 30,
"nonPrivateProps": 35,
"publicMethods": 50,
"afferentCoupling": 60,
"efferentCoupling": 30,
"objectCoupling": 30,
"inheritanceDepth": 6,
"childClasses": 50,
"codeRank": 3.0,
"fileLoc": 300
}
}
Note on ignoreIssues: The ignore list matches the complete issue message text, not short codes.
To find the exact strings to ignore, run the linter first and copy the issue messages you want to
suppress into the ignoreIssues array. When an issue message is listed in ignoreIssues, all
instances of that exact issue will be suppressed from the output.
Note on metricLimits: All metric limit keys are optional. If a key is not specified, the linter uses its default value (the class constant defined in the Analyzer). The example above shows all available metric limit keys with their default values.
Custom configuration file path
When using the linter programmatically, you can specify a custom path to the configuration file:
use DouglasGreen\PhpLinter\Config; // Use default php-linter.json in current directory $config = new Config($currentDir); // Use a custom configuration file $config = new Config($currentDir, '/path/to/custom-config.json');
Disclaimer
This project is not affiliated with or endorsed by the PHP Group.