PHPackages                             humanik/phpstan-laravel-actions - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. humanik/phpstan-laravel-actions

ActivePhpstan-extension

humanik/phpstan-laravel-actions
===============================

PHPStan extension for lorisleiva/laravel-actions: return types, throw types and argument validation for action classes.

v0.1.1(today)00[1 PRs](https://github.com/humanik/phpstan-laravel-actions/pulls)MITPHPPHP ^8.2CI passing

Since Aug 1Pushed todayCompare

[ Source](https://github.com/humanik/phpstan-laravel-actions)[ Packagist](https://packagist.org/packages/humanik/phpstan-laravel-actions)[ RSS](/packages/humanik-phpstan-laravel-actions/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (6)Versions (6)Used By (0)

phpstan-laravel-actions
=======================

[](#phpstan-laravel-actions)

A [PHPStan](https://phpstan.org) extension for [`lorisleiva/laravel-actions`](https://github.com/lorisleiva/laravel-actions).

Laravel Actions exposes every entry point as `mixed ...$arguments): mixed`:

```
public static function run(mixed ...$arguments): mixed
public static function dispatch(mixed ...$arguments): PendingDispatch
public function __invoke(mixed ...$arguments): mixed
```

Static analysis therefore sees nothing. Wrong argument counts, wrong argument types and lost return types all pass silently, and `@throws` never propagates. This extension reads the signature of the method each proxy actually forwards to — `handle()` or `asJob()` — and puts that information back.

```
class SendInvite
{
    use AsAction;

    public function handle(string $email, int $teamId): bool { /* ... */ }
}

SendInvite::run('a@b.com');        // SendInvite::handle() invoked via run() with 1 argument, exactly 2 expected.
SendInvite::run(1, 'a@b.com');     // Parameter #1 $email of SendInvite::handle() invoked via run() expects string, int given.
SendInvite::dispatch('a@b.com');   // SendInvite::handle() invoked via dispatch() with 1 argument, exactly 2 expected.

$ok = SendInvite::run('a@b.com', 1);   // bool, instead of mixed
```

Your IDE cannot read a PHPStan extension, so the same information is also available as a generated file — see [IDE helper](#ide-helper).

Installation
------------

[](#installation)

```
composer require --dev humanik/phpstan-laravel-actions
```

With [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer) the extension registers itself. Otherwise include it manually:

```
includes:
    - vendor/humanik/phpstan-laravel-actions/extension.neon
```

Requires PHP 8.2+ and PHPStan 2.1+. It has no other dependencies — Larastan is neither required nor conflicting.

What it does
------------

[](#what-it-does)

### Action detection

[](#action-detection)

A class counts as an action when it uses the relevant Laravel Actions trait, however it got there:

```
class A { use AsAction; }                  // via the AsAction bundle
class B { use AsObject; }                  // a single concern
class C extends Lorisleiva\Actions\Action {}   // the base class
class D extends C {}                       // any subclass
```

Detection uses `ClassReflection::getTraits(recursive: true)`. The more obvious `hasTraitUse()` walks parent classes with the raw `ReflectionClass::getTraitNames()`, which reports only *immediately* used traits — so `class C extends Action` resolves to `[AsAction]`and never to `AsObject`, and every such action would be silently ignored. `ActionDetectionTest`locks this behaviour down.

### Return types

[](#return-types)

CallInferred type`Action::run(...)`return type of `handle()``Action::runIf(...)` / `runUnless(...)``handle()`'s type | `Illuminate\Support\Fluent``Action::dispatchSync(...)` / `dispatchNow(...)`return type of `asJob()`, else `handle()``Action::makeJob(...)``UniqueJobDecorator` when the action implements `ShouldBeUnique``Action::mock()` / `spy()` / `partialMock()``Mockery\MockInterface&YourAction``$action(...)` / `$action->__invoke(...)`return type of `handle()``make()`, `dispatch()`, `dispatchIf()`, `dispatchUnless()`, `dispatchAfterResponse()` and `makeUniqueJob()` already declare accurate types, so they are left to PHPStan.

The Mockery intersection is what makes `MyAction::mock()->shouldReceive('handle')` and `partialMock()` keep both APIs visible at once.

### Throw types

[](#throw-types)

`@throws` declared on `handle()` / `asJob()` propagates through `run()`, `runIf()`, `runUnless()`, the `dispatch*` family and `$action->__invoke()`, so PHPStan's `missingCheckedExceptionInThrows` check works through the proxies.

### Argument validation

[](#argument-validation)

Arguments at the call site are checked against the target method's real signature, including named arguments and variadics.

IdentifierReported when`laravelActions.tooFewArguments`fewer arguments than the target requires`laravelActions.tooManyArguments`more arguments than the target accepts`laravelActions.argumentType`an argument's type is not accepted by the parameter`laravelActions.unknownNamedArgument`a named argument matches no parameter`laravelActions.missingHandle`a proxy is used on an action that declares no `handle()` (or `asJob()`)Counts and parameter positions are reported as they were written, so the leading condition of `runIf()` / `dispatchIf()` is included in the numbering.

All five share one prefix, so they can be silenced together:

```
parameters:
    ignoreErrors:
        - identifier: laravelActions.*
```

#### Jobs that receive a prepended argument

[](#jobs-that-receive-a-prepended-argument)

When an action is dispatched as a job, `JobDecorator` may insert an extra first argument:

```
public function asJob(JobDecorator $job, Team $team): void {}

SendInvites::dispatch($team);   // one argument, two parameters — correct, and not reported
```

The extension reproduces `JobDecorator::getPrependedParameters()` branch for branch, including the fact that a *variadic* target never takes the early return. Because the decision depends only on the number of dispatched arguments, this is exact rather than a heuristic — correct code is never flagged, and genuine mistakes behind the prepended argument still are.

IDE helper
----------

[](#ide-helper)

Everything above is invisible to an editor: PhpStorm sees `run(mixed ...$arguments): mixed`and offers nothing. The bundled generator writes the same signatures out as `@method` tags.

```
vendor/bin/laravel-actions-ide-helper
```

It scans `app/` by default and writes `_ide_helper_actions.php`, re-declaring each action as an empty stub that carries the tags:

```
namespace App\Actions {
    /**
     * @method static bool run(string $email, int $teamId)
     * @method static bool|\Illuminate\Support\Fluent runIf(bool $boolean, string $email, int $teamId)
     * @method bool __invoke(string $email, int $teamId)
     * @method static \Illuminate\Foundation\Bus\PendingDispatch dispatch(string $email, int $teamId)
     * @method static bool dispatchSync(string $email, int $teamId)
     * @method static \Lorisleiva\Actions\Decorators\JobDecorator makeJob(string $email, int $teamId)
     * @method static \Mockery\MockInterface&\App\Actions\SendInvite mock()
     */
    class SendInvite {}
}
```

**Exclude the generated file from static analysis and from your autoloader.** It re-declares classes on purpose; PHPStan and Psalm will otherwise report them as duplicates.

```
parameters:
    excludePaths:
        - _ide_helper_actions.php
```

```
laravel-actions-ide-helper [...] [options]

  ...          Directories or files to scan. Default: app
  --output=    Helper file to write. Default: _ide_helper_actions.php
  --write            Inject the tags into the action sources instead
  --dry-run          Report what would change and write nothing; exits 1 when stale
  --autoload=  Path to composer's autoload.php. Auto-detected by default
  -q, --quiet        Only report problems
  -h, --help         Show the usage

```

`--dry-run` is meant for CI: commit the helper file and fail the build when it drifts.

```
- run: vendor/bin/laravel-actions-ide-helper --dry-run
```

### Writing into the action files instead

[](#writing-into-the-action-files-instead)

`--write` skips the separate file and puts the tags into each action's own docblock, between markers so that regeneration is idempotent and anything you wrote by hand survives:

```
/**
 * Sends an invite. This line is left alone.
 *
 * @method void somethingHandWritten()
 *
 * @laravel-actions-ide-helper-start
 * @method static bool run(string $email, int $teamId)
 * @laravel-actions-ide-helper-end
 */
class SendInvite
```

No duplicate declarations, no exclusions to configure, and every editor sees the tags — at the cost of touching your sources, so review the diff.

### Types

[](#types)

Parameter and return types come from the target's real signature, and from its docblock where it has one — the docblock wins, since it is the more precise of the two:

```
/** @return Collection */
public function handle(int $id): Collection

// @method static \Illuminate\Support\Collection run(int $id)
```

Short names are resolved against the `use` statements of the file that declares the method, which is not necessarily the action's own file when `handle()` is inherited. `@phpstan-param`/`@phpstan-return` outrank `@psalm-*`, which outrank the plain tags, as they do for PHPStan itself.

### Limitations

[](#limitations)

- **PhpStorm reports "multiple definitions exist for class"** for the generated helper file. That is inherent to the `_ide_helper*.php` convention; use `--write` to avoid it entirely.
- **Generics are only as good as your docblocks.** `handle(): Collection` with no `@return`yields `\Illuminate\Support\Collection`, because that is all PHP was told.
- **The prepended `JobDecorator`/`?Batch` parameter is always dropped** from the `dispatch*`and `makeJob*` tags. The runtime decides per call site, from the argument count; one `@method` tag has to describe every call site, and dropping it is right for all the calls that do not pass the decorator explicitly. `JobPrependParityTest` pins this down.
- **`static` is resolved to the concrete action**, since `@method static static run()` is ambiguous to read.
- **Abstract actions are skipped**; the concrete subclass that inherits `handle()` is not.

Known limitations
-----------------

[](#known-limitations)

- **Phantom `@method` tags.** `AsController`, `AsJob`, `AsCommand`, `AsListener` and `WithAttributes` declare `@method`/`@property-read` tags for hooks your class does not implement (`asController()`, `getJobMiddleware()`, `$jobTries`, ...). PHPStan therefore believes those are callable on any action. Validating hook *declarations* is out of scope here; the extension only resolves forwarding targets through native reflection, so it is itself unaffected.
- **`$action($x)` and `@throws`.** PHPStan offers no throw-type extension point for the `FuncCall` form of an invocation. Use `$action->__invoke($x)` or `$action->handle($x)` when exception propagation matters. Argument validation works for both forms.
- **`__invoke()` targets `handle()`, not `asController()`.** The trait method literally does `return $this->handle(...$arguments)`. `ControllerDecorator` rewrites routes to call `asController()` directly and never goes through `__invoke()`.
- **Spread calls are skipped.** `Action::run(...$args)` has an unknowable arity.
- **Ambiguous callers are skipped.** A caller whose type is a union of several classes has no single target signature.
- **Named arguments** are type-checked, but arity checking is left to PHPStan for those calls.

Development
-----------

[](#development)

```
composer install
vendor/bin/phpunit
vendor/bin/phpstan analyse
```

Fixtures live in `tests/Fixtures`; the files with deliberate errors are analysed by `RuleTestCase`, and the `assertType()` files by `TypeInferenceTestCase`.

The IDE helper has its own fixture tree, `tests/Fixtures/IdeHelper`, so that adding a fixture for a rule test cannot churn the generator's golden file. That file is the generator's end-to-end test; refresh it after an intended change:

```
php bin/laravel-actions-ide-helper tests/Fixtures/IdeHelper --output=tests/IdeHelper/expected/actions.php.txt
```

Nothing under `src/` may import a Laravel, laravel-actions or Mockery class — the generator included, which is why it works through native reflection and the string constants in `src/LaravelActions.php`. CI enforces it by loading the extension in a project that has none of them installed.

License
-------

[](#license)

MIT.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 Bus Factor1

Top contributor holds 55.6% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/46780988?v=4)[humanik](/maintainers/humanik)[@humanik](https://github.com/humanik)

---

Top Contributors

[![humanik](https://avatars.githubusercontent.com/u/46780988?v=4)](https://github.com/humanik "humanik (5 commits)")[![Copilot](https://avatars.githubusercontent.com/in/1143301?v=4)](https://github.com/Copilot "Copilot (4 commits)")

---

Tags

PHPStanlaravelstatic analysisactionslaravel-actions

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/humanik-phpstan-laravel-actions/health.svg)

```
[![Health](https://phpackages.com/badges/humanik-phpstan-laravel-actions/health.svg)](https://phpackages.com/packages/humanik-phpstan-laravel-actions)
```

###  Alternatives

[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.5k60.6M9.8k](/packages/larastan-larastan)[deptrac/deptrac

Deptrac is a static code analysis tool that helps to enforce rules for dependencies between software layers.

3.0k9.8M220](/packages/deptrac-deptrac)[dedoc/scramble

Automatic generation of API documentation for Laravel applications.

2.2k12.6M122](/packages/dedoc-scramble)[shipmonk/dead-code-detector

Dead code detector to find unused PHP code via PHPStan extension. Can automatically remove dead PHP code. Supports libraries like Symfony, Doctrine, PHPUnit etc. Detects dead cycles. Can detect dead code that is tested.

5004.2M109](/packages/shipmonk-dead-code-detector)[phpstan/phpstan-doctrine

Doctrine extensions for PHPStan

67375.0M1.5k](/packages/phpstan-phpstan-doctrine)[rector/rector-src

Instant Upgrade and Automated Refactoring of any PHP code

136411.0k14](/packages/rector-rector-src)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
