Contributing

How the plugin works

The plugin boots a Laravel application, then hooks into Psalm’s event system to override type inference for Laravel APIs.

The app is needed at boot time to read config values (e.g. auth.php guards), resolve facade aliases via AliasLoader, and load service providers. When run inside a Laravel project, the plugin loads the project’s own bootstrap/app.php — so it sees the real config, routes, and providers. When no bootstrap/app.php is found (e.g. analyzing a Laravel package, or running the plugin’s own tests), it falls back to Orchestra Testbench which provides a minimal Laravel app skeleton.

See ApplicationProvider::doGetApp() for the resolution logic.

flowchart TD
    A["Plugin::__invoke"] --> B["Boot Laravel app"]
    B --> C["Build schema\n(if columnFallback=migrations)"]
    C --> D["Generate alias stubs\n(from AliasLoader)"]
    D --> E["Register handlers"]
    E --> F["Register stubs"]

    E --- handlers["
        Application — ContainerHandler, OffsetHandler
        Auth — AuthHandler, GuardHandler, RequestHandler
        Eloquent — ModelRegistrationHandler, RelationsMethodHandler,
        ModelMethodHandler, BuilderScopeHandler
        Console — CommandArgumentHandler
        Helpers — CacheHandler, PathHandler, TransHandler
        Rules — NoEnvOutsideConfigHandler
        SuppressHandler
    "]

    F --- stubs["
        stubs/common/
        stubs/12/, stubs/13/ (version-specific)
        stubs/taintAnalysis/
        aliases.stubphp (generated)
    "]

    G["Psalm scans all project files"] -.->|afterCodebasePopulated| H["ModelRegistrationHandler"]
    H --- models["
        Discover Model subclasses
        Register property handlers:
        relationship > factory > accessor > column
    "]

Getting started

git clone git@github.com:psalm/psalm-plugin-laravel.git
cd psalm-plugin-laravel
composer install
composer test        # lint + psalm + unit + type tests

Running tests

composer test          # full suite (lint + psalm + unit + type)
composer test:unit     # PHPUnit unit tests only
composer test:type     # type tests only (psalm-tester)
composer psalm         # self-analysis of plugin source
composer test:app      # creates a fresh Laravel project, scaffolds common class types (`make:xxx`), installs the plugin, and runs Psalm on the result
LARAVEL_INSTALLER_VERSION=12.11.2 composer test:app # run over a specific Laravel version

# single test file
./vendor/bin/phpunit tests/Unit/Handlers/Auth/AuthHandlerTest.php
./vendor/bin/phpunit --filter=AuthTest tests/Type/

Code style

  • PER Coding Style 3.0 (powered by php-cs-fixer: run composer cs to apply fixes)
  • Never use @psalm-suppress — fix the issue or add to psalm-baseline.xml
  • Explain decisions and ideas in comments
composer cs     # auto-fix style issues
composer rector # run rector refactoring

How to add a stub

Stubs override Laravel’s type signatures. Place them in:

  • stubs/common/ — shared across Laravel versions
  • stubs/12/, stubs/13/ — version-specific overrides
  • stubs/taintAnalysis/ — security taint annotations (sources, sinks, escapes)

Rules:

  • Verify signatures against actual Laravel code (not against Laravel PHPDoc or method signatures)
  • Add a type test in tests/Type/tests/ to prevent regression

How to add a handler

Handlers implement Psalm event interfaces to override type inference. Create the handler class in the appropriate src/Handlers/ subdirectory, then register it in Plugin::registerHandlers().

Psalm hooks used by the plugin

Psalm processes code in phases. Each hook fires at a specific phase and has different data available. Analysis hooks are hot paths — they fire on every matching expression. Scanning hooks fire once per class or once total.

flowchart LR
    subgraph scanning ["Phase 1: Scanning"]
        direction TB
        S1["AfterClassLikeVisitInterface
        fires after each class/trait/interface
        ----
        data: ClassLikeStorage
        (direct parent only), AST statements
        ----
        ContainerHandler
        ModelMethodHandler
        SuppressHandler"]
    end

    subgraph populated ["Phase 2: Codebase populated"]
        direction TB
        P1["AfterCodebasePopulatedInterface
        fires once, after all classes are known
        ----
        data: full Codebase, complete
        ClassLikeStorage (full parent chain)
        ----
        ModelRegistrationHandler
        SuppressHandler"]
    end

    subgraph analysis ["Phase 3: Analysis (hot path)"]
        direction TB
        A1["FunctionReturnTypeProvider
        on each global function call
        ----
        args, call location, file path
        ----
        CacheHandler, PathHandler
        TransHandler
        NoEnvOutsideConfigHandler"]

        A2["MethodReturnTypeProvider
        on each method call
        ----
        args, class FQCN, method name
        ----
        AuthHandler, GuardHandler
        RequestHandler, ContainerHandler
        CommandArgumentHandler
        RelationsMethodHandler
        ModelMethodHandler
        BuilderScopeHandler, PathHandler"]

        A3["MethodParamsProvider
        before type-checking args
        ----
        overrides parameter types
        ----
        AuthHandler"]

        A4["Property providers
        Existence / Type / Visibility
        ----
        property name, class FQCN
        read/write context
        ----
        Model property handlers
        (registered via closures)"]
    end

    scanning --> populated --> analysis

Registering handlers

There are two ways to register:

  1. Class-level (most handlers): implement the interface, register via $registration->registerHooksFromClass(MyHandler::class) in Plugin::registerHandlers()
  2. Closure-level (model property handlers): register via $providers->property_type_provider->registerClosure(...) — used by ModelRegistrationHandler to bind property handlers per-model after codebase is populated

See Architecture Decisions for design rationale and Debugging with Xdebug for stepping through handler code.

External resources


Table of contents


This site uses Just the Docs, a documentation theme for Jekyll.