graham-campbell/analyzer
Analyzer is a PHP test utility by Graham Campbell that checks your code for references to classes that don’t actually exist. Compatible with PHP 8.1–8.5 and PHPUnit 10–13, helping catch broken imports and missing dependencies early.
composer require --dev graham-campbell/analyzer:^5.1
GrahamCampbell\Analyzer\AnalyzerTestCase:
use GrahamCampbell\Analyzer\AnalyzerTestCase;
class ClassReferenceTest extends AnalyzerTestCase
{
protected function getPaths(): array
{
return [__DIR__.'/../../app', __DIR__.'/../../config'];
}
}
./vendor/bin/phpunit --testsuite ClassReferenceTest
Add the test to your phpunit.xml:
<testsuites>
<testsuite name="Analyzer">
<directory>./tests/</directory>
<file>./tests/ClassReferenceTest.php</file>
</testsuite>
</testsuites>
Configure GitHub Actions to run it on PRs:
- name: Run Analyzer
run: ./vendor/bin/phpunit --testsuite Analyzer
AnalyzerTestCasegetPaths()):
protected function getPaths(): array
{
return [
app_path(),
database_path(),
config_path(),
];
}
getIgnored()):
protected function getIgnored(): array
{
return [
'App\Services\Legacy\*', // Ignore legacy classes
'App\Contracts\*', // Ignore interfaces/contracts
];
}
shouldAnalyzeFile()):
protected function shouldAnalyzeFile(string $file): bool
{
return !str_contains($file, 'vendor/') && parent::shouldAnalyzeFile($file);
}
provideFilesToCheck()):
protected function provideFilesToCheck(): array
{
return [
__DIR__.'/../app/Providers/AppServiceProvider.php',
__DIR__.'/../app/Http/Controllers/ApiController.php',
];
}
register()/boot() methods for missing classes:
// tests/ClassReferenceTest.php
protected function getPaths(): array
{
return [app_path('Providers')];
}
// app/Http/Resources/UserResource.php
/**
* @property-read App\Models\User $user
*/
// tests/ClassReferenceTest.php
protected function getIgnored(): array
{
return ['App\Events\*']; // Exclude events (handled separately)
}
Extend GrahamCampbell\Analyzer\Analyzer for bespoke rules:
use GrahamCampbell\Analyzer\Analyzer;
class CustomAnalyzer extends Analyzer
{
protected function analyzeClass(string $class): void
{
if (str_contains($class, 'App\Services\Payment')) {
$this->assertClassExists($class, 'Payment service classes must exist.');
}
parent::analyzeClass($class);
}
}
Use it in tests:
use GrahamCampbell\Analyzer\AnalyzerTestCase;
class CustomClassReferenceTest extends AnalyzerTestCase
{
protected function createAnalyzer(): Analyzer
{
return new CustomAnalyzer();
}
}
False Positives with PHPDoc:
@property App\User $user) may fail if the class exists but the property doesn’t.getIgnored() to exclude PHPDoc-only checks:
protected function getIgnored(): array
{
return ['@property', '@method', '@var'];
}
shouldAnalyzeFile() to skip files with known PHPDoc issues.Namespace Aliases:
use App; aliases are not resolved by default.protected function shouldAnalyzeFile(string $file): bool
{
return !str_contains(file_get_contents($file), 'use App;');
}
Dynamic Class Loading:
class_alias() or eval() will fail.protected function getIgnored(): array
{
return ['App\Dynamic\*'];
}
Performance in Large Codebases:
app/ + vendor/ can be slow.provideFilesToCheck() for targeted scans:
protected function provideFilesToCheck(): array
{
return glob(app_path('*/*.php'));
}
PHPUnit Version Mismatches:
composer.json:
"require-dev": {
"phpunit/phpunit": "^10.0"
}
AnalyzerTestCase:
protected function setUp(): void
{
$this->analyzer->setDebug(true);
}
./vendor/bin/phpunit --filter ClassReferenceTest --testdox-html
getIgnored() isn’t hiding real issues:
public function testIgnoredClasses()
{
$this->assertArrayNotContainsString('App\Nonexistent', $this->getIgnored());
}
Custom Class Resolver:
Override getClassResolver() to use a custom resolver (e.g., for Laravel’s autoloader):
protected function getClassResolver(): \phpDocumentor\Reflection\Types\Resolver\ClassString
{
return new CustomClassResolver();
}
Pre-Analysis Hooks:
Modify analyzeFile() to skip specific patterns:
protected function analyzeFile(string $file): void
{
if (str_contains($file, 'migrations/')) {
return; // Skip migrations
}
parent::analyzeFile($file);
}
Post-Analysis Actions:
Extend afterAnalysis() to log or notify:
protected function afterAnalysis(): void
{
if ($this->hasErrors()) {
$this->logErrorsToSlack();
}
}
Cache::class) will fail unless their underlying classes exist.
Fix: Ignore facades or validate their bindings:
protected function getIgnored(): array
{
return ['Illuminate\Support\Facades\*'];
}
App\Services\PaymentService) must exist at runtime.
Fix: Run Analyzer after bootstrap/app.php:
$this->analyzer->setPaths([app_path()]);
$this->analyzer->analyze();
resources/js or resources/css if they contain PHP:
protected function shouldAnalyzeFile(string $file): bool
{
return !str_starts_with($file, resource_path('js/'));
}
--cache-result flag to avoid re-scanning:
./vendor/bin/phpunit --testsuite Analyzer --cache-result
<!-- phpunit.xml -->
<phpunit parallel="true">
<testsuites>
<testsuite name="Analyzer">
<file>tests/ClassReferenceTest.php</file>
</testsuite>
</testsuites>
</phpunit>
vendor/ to speed up scans:
protected function shouldAnalyzeFile(string $file): bool
{
return !str_contains($file, 'vendor/');
}
How can I help you explore Laravel packages today?