psalm/psalm-plugin-api
Psalm plugin API providing interfaces and helpers to build and integrate custom plugins with the Psalm static analysis tool, enabling extensions such as new checks, type providers, and project-specific analysis behavior.
Installation Add the package to your project via Composer:
composer require psalm/psalm-plugin-api
Ensure your composer.json includes "psalm/psalm-plugin-api": "^x.y.z" under require-dev if using Psalm as a dev dependency.
First Use Case Create a basic plugin skeleton:
// src/Plugin/MyPlugin.php
namespace MyPlugin;
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
class MyPlugin implements PluginEntryPointInterface
{
public function __invoke(RegistrationInterface $registration, ?string $tempDir = null): void
{
// Register hooks here
}
}
Psalm Configuration
Update psalm.xml to include your plugin:
<plugin_class>MyPlugin\MyPlugin</plugin_class>
Verify
Run Psalm with --init to generate stubs, then analyze:
vendor/bin/psalm --init
vendor/bin/psalm
Hook Registration
Use RegistrationInterface to attach to Psalm’s lifecycle:
$registration->registerHook(Hook::AFTER_ANALYSIS, [$this, 'postAnalysis']);
Custom Analysis
Extend Psalm\CodeLocation and Psalm\Type for type inference:
$registration->registerHook(Hook::AFTER_METHOD_ANALYSIS, function (AfterMethodAnalysisInterface $event) {
$method = $event->getMethod();
if ($method->getDeclaringClass()->getName() === 'MyClass') {
$event->getType()->addPossibleType(new \Psalm\Type\Atomic\TString());
}
});
Stub Generation
Implement StubGeneratorInterface for custom stubs:
$registration->registerStubGenerator(new class implements StubGeneratorInterface {
public function generateStubFile(string $file_path, string $contents): string {
return str_replace('// STUB', '// CUSTOM_STUB', $contents);
}
});
Psalm\Plugin\PluginEntryPointInterface to access Psalm’s container.Hook::AFTER_ANALYSIS over Hook::BEFORE_ANALYSIS.PluginEntryPointInterface::__invoke()’s $tempDir or environment variables.Type Safety
Psalm plugins must adhere to Psalm’s type system. Misusing Psalm\Type classes (e.g., TArray vs TArrayLike) can cause false positives/negatives.
Fix: Use Psalm\Type\Union for ambiguous cases.
Hook Order
Hooks fire in registration order. Override another plugin’s behavior by registering later.
Tip: Log hook execution order with Psalm\LoggerInterface.
Stub Conflicts
Custom stubs may override Psalm’s built-in stubs. Test with --no-cache to avoid stale stubs.
Workaround: Use StubGeneratorInterface’s $contents to merge stubs.
Performance
Heavy analysis in Hook::BEFORE_ANALYSIS can slow Psalm. Offload work to Hook::AFTER_ANALYSIS or use caching.
Psalm\LoggerInterface via the container:
$logger = $registration->getContainer()->getByType(LoggerInterface::class);
$logger->info('Plugin executed');
Psalm\Internal\ProjectFilesAnalysis to inspect parsed files during AFTER_ANALYSIS.Custom Rules
Extend Psalm\Plugin\Rule\RuleInterface for static analysis rules:
$registration->registerRule(new class implements RuleInterface {
public function getPropertyName(): string { return 'my_rule'; }
public function run(CodeLocation $location, Callable $callback): void { /* ... */ }
});
Template Analysis
Hook into Hook::AFTER_TEMPLATE_ANALYSIS to modify template logic (e.g., Blade).
Configuration
Use Psalm\Config to read plugin-specific settings from psalm.xml:
<plugin_config>
<my_plugin>
<option>value</option>
</my_plugin>
</plugin_config>
Access via:
$config = $registration->getContainer()->getByType(Config::class);
$value = $config->getPluginConfig('my_plugin', 'option');
How can I help you explore Laravel packages today?