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: 2
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-04 16:26:25 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.