php-standard-library/psalm-plugin
Psalm plugin for PHP Standard Library (PSL) that improves type inference for PSL Type\specifications. Enables more precise array/shape types (e.g., Type\shape coercions) so Psalm reports correct, specific types during static analysis.
composer require --dev php-standard-library/psalm-plugin
php vendor/bin/psalm-plugin enable php-standard-library/psalm-plugin
psalm-plugin config (psalm-plugin.json):
{
"plugins": ["php-standard-library/psalm-plugin"]
}
Replace generic array coercion with PSL shapes:
use Psl\Type;
$shape = Type\shape([
'name' => Type\string(),
'age' => Type\int(),
]);
$validated = $shape->coerce($_GET['user']);
// Psalm now infers: array{name: string, age: int} (not generic array)
psalm-plugin is enabled and the plugin is listed.psalm:suppress for false positives until the plugin’s type inference matures.Types trait/class:
trait UserShape
{
public function shape(): Type\TypeInterface
{
return Type\shape([
'id' => Type\int(),
'email' => Type\email(),
'roles' => Type\optional(Type\list_of(Type\string())),
]);
}
}
public function rules(): array
{
$shape = (new UserShape())->shape();
$validated = $shape->coerce($this->all());
// Psalm now knows $validated is array{id: int, email: string, roles?: list<string>}
return [
'email' => ['required', 'email'],
// ... other rules
];
}
public function createUser(array $data): User
{
$shape = (new UserShape())->shape();
$validated = $shape->coerce($data); // Psalm infers exact shape
return User::create([
'email' => $validated['email'],
// ... other fields
]);
}
psalm.xml:
<file-list>
<dir>app/Shapes</dir> <!-- PSL shape definitions -->
</file-list>
- name: Psalm + PSL Plugin
run: vendor/bin/psalm --init --plugins
| Pattern | PSL + Psalm Example | Benefit |
|---|---|---|
| Input validation | Type\shape()->coerce($request->all()) |
Eliminates runtime Validator for simple cases. |
| API contracts | Type\shape() for DTOs in app/Http/Resources |
Self-documenting API responses. |
| Eloquent casting | protected $casts = ['metadata' => 'array']; // + PSL shape |
Type-safe JSON attributes. |
| Event payloads | Type\shape() for Illuminate\Events\Dispatchable |
Guarantee event consistency. |
False positives with coerce():
/** @psalm-suppress MixedAssignment */
$validated = $shape->coerce($data);
@psalm-trace to debug inferred types:
/** @psalm-trace $validated */
Plugin compatibility:
^2.1. Downgrade if issues arise:
composer require php-standard-library/psalm-plugin:^2.0
TList types may not be fully supported. Prefer list<T> or array<T>.Nested shapes:
Type\shape() may confuse Psalm. Flatten or use @var annotations:
/** @var array{user: array{id: int, name: string}} */
$data = $shape->coerce($input);
Check plugin status:
php vendor/bin/psalm-plugin list
Ensure php-standard-library/psalm-plugin shows as "enabled."
Verbose Psalm output:
php vendor/bin/psalm --no-cache --output-format=json | jq '.errors[] | select(.message | test("PSL"))'
Custom return types: Extend the plugin by adding return type providers (see PSL docs). Example:
// In a custom plugin class
public function getReturnTypeProviders(): array
{
return [
new ReturnTypeProvider(
'Psl\MyNamespace\my_function',
fn (TypeElement $type): ?TypeElement => Type\string()
),
];
}
Psalm config overrides: Disable plugin for specific files:
<file-list exclude="tests/legacy-code.php">
<!-- ... -->
</file-list>
Performance:
static property to avoid re-parsing:
private static ?Type\TypeInterface $userShape;
public static function shape(): Type\TypeInterface
{
return self::$userShape ??= Type\shape([...]);
}
Leverage nullish():
Use Type\nullish() for optional fields with runtime null checks:
$shape = Type\shape([
'metadata' => Type\nullish(Type\array_of(Type\string())),
]);
Psalm will infer array<string>|null.
Pipe operator support:
Chain PSL functions with -> (PHP 8.1+):
$result = Str\lowercase($_GET['name'])
->trim()
->coerce(Type\string());
The plugin improves type inference for each step.
Test with psalm:fix:
Run php vendor/bin/psalm --fix to auto-correct simple type issues after enabling the plugin.
How can I help you explore Laravel packages today?