sebastiaanwouters / diffalyzer
Analyze git changes and run only affected PHP tests and static analysis. Speeds up PHPUnit, Psalm, ECS, and PHP-CS-Fixer in CI/CD pipelines.
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
pkg:composer/sebastiaanwouters/diffalyzer
Requires
- php: ^8.1
 - nikic/php-parser: ^5.0
 - symfony/console: ^6.4 || ^7.0
 - symfony/finder: ^6.4 || ^7.0
 - symfony/process: ^6.4 || ^7.0
 
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
 
This package is auto-updated.
Last update: 2025-11-03 23:37:04 UTC
README
A PHP CLI tool that analyzes git changes and outputs affected PHP file paths in formats compatible with PHPUnit, Psalm, ECS, and PHP-CS-Fixer. This enables optimized test and analysis runs by only processing files that are actually affected by changes.
Features
- Git Integration: Analyze uncommitted, staged, or branch-based changes
 - Dependency Analysis: Uses nikic/php-parser to build comprehensive dependency graphs
 - Multiple Strategies: Choose between conservative, moderate, or minimal analysis depth
 - Format-Specific Output: Tailored output for PHPUnit, Psalm, ECS, and PHP-CS-Fixer
 - Full Scan Triggers: Regex patterns to force complete project scans for critical files
 - PSR-12 Compliant: Clean, readable, and well-structured code
 
Installation
Via Composer (Recommended)
composer require --dev sebastiaanwouters/diffalyzer
After installation, the binary will be available at vendor/bin/diffalyzer.
From Source
git clone https://github.com/sebastiaanwouters/diffalyzer.git
cd diffalyzer
composer install
Quick Start
# Run only tests affected by your changes vendor/bin/phpunit $(vendor/bin/diffalyzer --output test) # Analyze only affected files with Psalm, ECS, or PHP-CS-Fixer vendor/bin/psalm $(vendor/bin/diffalyzer --output files) vendor/bin/ecs check $(vendor/bin/diffalyzer --output files) vendor/bin/php-cs-fixer fix $(vendor/bin/diffalyzer --output files)
Usage
Basic Usage
# Get test files affected by uncommitted changes vendor/bin/diffalyzer --output test # Get all affected files (for Psalm, ECS, PHP-CS-Fixer, etc.) vendor/bin/diffalyzer --output files
With Strategies
# Conservative strategy (default): includes all dependencies vendor/bin/diffalyzer --output test --strategy conservative # Moderate strategy: excludes dynamic method calls vendor/bin/diffalyzer --output test --strategy moderate # Minimal strategy: only imports and direct inheritance vendor/bin/diffalyzer --output test --strategy minimal
Git Comparison Options
# Staged files only vendor/bin/diffalyzer --output test --staged # Compare branches vendor/bin/diffalyzer --output test --from main --to feature-branch # Compare commits vendor/bin/diffalyzer --output test --from abc123 --to def456 # Compare from specific branch to HEAD vendor/bin/diffalyzer --output test --from main
Full Scan Pattern
# Trigger full scan if any .yml file changes vendor/bin/diffalyzer --output test --full-scan-pattern '/.*\.yml$/' # Trigger full scan for config changes vendor/bin/diffalyzer --output test --full-scan-pattern '/^config\//'
Integration Examples
PHPUnit
# Run only affected tests vendor/bin/phpunit $(vendor/bin/diffalyzer --output test) # With configuration vendor/bin/phpunit -c phpunit.xml $(vendor/bin/diffalyzer --output test)
Psalm
# Analyze only affected files vendor/bin/psalm $(vendor/bin/diffalyzer --output files) # With specific error level vendor/bin/psalm --show-info=false $(vendor/bin/diffalyzer --output files)
ECS (Easy Coding Standard)
# Check only affected files vendor/bin/ecs check $(vendor/bin/diffalyzer --output files) # With fix vendor/bin/ecs check --fix $(vendor/bin/diffalyzer --output files)
PHP-CS-Fixer
# Fix only affected files vendor/bin/php-cs-fixer fix $(vendor/bin/diffalyzer --output files) # Dry run vendor/bin/php-cs-fixer fix --dry-run $(vendor/bin/diffalyzer --output files)
Command Line Options
| Option | Short | Description | Default | 
|---|---|---|---|
--output | 
-o | 
Output format: test (test files only) or files (all files) | 
Required | 
--strategy | 
-s | 
Analysis strategy: conservative, moderate, minimal | 
conservative | 
--from | 
Source ref for comparison (branch or commit hash) | ||
--to | 
Target ref for comparison (branch or commit hash) | HEAD | 
|
--staged | 
Only analyze staged files | false | 
|
--full-scan-pattern | 
Regex pattern to trigger full scan | 
Analysis Strategies
Conservative (Default)
Includes all dependency types:
- Use statements (imports)
 - Class inheritance (extends)
 - Interface implementations
 - Trait usage
 - Instantiations (
newkeyword) - Static calls
 
Most comprehensive but may include some false positives.
Moderate
Includes:
- Use statements (imports)
 - Class inheritance (extends)
 - Interface implementations
 - Trait usage
 
Excludes dynamic method calls and instantiations.
Minimal
Includes only:
- Use statements (imports)
 - Direct inheritance (extends/implements)
 
Fastest but may miss some affected files.
Output Behavior
Partial Scan (Normal Mode)
--output test
Outputs space-separated test file paths that are affected by the changes.
How it works: The tool uses AST-based dependency analysis to find ALL test files that import or use the affected classes. It does NOT assume file naming conventions or directory structures.
Examples:
- 
Test files that import changed classes
src/User.phpchanges (declaresDiffalyzer\User)tests/UserTest.phpimportsDiffalyzer\User→ included in output- Works regardless of file names or directory structure
 
 - 
Transitive dependencies
src/User.phpchangessrc/UserCollector.phpimports/usesUser→ affectedtests/UserCollectorTest.phpimportsUserCollector→ included in output- Full dependency chain is traversed automatically
 
 - 
Test files that changed directly
tests/UserTest.phpmodified → included in output
 - 
No assumptions about structure
- Works with 
tests/,test/,Tests/,Test/, or any directory - Works with any test file naming convention (as long as it contains "Test.php")
 - Works with custom project structures
 
 - Works with 
 - 
Data fixtures via PHP classes
- If tests use fixture/factory classes (e.g., 
UserFixture,UserFactory) - Changes to those fixture classes are tracked via normal dependency analysis
 - When 
UserFixture.phpchanges → tests importing it are included - No special configuration needed
 
 - If tests use fixture/factory classes (e.g., 
 
Example output: tests/UserTest.php tests/UserCollectorTest.php tests/Integration/UserFlowTest.php
--output files
Outputs space-separated file paths for all affected files (includes both source and test files).
Use this for static analysis tools (Psalm), code style checkers (ECS, PHP-CS-Fixer), or any tool that needs to process all affected files.
Example output: src/Foo/Bar.php src/Baz/Qux.php tests/FooTest.php
Full Scan Mode
When --full-scan-pattern matches or no specific files are needed:
- All formats output an empty string
 - Empty output tells each tool to scan the entire project
 - Example: 
phpunitwith no arguments runs all tests 
Built-in Full Scan Triggers:
The following files automatically trigger a full scan (no --full-scan-pattern needed):
composer.json- Dependencies changed, could affect anythingcomposer.lock- Dependency versions changed
You can add additional patterns with --full-scan-pattern to supplement these built-in triggers.
How It Works
- Git Change Detection: Detects changed PHP files using git diff
 - AST Parsing: Parses all project PHP files using nikic/php-parser
 - Dependency Graph: Builds forward and reverse dependency maps
 - Impact Analysis: Traverses graph to find all affected files
 - Format Output: Generates tool-specific output format
 
Example Workflow
Step 1: Git detects changes
Changed: src/User.php
Step 2: Dependency analysis
User.php changed
    ↓
UserCollector.php uses User (affected)
    ↓
UserService.php uses UserCollector (affected)
Step 3: Output by format
For --output files (all source files):
src/User.php src/UserCollector.php src/UserService.php
For --output test (test files only):
tests/UserTest.php tests/UserCollectorTest.php tests/UserServiceTest.php
Result: Run only the 3 tests affected by the User.php change, not all 100+ tests in your suite!
Advanced Integration
Makefile
Use the included Makefile for convenient local development:
# Run affected tests make test-affected # Run tests for changes from main branch make test-branch # Analyze with Psalm make psalm-affected # Fix code style make cs-fix-affected make ecs-affected # See all available targets make help
Pre-commit Hook
Create .git/hooks/pre-commit:
#!/bin/bash # Run tests on staged files TESTS=$(vendor/bin/diffalyzer --output test --staged) if [ -n "$TESTS" ]; then echo "Running affected tests..." vendor/bin/phpunit $TESTS if [ $? -ne 0 ]; then echo "Tests failed. Commit aborted." exit 1 fi fi exit 0
Make it executable:
chmod +x .git/hooks/pre-commit
Composer Scripts
Add to your composer.json:
{
    "scripts": {
        "test:affected": [
            "@php vendor/bin/phpunit $(vendor/bin/diffalyzer --output test)"
        ],
        "psalm:affected": [
            "@php vendor/bin/psalm $(vendor/bin/diffalyzer --output files)"
        ],
        "cs:fix:affected": [
            "@php vendor/bin/php-cs-fixer fix $(vendor/bin/diffalyzer --output files)"
        ]
    }
}
Then run:
composer test:affected composer psalm:affected composer cs:fix:affected
CI/CD Integration
GitHub Actions
See .github/workflows/ci-example.yml for a complete working example. Basic setup:
name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Full history for git diff - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.1 - name: Install Dependencies run: composer install - name: Run Affected Tests run: | if [ "${{ github.event_name }}" == "pull_request" ]; then TESTS=$(vendor/bin/diffalyzer --output test --from origin/${{ github.base_ref }}) else TESTS=$(vendor/bin/diffalyzer --output test) fi if [ -n "$TESTS" ]; then echo "Running affected tests: $TESTS" vendor/bin/phpunit $TESTS else echo "Running all tests (full scan triggered)" vendor/bin/phpunit fi
GitLab CI
See .gitlab-ci-example.yml for a complete working example. Basic setup:
variables: GIT_DEPTH: 0 # Fetch full git history test: script: - composer install - | if [ -n "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" ]; then BASE_BRANCH="origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" else BASE_BRANCH="HEAD~1" fi TESTS=$(vendor/bin/diffalyzer --output test --from $BASE_BRANCH) if [ -n "$TESTS" ]; then vendor/bin/phpunit $TESTS else vendor/bin/phpunit fi
Troubleshooting
No tests run but I made changes
Cause: You changed a file that no tests depend on.
Solutions:
- Ensure your test files import the classes they test
 - Try conservative strategy: 
--strategy conservative - Verify your changes are to tracked files (not untracked/ignored)
 - Check dependency chain: does a test import your changed class?
 
All tests run when I change one file
Cause: Full scan was triggered.
Solutions:
- Check if you modified 
composer.jsonorcomposer.lock - Check if your change matches 
--full-scan-pattern - Review changed files: 
git status - This is by design for critical files
 
Git errors
Cause: Not in a git repository or no commits exist.
Solutions:
- Ensure you're in a git repository: 
git init - Create an initial commit: 
git add . && git commit -m "Initial commit" - Verify git is accessible: 
git --version 
Empty output
This is normal behavior and means:
- For PHPUnit: No test files affected OR full scan triggered → run all tests
 - For other tools: No files affected OR full scan triggered → analyze all files
 
Always handle empty output by running the full tool:
TESTS=$(vendor/bin/diffalyzer --output test) if [ -n "$TESTS" ]; then vendor/bin/phpunit $TESTS else vendor/bin/phpunit # Full run fi
Tips & Best Practices
- Use Makefile: Convenient shortcuts for common operations
 - CI/CD Branch Comparison: Use 
--from origin/mainin pipelines - Start Conservative: Begin with conservative strategy, optimize later if needed
 - Fixture Classes: Modern fixtures (PHP classes) are automatically tracked
 - Pre-commit Hooks: Use 
--stagedflag for pre-commit validation - Full Scan Patterns: Add critical config files to trigger complete scans
 - Test Imports: Ensure test files import the classes they test for proper tracking
 - Git History: Use 
fetch-depth: 0in CI to enable proper branch comparison 
Requirements
PHP Version Support
Diffalyzer supports the following PHP versions:
| PHP Version | Status | Tested | 
|---|---|---|
| 8.1 | ✅ Supported | ✅ Yes | 
| 8.2 | ✅ Supported | ✅ Yes | 
| 8.3 | ✅ Supported | ✅ Yes | 
| 8.4 | ✅ Supported | ✅ Yes | 
| 8.0 or lower | ❌ Not supported | - | 
All versions are actively tested in CI/CD with both prefer-lowest and prefer-stable dependency strategies.
Other Requirements
- Git: Any recent version
 - Composer: 2.0 or higher
 
Dependencies
nikic/php-parser^5.0symfony/console^6.4 || ^7.0symfony/process^6.4 || ^7.0symfony/finder^6.4 || ^7.0
License
MIT
Author
Created by Sebastiaan Wouters for optimized PHP testing and analysis workflows.