DEV Community

Giacomo Masseroni
Giacomo Masseroni

Posted on

Laravel Pulse card for Clean Architecture UseCase

When building Laravel applications with Clean Architecture principles, tracking the performance and execution of your UseCases becomes crucial for monitoring application health.

Laravel Pulse provides an excellent framework for real-time application monitoring, and with the php-clean-architecture package, you can create custom recorder cards to track UseCase execution metrics.

Understanding the Architecture

The php-clean-architecture package helps you implement Clean Architecture patterns in Laravel by providing a structured approach to UseCases.

When combined with Laravel Pulse, you can gain valuable insights into how your UseCases perform in production.

Our custom recorder will track two key events:

  • UseCaseStarted: Fired when a UseCase begins execution
  • UseCaseCompleted: Fired when a UseCase finishes execution

By measuring the time between these events, we can calculate execution duration and aggregate statistics.

The Custom Recorder

Let's examine the UseCases recorder class that makes this possible:

<?php

declare(strict_types=1);

namespace App\PulseRecorders;

use App\Events\UseCaseCompleted;
use App\Events\UseCaseStarted;
use Carbon\CarbonImmutable;
use Laravel\Pulse\Facades\Pulse;
use Laravel\Pulse\Recorders\Concerns\Groups;
use Laravel\Pulse\Recorders\Concerns\Ignores;
use Laravel\Pulse\Recorders\Concerns\Sampling;

class UseCases
{
    use Groups;
    use Ignores;
    use Sampling;

    /**
     * The time the last job started processing.
     *
     * @var list<int>
     */
    protected array $lastUseCaseStartedAt = [];

    /**
     * The events to listen for.
     *
     * @var list<class-string>
     */
    public array $listen = [
        UseCaseStarted::class,
        UseCaseCompleted::class,
    ];

    /**
     * Record the request.
     */
    public function record(UseCaseStarted|UseCaseCompleted $event): void
    {
        $now = CarbonImmutable::now();

        if ($event instanceof UseCaseStarted) {
            $this->lastUseCaseStartedAt[get_class($event->useCase)] = $now->getTimestampMs();

            return;
        }

        [$timestamp, $timestampMs, $name, $lastUseCaseStartedAt] = [
            $now->getTimestamp(),
            $now->getTimestampMs(),
            get_class($event->useCase),
            tap($this->lastUseCaseStartedAt[get_class($event->useCase)], fn () => ($this->lastUseCaseStartedAt[get_class($event->useCase)] = null)),
        ];

        $duration = $timestampMs - $lastUseCaseStartedAt;

        Pulse::lazy(function () use ($timestamp, $name, $duration) {
            Pulse::record(
                type: 'use_cases',
                key: $name,
                value: $duration,
                timestamp: $timestamp,
            )
                ->count()
                ->avg();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

How It Works

Event Listening

The recorder listens to two events through the $listen property. When either event is dispatched, the record() method is automatically invoked by Laravel Pulse.

Tracking Start Times

When a UseCaseStarted event is received, the recorder stores the current timestamp in milliseconds, keyed by the UseCase class name. This creates a simple in-memory registry of when each UseCase type began execution.

Calculating Duration

When a UseCaseCompleted event arrives, the recorder:

  1. Retrieves the start timestamp for that UseCase class.
  2. Calculates the duration by subtracting the start time from the current time.
  3. Records the metric to Pulse with both count and average aggregations.
  4. Cleans up the stored start timestamp.

Pulse Traits

The recorder uses three standard Pulse concerns:

  • Groups: Allows grouping of recorded data
  • Ignores: Enables filtering of certain UseCases
  • Sampling: Provides sampling capabilities to reduce overhead

Setting Up the Events

You'll need to create two events that your UseCases will dispatch. Here's an example structure:

<?php

namespace App\Events;

use Illuminate\Foundation\Events\Dispatchable;

class UseCaseStarted
{
    use Dispatchable;

    public function __construct(
        public object $useCase
    ) {}
}
Enter fullscreen mode Exit fullscreen mode
<?php

namespace App\Events;

use Illuminate\Foundation\Events\Dispatchable;

class UseCaseCompleted
{
    use Dispatchable;

    public function __construct(
        public object $useCase
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

Configuring Pulse

Register the recorder in your config/pulse.php file:

'recorders' => [
    // ... other recorders

    \App\PulseRecorders\UseCases::class => [
        'enabled' => env('PULSE_USE_CASES_ENABLED', true),
        'sample_rate' => env('PULSE_USE_CASES_SAMPLE_RATE', 1),
        'ignore' => [
            // Add UseCase class names to ignore
        ],
    ],
],
Enter fullscreen mode Exit fullscreen mode

Creating the Dashboard Card

To visualize this data, you'll want to create a custom Pulse card. Create a Livewire component that queries the recorded data:

<?php

namespace App\Livewire\Pulse;

use Laravel\Pulse\Livewire\Card;
use Livewire\Attributes\Lazy;

#[Lazy]
class UseCases extends Card
{
    use Thresholds;

    /**
     * Ordering.
     *
     * @var 'slowest'|'count'
     */
    #[Url(as: 'use-cases')]
    public string $orderBy = 'slowest';

    /**
     * Render the component.
     */
    public function render(): Renderable
    {
        [$useCases, $time, $runAt] = $this->remember(
            fn () => $this->aggregate(
                'use_cases',
                ['avg', 'count'],
                match ($this->orderBy) {
                    'count' => 'count',
                    default => 'avg',
                },
            )->map(function ($row) {
                return (object) [
                    'use_case' => $row->key,
                    'count' => $row->count,
                    'avg' => $row->avg,
                ];
            }),
            $this->orderBy,
        );

        return View::make('pulse.use-cases', [
            'time' => $time,
            'runAt' => $runAt,
            'useCases' => $useCases,
            'config' => [
                'threshold' => Config::get('pulse.recorders.'.SlowRequestsRecorder::class.'.threshold'),
                'sample_rate' => Config::get('pulse.recorders.'.SlowRequestsRecorder::class.'.sample_rate'),
            ],
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefits of This Approach

This recorder provides several advantages for Clean Architecture applications:

  1. Performance Monitoring: Track how long each UseCase takes to execute
  2. Usage Patterns: See which UseCases are called most frequently
  3. Bottleneck Identification: Quickly identify slow UseCases that need optimization
  4. Production Insights: Get real-time visibility into your application's business logic layer

Top comments (0)