PHPackages                             splitstack/laravel-nudge - 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. [Mail &amp; Notifications](/categories/mail)
4. /
5. splitstack/laravel-nudge

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

splitstack/laravel-nudge
========================

Give Laravel notifications a lifecycle — they resolve themselves when their expected action is executed.

v0.6.6(1mo ago)3117↓83.5%MITPHPPHP ^8.2CI passing

Since May 31Pushed 1mo agoCompare

[ Source](https://github.com/EmilienKopp/laravel-nudge)[ Packagist](https://packagist.org/packages/splitstack/laravel-nudge)[ RSS](/packages/splitstack-laravel-nudge/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (10)Dependencies (8)Versions (22)Used By (0)

Laravel Nudge
=============

[](#laravel-nudge)

[![Tests](https://camo.githubusercontent.com/cca6c879a7ded9df48f05a29bf79db59fe1437f76e070031407cf7346cd59c4a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f656d696c69656e6b6f70702f6c61726176656c2d6e756467652f74657374732e796d6c3f6c6162656c3d7465737473)](https://camo.githubusercontent.com/cca6c879a7ded9df48f05a29bf79db59fe1437f76e070031407cf7346cd59c4a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f656d696c69656e6b6f70702f6c61726176656c2d6e756467652f74657374732e796d6c3f6c6162656c3d7465737473)[![PHP Version](https://camo.githubusercontent.com/2b772b0e9daa8f19f7ae8cbbd897a75f9daecc22ee83ef8865e569c50ac3565a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e322d626c75652e7376673f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/2b772b0e9daa8f19f7ae8cbbd897a75f9daecc22ee83ef8865e569c50ac3565a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d253545382e322d626c75652e7376673f7374796c653d666c61742d737175617265)[![Laravel Version](https://camo.githubusercontent.com/e06d835966e2c7d5e5f60b16d88a3ec1f1d16c0f58667b981d4a769a66a28859/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c61726176656c2d25354531312e302d6f72616e67652e7376673f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/e06d835966e2c7d5e5f60b16d88a3ec1f1d16c0f58667b981d4a769a66a28859/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c61726176656c2d25354531312e302d6f72616e67652e7376673f7374796c653d666c61742d737175617265)[![Total Downloads](https://camo.githubusercontent.com/879e78d02c9100927f59527301af9cb8df9b062558b1e0868897d28c990a09dc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f73706c6974737461636b2f6c61726176656c2d6e756467652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/splitstack/laravel-nudge)

 [![laravel-nudge](./art/Nudge-LOGO-round-sm.png)](./art/Nudge-LOGO-round-sm.png)

Give notifications a lifecycle. A notification declares the action it is waiting on; when that action runs anywhere in your application, the notification resolves itself — no manual wiring required.

→ [See it in action](./DEMO.md)

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

[](#installation)

```
composer require splitstack/laravel-nudge
```

Publish and run the migration:

```
php artisan vendor:publish --tag=nudge-migrations
php artisan migrate
```

The migration adds a `resolved_at` column to your `notifications` table. If the table does not exist yet, it creates it.

Concept
-------

[](#concept)

A notification is not just a message — it is a pending state tied to an expected future action.

```
User is notified to install the GitHub App
  └─ notification stored with action_key = "github.install", user_id = 5

User installs the GitHub App (via webhook, OAuth callback, CLI — anywhere)
  └─ nudge(InstallGitHubApp::class, ['user_id' => 5, 'installation_id' => 88])
       └─ fires ActionExecuted("github.install", [...])
            └─ listener finds the notification, stamps resolved_at

```

The controller, the webhook handler, and the notification have no knowledge of each other.

Actions
-------

[](#actions)

There are two ways to make an action class resolvable, depending on whether it already exists or is being written from scratch.

---

### New action classes — extend `NudgeAction`

[](#new-action-classes--extend-nudgeaction)

If you are writing an action class from scratch, extend `NudgeAction`. Implement your logic in a `protected nudge()` method (convention) or in any method marked `#[Nudge]` (for a custom name). The `handle()` entry point, event dispatch, and resolution are all handled for you.

```
use Splitstack\Nudge\NudgeAction;

class InstallGitHubApp extends NudgeAction
{
    public function actionKey(): string
    {
        return 'github.install';
    }

    protected function nudge(int $user_id, int $installation_id): mixed
    {
        // your logic here — no event dispatch needed
    }
}
```

If you prefer a different method name, mark it with `#[Nudge]`:

```
use Splitstack\Nudge\Attributes\Nudge;
use Splitstack\Nudge\NudgeAction;

class InstallGitHubApp extends NudgeAction
{
    public function actionKey(): string
    {
        return 'github.install';
    }

    #[Nudge]
    protected function install(int $user_id, int $installation_id): mixed
    {
        // your logic here
    }
}
```

**Call sites** — pick whichever style fits your codebase:

```
// Global helper
nudge(InstallGitHubApp::class, ['user_id' => $user->id]);

// Facade
use Splitstack\Nudge\Facades\Nudge;
Nudge::run(InstallGitHubApp::class, ['user_id' => $user->id]);
// or
Nudge::run($myActionInstance, ['user_id' => $user->id]);

// Direct call — use named args
$action->handle(user_id: $user->id, installation_id: 88);
```

---

### Existing action classes — use the trait

[](#existing-action-classes--use-the-trait)

If you have an action class that already exists and already has its own call sites, you do not need to rewrite call sites. (You're free to do so if you want, of course. In that case refer to the "New action classes" section above)

Implement `ResolvableAction`, add the `DispatchesActionExecuted` trait, and drop `$this->nudge($params)` at the point in your method where the action completes. Your call site stays exactly as it was.

```
use Splitstack\Nudge\Concerns\DispatchesActionExecuted;
use Splitstack\Nudge\Contracts\ResolvableAction;

class InstallGitHubApp implements ResolvableAction
{
    use DispatchesActionExecuted;

    public function actionKey(): string // Add this method to declare the action key that resolves notifications
    {
        return 'github.install';
    }

    public function doOrDoNotThereIsNoTry(string $action, array $installation, array $repositories): mixed
    {
        // your existing logic, untouched

        $this->nudge(installation_id: $installation['id']); // ← only addition; pass only what's needed for matching

        return $result;
    }
}
```

```
// call site — completely unchanged
(new InstallGitHubApp)->doOrDoNotThereIsNoTry($action, $installation, $repositories);
```

---

You can also dispatch the event manually as an escape hatch — useful for actions you don't own:

```
use Splitstack\Nudge\Events\ActionExecuted;

ActionExecuted::dispatch('github.install', ['user_id' => $user->id]);
```

Notifications
-------------

[](#notifications)

Extend `ActionableNotification` and supply the action key either via `forAction()` at call site or by overriding `useActionKey()` on the class.

**Option A — `forAction()` at call site** (action key decided by the caller):

```
$user->notify(
    (new GitHubSetupReminder)->forAction('github.install', ['user_id' => $user->id])
);
```

**Option B — `useActionKey()` on the class** (action key baked into the notification):

```
class AppNotification extends ActionableNotification
{
    public function __construct(
        public readonly string $message,
        public readonly ?string $actionKey = null,
    ) {}

    protected function withData(object $notifiable): array
    {
        return ['message' => $this->message];
    }

    public function useActionKey(): ?string
    {
        return $this->actionKey;
    }
}

// sending (no params):
$user->notify(new AppNotification('Connect your GitHub account.', 'github.install'));

// sending (with params):
$user->notify(
    (new AppNotification('Connect your GitHub account.', 'github.install'))
        ->withParams(['user_id' => $user->id])
);
```

`withData()` is optional — omit it if your notification needs no payload beyond the action metadata. If both `useActionKey()` and `forAction()` are used, `useActionKey()` takes precedence.

### Param matching

[](#param-matching)

The stored params are matched as a **subset** of the executed params. This means:

```
// stored:   ['user_id' => 5]
// executed: ['user_id' => 5, 'installation_id' => 88]
// → resolves ✓

// stored:   ['user_id' => 5]
// executed: ['user_id' => 9]
// → does not resolve ✗
```

Extra keys in the executed params are ignored. Store only the params that must match.

#### Deep matching

[](#deep-matching)

By default matching is **shallow** — only top-level keys are compared. Set `match_params` to `'deep'` in `config/nudge.php` to enable recursive subset matching, useful when your action params contain nested arrays:

```
// config/nudge.php
'match_params' => 'deep',
```

```
// stored:   ['installation' => ['id' => 88]]
// executed: ['installation' => ['id' => 88, 'account' => 'acme'], 'user_id' => 5]
// shallow → does not resolve ✗  (array !== array strict comparison)
// deep    → resolves ✓          (['id' => 88] is a subset of ['id' => 88, 'account' => 'acme'])
```

Matching is strict (`===`) at every level regardless of mode, so `'5'` and `5` are not considered equal.

Querying
--------

[](#querying)

Add `HasResolvableNotifications` to your notifiable model for convenience scopes:

```
use Splitstack\Nudge\Models\Concerns\HasResolvableNotifications;

class User extends Authenticatable
{
    use HasResolvableNotifications;
}
```

```
$user->pendingNotifications()->get();
$user->resolvedNotifications()->get();
```

Or query directly on `DatabaseNotification`:

```
use Illuminate\Notifications\DatabaseNotification;

DatabaseNotification::whereNull('resolved_at')->get();
DatabaseNotification::whereNotNull('resolved_at')->get();
```

Migrating an existing notification class
----------------------------------------

[](#migrating-an-existing-notification-class)

If you already have a notification with a `toDatabase()` method and you swap `extends Notification` for `extends ActionableNotification`, **do not keep the `toDatabase()` override**. `ActionableNotification::toDatabase()` is what injects `_action_key` and `_action_params` into the stored payload — overriding it silently strips that metadata and the notification will never resolve.

Replace `toDatabase()` with `withData()` instead:

```
// before — toDatabase() override will break resolution
class MyNotification extends ActionableNotification
{
    public function toDatabase(object $notifiable): array
    {
        return ['message' => 'Do the thing.'];
    }
}

// after
class MyNotification extends ActionableNotification
{
    protected function withData(object $notifiable): array
    {
        return ['message' => 'Do the thing.'];
    }
}
```

`withData()` is merged into the final payload by the parent; your data and the action metadata both end up in the database.

Upgrading from &lt; 0.6.0
-------------------------

[](#upgrading-from--060)

The old `execute()` entry point and the requirement to name your handler `handle()` have been removed in favour of the two-path API above. `execute()` still works but emits `E_USER_DEPRECATED` — follow the deprecation message to migrate at your own pace.

Caveats
-------

[](#caveats)

### Avoid sending notifications inside controller methods that can be hit multiple times

[](#avoid-sending-notifications-inside-controller-methods-that-can-be-hit-multiple-times)

Some middleware or frontend patterns cause controller methods to be invoked more than once per user interaction. A well-known case is [Inertia.js deferred props](https://inertiajs.com/deferred-props): a `HandleInertiaRequests` middleware that uses `Inertia::defer()` triggers a second request to the same route, calling the controller twice. If `$user->notify(...)` is in that controller, the same notification gets stored twice.

The safer placement is inside a dedicated action, a queued job, an observer, or a service — anywhere outside the HTTP layer that can be hit multiple times.

```
// risky: controller may be called more than once
public function dashboard(Request $request): Response
{
    $request->user()->notify(new SomeReminder); // could fire twice
    return Inertia::render('Dashboard');
}

// safer: notification lives outside the repeatedly-hit method
public function dashboard(Request $request): Response
{
    SendOnboardingReminder::dispatchIf(
        ! $request->user()->hasCompletedOnboarding()
    );
    return Inertia::render('Dashboard');
}
```

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11+

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance90

Actively maintained with recent releases

Popularity17

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 95.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

19

Last Release

53d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b9e77e55bb8341a1fa99f8348597070b4de230b5fc457e866e4f196dd56843f1?d=identicon)[EmilienKopp](/maintainers/EmilienKopp)

---

Top Contributors

[![EmilienKopp](https://avatars.githubusercontent.com/u/91975560?v=4)](https://github.com/EmilienKopp "EmilienKopp (43 commits)")[![Copilot](https://avatars.githubusercontent.com/in/1143301?v=4)](https://github.com/Copilot "Copilot (2 commits)")

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/splitstack-laravel-nudge/health.svg)

```
[![Health](https://phpackages.com/badges/splitstack-laravel-nudge/health.svg)](https://phpackages.com/packages/splitstack-laravel-nudge)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k30.2M151](/packages/laravel-cashier)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45444.2k1](/packages/pressbooks-pressbooks)[spatie/laravel-health

Monitor the health of a Laravel application

87912.0M177](/packages/spatie-laravel-health)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M246](/packages/laravel-ai)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)

PHPackages © 2026

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