Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Psalm Laravel Nova Laravel Package

interaction-design-foundation/psalm-laravel-nova

Psalm plugin for Laravel Nova that improves static analysis by providing accurate type information and stubs for Nova resources, fields, actions, and filters. Helps catch errors earlier and reduces false positives in Psalm checks.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites

  • Psalm ^5 (or ^6 — check the plugin's composer.json constraint before installing; the plugin API changed between majors).
  • Laravel Nova ^4 or ^5 in the host application. Nova is a paid package, so your CI needs COMPOSER_AUTH credentials (or a mirrored repository) to install it — the plugin itself cannot be evaluated without Nova present.
  • PHP 8.1+.

Install

composer require --dev interaction-design-foundation/psalm-laravel-nova:^0.2.3

This is a dev-only, analysis-time dependency. It emits no runtime code, so it should never land in require.

Use ^0.2.3 rather than ^0.2.0 — 0.2.3 fixes two stub-signature false positives (canSee() on Action/Filter/Lens rejecting NovaRequest callbacks, and Field::make() resolve callbacks rejecting typed resource/attribute arguments) that were unfixable-by-config in 0.2.2 and earlier.

Register the plugin

Add the plugin class to the <plugins> block of psalm.xml. The entry point lives under src/ in the repo (verify the FQCN from the README or composer show before wiring it up — it follows the usual …\Plugin convention):

<psalm errorLevel="4">
    <plugins>
        <pluginClass class="InteractionDesignFoundation\PsalmLaravelNova\Plugin" />
    </plugins>
</psalm>

Where to look first

  1. README.md — supported Nova/Psalm versions and the exact plugin FQCN.
  2. src/ — the PluginEntryPointInterface implementation; this tells you whether it registers stubs only, or also method-call/return-type handlers. 0.2.3's changelog makes it clear there are signature-level fixes (callbacks on canSee, make), so expect handlers as well as stubs.
  3. stubs/ — the actual Nova surface being typed. This is the real contract; read it to know what the plugin will and won't cover.
  4. tests/ — any fixture-project setup is the fastest way to see intended usage, and the two 0.2.3 fixes almost certainly landed with regression fixtures worth copying.

First use case

The classic Nova false positives: fluent field chains, Nova facade, and typed resolve / canSee callbacks.

public function fields(Request $request): array
{
    return [
        Text::make('Name')
            ->sortable()
            ->rules('required', 'max:255'),
        BelongsTo::make('User', 'user', User::class)
            ->searchable(),
        Text::make('Slug', fn (User $user) => $user->slug),
        Text::make('Email')
            ->canSee(fn (NovaRequest $request) => $request->user()->isAdmin()),
    ];
}

Run vendor/bin/psalm --no-cache and compare the error count before/after enabling the plugin. That delta is your actual payoff — expect undefined-method/undefined-class noise on Nova's magic to drop sharply, and (on 0.2.3+) the typed-callback errors above to stay quiet.

Implementation Patterns

Measure before you adopt

Run Psalm on a Nova-heavy module with and without the plugin and diff the counts (--output-format=json | jq length is handy). This gives you a defensible "Nova noise removed" number and tells you immediately whether the plugin's stubs match your Nova version.

Upgrade 0.2.2 → 0.2.3 as a dedicated step

If you adopted early, 0.2.3 is a pure noise-reduction upgrade: ArgumentTypeCoercion/InvalidArgument-style errors on ->canSee(fn (NovaRequest $r) => …) and on Field::make('X', fn (Model $m) => …) should disappear without any code change. Measure before and after, and delete any @psalm-suppress you added to work around them — those are now dead annotations.

composer update interaction-design-foundation/psalm-laravel-nova --with-dependencies
vendor/bin/psalm --no-cache
vendor/bin/psalm --find-unused-psalm-suppress

Enable the Laravel plugin first

If you also use psalm/plugin-laravel, order matters: the Laravel plugin resolves models, facades, and Eloquent magic; the Nova plugin layers on top of that. Put Laravel first, Nova second:

<plugins>
    <pluginClass class="Psalm\LaravelPlugin\Plugin" />
    <pluginClass class="InteractionDesignFoundation\PsalmLaravelNova\Plugin" />
</plugins>

This matters even more now that the Nova stubs type resolve callbacks: a fn (User $user) => … only checks cleanly if the Laravel plugin has already resolved User.

Prefer the plugin over blanket suppressions

The everyday win is deleting @psalm-suppress UndefinedMethod / UndefinedClass annotations scattered across your Nova resources. Adopt the plugin, then run psalm --find-unused-psalm-suppress to harvest the annotations that are no longer needed. That is the single highest-value migration step — and it is worth re-running after every plugin patch release, since stub precision is visibly improving.

Keep app-level Nova types in your own stubs

The plugin cannot know your custom Resource, Tool, Card, or Field subclasses, nor your Nova::resources() return shapes. Pair the plugin with a small local stub or @psalm-type aliases:

/** @psalm-type NovaFields = array<int, \Laravel\Nova\Fields\Field> */
final class UserResource extends Resource
{
    /** @return NovaFields */
    public function fields(Request $request): array { /* ... */ }
}

Baseline ordering

Generate psalm-baseline.xml after enabling the plugin, not before. A baseline captured without the plugin will pin hundreds of Nova false positives as "known errors" forever, and every one becomes debt you can't see. Likewise, regenerate the baseline after the 0.2.3 upgrade so the newly-fixed callback errors get removed from it.

CI integration

- run: composer install --no-interaction
  env:
    COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}   # required for laravel/nova
- run: vendor/bin/psalm --no-cache --threads=4 --output-format=github

Cache the Psalm cache directory across runs, but use --no-cache on the first run after any plugin/Nova upgrade so stale inference doesn't mask breakage.

Incremental rollout on legacy Nova apps

Raise errorLevel one step at a time (e.g. 6 → 5 → 4). Nova-heavy admin code often has deep mixed chains that surface as new MixedAssignment/MixedMethodCall errors once stubs start resolving. Budget for that, and consider per-directory <ignoreFiles> for app/Nova until it clears.

Local debugging loop

To confirm the plugin actually loaded:

vendor/bin/psalm --debug --no-cache 2>&1 | grep -i nova

If nothing matches, the plugin class is not being resolved — most often an autoload or version issue, not a stub gap.

Gotchas and Tips

Nova version drift is the main failure mode. The stubs are hand-written against a specific Nova major. When you upgrade Nova (say 4 → 5), methods get added, renamed, or change return types, and the plugin will keep asserting the old shape — producing new false positives or, worse, silently accepting methods that no longer exist. Re-check the plugin's composer.json Nova constraint on every upgrade and pin the plugin version explicitly ("…": "0.2.*" for patch-level stub fixes, exact pin if you're risk-averse).

Don't suppress what 0.2.3 fixed — upgrade. If you're seeing errors on canSee() closures taking a NovaRequest, or on Field::make('Label', fn (User $u) => …) with a typed first parameter, you're on ≤0.2.2. The fix is a version bump, not a stub override or a suppression. Suppressing these now creates annotations you'll be hunting later with --find-unused-psalm-suppress.

Commercial-package CI is a real hurdle. Anything that installs laravel/nova needs credentials. If you can't put them in CI, the plugin will never run in your pipeline — decide up front whether that's acceptable.

"Plugin does nothing" is almost always the cache. Psalm caches the project graph aggressively. After editing psalm.xml or bumping the plugin, run --no-cache once. Similarly, running Psalm from a different working directory (or a Docker image with a separate vendor/) can make pluginClass unresolvable. This bites hardest on patch upgrades, where the version number changes but nobody thinks to bust the cache — the symptom is "I upgraded, the errors are still there."

Stub precision improves release over release — treat it as a moving target. 0.2.3 tightened callback signatures on canSee and make. That direction of travel is good but means your baseline and suppressions decay with each bump. Still, many Nova fluent methods are typed as returning static/$this even where the real API returns something narrower (or mixed). That keeps chains analysable but means the plugin will not catch a field configured with the wrong argument type. Treat it as noise reduction, not full type coverage — don't claim Nova code is "type-safe" because Psalm is green.

Extension points. If your codebase relies on Nova macros (Field::macro(...), Nova::macro(...)), the plugin won't know about them. Two options: (a) keep targeted @psalm-suppress on macro call sites, or (b) add a stub file alongside and register it via <stubs> in psalm.xml. Option (b) is the clean path if the macro is used widely.

Custom handlers vs. stubs. 0.2.3 confirms the plugin registers real handlers for specific method signatures — so before hand-rolling a MethodCallReturnTypeProvider for app-specific Nova magic, check the plugin's handler list. Don't duplicate a handler the plugin already registers for the same symbol, or behavior becomes order-dependent. It also means a plugin upgrade can override your local stub's signature for a method you thought you controlled; re-run the analysis after each bump.

Maturity risk is non-trivial but trending positive. Still a small, single-maintainer-org project (two stars, no dependents), but 0.2.3 is a substantive correctness release with named PRs, which is a healthier signal than a dependency-bot-only changelog. MIT licensing means forking is cheap if it stalls. Practical mitigations: pin the version, add a smoke test that runs Psalm against a fixture with a known Nova chain (include a typed canSee and a typed make resolve callback — exactly the 0.2.3 regressions) and asserts specific errors are not reported, and keep the plugin behind require-dev so a bad release can never break production deploys.

Don't expect it to type your app's Nova layer. Resource authorization logic, resolve() implementations, tool routing, and Nova::resources() result shapes are app-defined. The plugin types the framework surface and now checks the arguments you pass to framework callbacks; the bodies are yours.

Sanity-check severity. If enabling the plugin increases the error count, stop and diff the stub files against your installed Nova version — that's a version mismatch, not an improvement in strictness. Conversely, if you're on ≤0.2.2 and see callback-argument errors on canSee/make, that's a known stub bug, not your code.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle