PHPackages                             ahmedebead/laravel-hooks - 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. [Utility &amp; Helpers](/categories/utility)
4. /
5. ahmedebead/laravel-hooks

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

ahmedebead/laravel-hooks
========================

A standalone, framework-agnostic hook system for Laravel applications. Attach lifecycle callbacks (before/after/error) to any service method with sync, queued, delayed, batched, and conditional execution strategies.

v1.1(3mo ago)013MITPHPPHP ^8.2|^8.3

Since Apr 16Pushed 3mo agoCompare

[ Source](https://github.com/ahmed3bead/laravel-hooks)[ Packagist](https://packagist.org/packages/ahmedebead/laravel-hooks)[ RSS](/packages/ahmedebead-laravel-hooks/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (2)Dependencies (6)Versions (3)Used By (0)

 [![Lara-CRUD Banner](https://github.com/ahmed3bead/laravel-hooks/raw/main/image.png?raw=true)](https://github.com/ahmed3bead/laravel-hooks/blob/main/image.png?raw=true)

Laravel Hooks
=============

[](#laravel-hooks)

Add **before**, **after**, and **error** lifecycle hooks to any method in any class — controllers, services, jobs, models, or plain PHP objects.

No event system boilerplate. No observers. Just attach a hook to a method and it runs automatically.

---

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

[](#requirements)

- PHP 8.2+
- Laravel 10, 11, 12, or 13

---

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

[](#installation)

```
composer require ahmedebead/laravel-hooks
```

Laravel auto-discovers the package. To publish the config:

```
php artisan vendor:publish --tag=laravel-hooks-config
```

---

Quick Start
-----------

[](#quick-start)

### 1. Add the trait to any class

[](#1-add-the-trait-to-any-class)

```
use Ahmed3bead\LaravelHooks\HookableTrait;

class OrderService
{
    use HookableTrait;

    public function create(array $data): Order
    {
        return $this->executeWithHooks('create', function () use ($data) {
            return Order::create($data);
        });
    }
}
```

### 2. Create a hook

[](#2-create-a-hook)

```
php artisan make:hook OrderCreated --type=audit --phase=after --sync
```

Or manually:

```
use Ahmed3bead\LaravelHooks\BaseHookJob;
use Ahmed3bead\LaravelHooks\HookContext;

class OrderCreatedHook extends BaseHookJob
{
    public function handle(HookContext $context): void
    {
        $order = $context->getModelFromResult();

        AuditLog::create([
            'action'   => 'order.created',
            'order_id' => $order?->id,
            'user_id'  => $context->getUserId(),
        ]);
    }
}
```

### 3. Register the hook

[](#3-register-the-hook)

**From inside the class** — `registerHooks()` is called automatically when the class first uses hooks:

```
class OrderService
{
    use HookableTrait;

    protected function registerHooks(): void
    {
        $this->afterHook('create', OrderCreatedHook::class);
    }
}
```

**From a service provider** — register hooks for any class from anywhere:

```
use Ahmed3bead\LaravelHooks\HookManager;

class AppServiceProvider extends ServiceProvider
{
    public function boot(HookManager $hooks): void
    {
        $hooks->addSyncHook(OrderService::class, 'create', 'after', OrderCreatedHook::class);
    }
}
```

**Inline, anywhere** — fluent API on any instance:

```
$orderService->afterHook('create', OrderCreatedHook::class);
```

---

Works With Any Class
--------------------

[](#works-with-any-class)

`HookableTrait` is not limited to services:

```
class UserController  { use HookableTrait; }
class ProcessOrderJob { use HookableTrait; }
class DataImporter    { use HookableTrait; }
class User extends Model { use HookableTrait; }
```

---

Execution Phases
----------------

[](#execution-phases)

PhaseWhen it fires`before`Before the method body runs`after`After the method returns`error`When the method throws — hook fires, exception re-thrown---

Registering Hooks
-----------------

[](#registering-hooks)

### From inside the class (public API)

[](#from-inside-the-class-public-api)

```
// Phase shortcuts — always synchronous
$this->beforeHook('create', MyHook::class);
$this->afterHook('create', MyHook::class);
$this->errorHook('create', MyHook::class);

// Explicit sync — choose any phase
$this->syncHook('before', 'create', MyHook::class);
$this->syncHook('after',  'create', MyHook::class);
$this->syncHook('error',  'create', MyHook::class);

// Inline closure — no separate class needed
$this->syncHookWithLogic('after', 'create', function (HookContext $ctx) {
    Log::info('Created', ['user' => $ctx->getUserId()]);
});

// Any strategy
$this->hook('after', 'create', MyHook::class, strategy: 'queue');
$this->hook('after', 'create', MyHook::class, strategy: 'delay', options: ['delay' => 300]);

// Inline closure with any strategy
$this->hookWithLogic('after', 'create', function (HookContext $ctx) {
    // runs in the background
}, strategy: 'queue');
```

### From outside the class (HookManager)

[](#from-outside-the-class-hookmanager)

```
$hooks = app(HookManager::class);

$hooks->addSyncHook(OrderService::class, 'create', 'after', AuditHook::class);
$hooks->addQueuedHook(OrderService::class, 'create', 'after', EmailHook::class);
$hooks->addDelayedHook(OrderService::class, 'create', 'after', FollowUpHook::class, delay: 3600);
$hooks->addBatchedHook(OrderService::class, 'index', 'after', AnalyticsHook::class);

// Register multiple at once
$hooks->addHooks([
    ['target' => OrderService::class, 'method' => 'create', 'phase' => 'after', 'hook' => AuditHook::class],
    ['target' => OrderService::class, 'method' => 'update', 'phase' => 'after', 'hook' => AuditHook::class],
]);

// Global hook — fires for every class using HookableTrait
$hooks->addGlobalHook('create', 'after', GlobalAuditHook::class);
```

---

Execution Strategies
--------------------

[](#execution-strategies)

### Sync (default)

[](#sync-default)

Runs immediately in the same request.

```
$this->afterHook('create', AuditHook::class);
// or
$hooks->addSyncHook(OrderService::class, 'create', 'after', AuditHook::class);
```

### Queued

[](#queued)

Pushed to a Laravel queue and processed in the background.

```
$this->hook('after', 'create', EmailHook::class, strategy: 'queue');
// or
$hooks->addQueuedHook(OrderService::class, 'create', 'after', EmailHook::class);
```

### Delayed

[](#delayed)

Queued with a delay (seconds).

```
$this->hook('after', 'create', FollowUpHook::class, strategy: 'delay', options: ['delay' => 3600]);
// or
$hooks->addDelayedHook(OrderService::class, 'create', 'after', FollowUpHook::class, delay: 3600);
```

### Batched

[](#batched)

Collects executions and processes them together when the batch is full.

```
$hooks->addBatchedHook(OrderService::class, 'index', 'after', AnalyticsHook::class, options: [
    'batch_size' => 50,
    'batch_delay' => 60, // seconds
]);
```

### Conditional

[](#conditional)

Wraps any strategy with runtime conditions.

```
use Ahmed3bead\LaravelHooks\Strategies\ConditionalHookStrategy;
use Ahmed3bead\LaravelHooks\Strategies\SyncHookStrategy;

$strategy = (new ConditionalHookStrategy(new SyncHookStrategy()))
    ->onlyInEnvironment('production')
    ->onlyWhenConfigEnabled('features.audit');

$hooks->registerStrategy('prod_audit', $strategy);
$hooks->addHook(OrderService::class, 'create', 'after', AuditHook::class, 'prod_audit');
```

---

Wrapping a Method
-----------------

[](#wrapping-a-method)

Use `executeWithHooks()` inside any method to trigger the full before/after/error cycle automatically:

```
public function process(array $data): mixed
{
    return $this->executeWithHooks('process', function () use ($data) {
        return $this->doWork($data);
    }, $data); // optional: pass $data into HookContext::$data
}
```

---

Writing Hook Classes
--------------------

[](#writing-hook-classes)

### Extend BaseHookJob

[](#extend-basehookjob)

```
use Ahmed3bead\LaravelHooks\BaseHookJob;
use Ahmed3bead\LaravelHooks\HookContext;

class MyHook extends BaseHookJob
{
    protected int $priority = 10;           // lower = runs first (default: 100)
    protected bool $async = false;          // true = queued
    protected string $queueName = 'hooks';  // queue name when async

    public function handle(HookContext $context): void
    {
        // your logic
    }
}
```

### HookContext — what's available

[](#hookcontext--whats-available)

```
public function handle(HookContext $context): void
{
    // Core
    $context->method;                  // 'create', 'update', ...
    $context->phase;                   // 'before', 'after', 'error'
    $context->data;                    // value passed as $data to executeWithHooks()
    $context->result;                  // raw return value (after/error phase)
    $context->target;                  // the object the method was called on
    $context->user;                    // Auth::user() at the time of the call

    // Helpers
    $context->isBefore();              // true when phase === 'before'
    $context->isAfter();               // true when phase === 'after'
    $context->getParameter('key');     // named parameter by key
    $context->getMetadata('key');      // metadata value by key

    // Model extraction (works with plain models and wrapped responses)
    $context->getModelFromResult();    // first Eloquent model found in result
    $context->getDataFromResult();     // unwrapped data from result
    $context->getModelAttributes();    // model->toArray()
    $context->getModelChanges();       // model->getChanges()
    $context->getOriginalAttributes(); // model->getOriginal()
    $context->wasModelRecentlyCreated(); // model->wasRecentlyCreated

    // Response helpers (when result implements WrappedResponseInterface)
    $context->getStatusCode();         // HTTP status code
    $context->getMessage();            // response message
    $context->isSuccessful();          // true for 2xx status codes
    $context->hasWrappedResponse();    // true if result is a WrappedResponseInterface

    // User
    $context->getUserId();             // $user->id ?? $user->getKey()
}
```

### Conditional execution

[](#conditional-execution)

```
class MyHook extends BaseHookJob
{
    public function shouldExecute(HookContext $context): bool
    {
        return $context->user !== null; // only run for authenticated users
    }
}
```

---

Inline Closures (No Class Required)
-----------------------------------

[](#inline-closures-no-class-required)

For quick, one-off hooks you don't need a full class:

```
// Synchronous inline hook
$this->syncHookWithLogic('after', 'create', function (HookContext $ctx) {
    Log::info('Created', [
        'method'  => $ctx->method,
        'user_id' => $ctx->getUserId(),
    ]);
});

// Inline hook with any strategy
$this->hookWithLogic('after', 'create', function (HookContext $ctx) {
    dispatch(new SendWelcomeEmail($ctx->getUserId()));
}, strategy: 'queue');
```

---

Priority
--------

[](#priority)

Lower numbers run first. Default priority is `100`.

```
// Runs first
$hooks->addHook(OrderService::class, 'create', 'after', ValidationHook::class, 'sync', ['priority' => 10]);

// Runs last
$hooks->addHook(OrderService::class, 'create', 'after', NotificationHook::class, 'sync', ['priority' => 200]);
```

---

Custom Response Objects
-----------------------

[](#custom-response-objects)

If your methods return a custom response wrapper, implement `WrappedResponseInterface` so hooks can extract the model and status automatically:

```
use Ahmed3bead\LaravelHooks\Contracts\WrappedResponseInterface;

class ApiResponse implements WrappedResponseInterface
{
    public function getData(): mixed      { return $this->data; }
    public function getStatusCode(): int  { return $this->statusCode; }
    public function getMessage(): string  { return $this->message; }
}
```

---

Artisan Commands
----------------

[](#artisan-commands)

### Generate a hook class

[](#generate-a-hook-class)

```
php artisan make:hook OrderCreated
php artisan make:hook OrderCreated --type=audit --phase=after --queue
php artisan make:hook OrderCreated --type=notification --sync
```

Available `--type` values: `audit`, `notification`, `cache`, `logging`, `validation`, `security`, `analytics`, `general`

### Manage hooks at runtime

[](#manage-hooks-at-runtime)

```
php artisan hooks:manage list                                        # all registered hooks
php artisan hooks:manage stats                                       # counts, strategies, debug mode
php artisan hooks:manage debug --target="App\Services\OrderService" # hooks for one class
php artisan hooks:manage test                                        # verify the system loads
php artisan hooks:manage enable
php artisan hooks:manage disable --force
php artisan hooks:manage clear  --force
php artisan hooks:manage flush                                       # flush pending batched hooks
php artisan hooks:manage export --export=hooks.json
```

---

Configuration
-------------

[](#configuration)

`config/laravel-hooks.php`:

KeyDefaultDescription`enabled``true`Enable/disable all hooks globally`debug``false`Log every hook registration and execution`queue_connection``null`Queue connection for async hooks (`null` = default)`default_queue``'default'`Queue name for queued/delayed hooks`batch_queue``'batch'`Queue name for batched hooks`generation_directory``'App\\Hooks'`Namespace for generated hook classesEnvironment variables:

```
LARAVEL_HOOKS_ENABLED=true
LARAVEL_HOOKS_DEBUG=false
LARAVEL_HOOKS_QUEUE_CONNECTION=redis
LARAVEL_HOOKS_DEFAULT_QUEUE=default
LARAVEL_HOOKS_BATCH_QUEUE=batch
```

---

Testing
-------

[](#testing)

```
vendor/bin/pest
vendor/bin/pest --coverage
```

Use `Queue::fake()` to assert queued hooks were dispatched without processing them:

```
use Illuminate\Support\Facades\Queue;
use Ahmed3bead\LaravelHooks\Jobs\QueuedHookJob;

Queue::fake();

$service->create(['name' => 'Test']);

Queue::assertPushed(QueuedHookJob::class);
```

---

Upgrading from an Earlier Version
---------------------------------

[](#upgrading-from-an-earlier-version)

### Renamed identifiers

[](#renamed-identifiers)

OldNew`HookContext::$service``HookContext::$target``addServiceSyncHook()``addSyncHookRegistration()``addServiceQueuedHook()``addQueuedHookRegistration()``addServiceDelayedHook()``addDelayedHookRegistration()``addServiceBatchedHook()``addBatchedHookRegistration()``addServiceHook()``addHookRegistration()``removeServiceHooks()``removeHooks()``removeServiceHook()``removeHook()``enableServiceHooks()``enableHooks()``debugService()``debugTarget()``--service` CLI flag`--target` CLI flag`total_service_hooks` stat key`total_target_hooks``service_hooks` array key`target_hooks`All old names still work but emit `E_USER_DEPRECATED`. Update at your own pace.

---

License
-------

[](#license)

MIT

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance81

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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

99d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/98d9e0c899adb8f509d8d30bfaaaff6393df92ed0daadc384c16c201d4b8e52c?d=identicon)[ahmedm3bead](/maintainers/ahmedm3bead)

---

Top Contributors

[![AhmedMEbead](https://avatars.githubusercontent.com/u/157358238?v=4)](https://github.com/AhmedMEbead "AhmedMEbead (9 commits)")

---

Tags

laraveleventsservicehookspipelinelifecycle

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ahmedebead-laravel-hooks/health.svg)

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

###  Alternatives

[firefly-iii/data-importer

Firefly III Data Import Tool.

8055.8k](/packages/firefly-iii-data-importer)[markwalet/nova-modal-response

A Laravel Nova asset for Modal responses on an action.

17878.9k](/packages/markwalet-nova-modal-response)[team-nifty-gmbh/tall-datatables

Server-side rendered datatables for Laravel and Livewire

1320.9k4](/packages/team-nifty-gmbh-tall-datatables)[tomshaw/electricgrid

A feature-rich Livewire package designed for projects that require dynamic, interactive data tables.

119.4k](/packages/tomshaw-electricgrid)

PHPackages © 2026

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