avoqado-dev/laravel-usecase

A lightweight, type-safe Use Case pattern implementation for Laravel with business rule validation

Maintainers

Package info

github.com/avoqado-dev/laravel-usecase

pkg:composer/avoqado-dev/laravel-usecase

Statistics

Installs: 251

Dependents: 0

Suggesters: 0

Stars: 22

Open Issues: 1

v1.0.1 2025-12-21 13:01 UTC

This package is auto-updated.

Last update: 2026-03-23 15:09:59 UTC


README

A lightweight, type-safe Use Case pattern implementation for Laravel with built-in business rule validation

Tests Latest Version Total Downloads License GitHub Stars

๐Ÿ“– Table of Contents

๐ŸŽฏ What is Laravel UseCase?

Laravel UseCase brings clean architecture principles to your Laravel applications by implementing the Use Case pattern (also known as Application Services or Command/Query pattern). It helps you organize your business logic into focused, testable, and reusable units that are completely independent of your HTTP layer.

Instead of fat controllers or bloated models, your business logic lives in dedicated Use Case classes that can be called from anywhere - controllers, jobs, commands, or even other use cases.

The Problem It Solves

// โŒ Before: Fat Controller with mixed concerns
class UserController
{
    public function store(Request $request)
    {
        // HTTP validation
        $validated = $request->validate([...]);
        
        // Business rule checking
        if (User::where('email', $validated['email'])->exists()) {
            return back()->withErrors(['email' => 'Email taken']);
        }
        
        // Database transaction
        DB::beginTransaction();
        try {
            $user = User::create($validated);
            event(new UserCreated($user));
            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
        
        // Logging
        Log::info('User created', ['user_id' => $user->id]);
        
        return redirect()->route('users.show', $user);
    }
}
// โœ… After: Clean Controller with Use Case
class UserController
{
    public function store(CreateUserRequest $request)
    {
        $userId = Mediator::dispatch(new CreateUser(
            name: $request->validated('name'),
            email: $request->validated('email'),
            password: $request->validated('password'),
        ));
        
        return redirect()->route('users.show', $userId);
    }
}

๐Ÿš€ Why Use This Package?

โœจ Benefits

Benefit Description
๐ŸŽฏ Separation of Concerns Business logic is completely isolated from HTTP, making it reusable across controllers, jobs, commands, and tests
๐Ÿ”’ Type Safety Full PHP 8.2+ type hints and generics catch errors at compile time, not runtime
๐Ÿงช Testability Test business logic in isolation without booting the HTTP layer or database
๐Ÿ“ฆ Maintainability Clear, predictable structure makes onboarding new developers faster
โ™ป๏ธ Reusability Use cases can be called from anywhere - no duplication needed
๐ŸŽจ Business-First Domain rules are first-class citizens with dedicated validation layer
๐Ÿ”ง Laravel-Native Uses familiar Laravel patterns - facades, container, middleware, events
๐Ÿ“Š Observability Built-in logging, caching, and transaction management
โšก Performance Automatic caching and atomic locks prevent redundant work
๐Ÿ›ก๏ธ Safety Database transactions and retry logic built-in

โœจ Key Features

  • โœ… Type-Safe - Uses PHP 8.2+ generics for return type safety
  • โœ… Business Rule Validation - Separates domain rules from HTTP validation
  • โœ… Middleware Pipeline - Familiar Laravel-style middleware for cross-cutting concerns
  • โœ… Zero Dependencies - Only requires Laravel, no third-party packages
  • โœ… Comprehensive Testing Utilities - Full mocking and assertion capabilities
  • โœ… Artisan Generator - Scaffold use cases with a single command
  • โœ… Built-in Middleware - Transactions, caching, locks, logging out of the box
  • โœ… Laravel-Native Integration - Uses facades, container, events naturally

๐Ÿ“ฆ Installation

Install via Composer:

composer require avoqado-dev/laravel-usecase

The package will auto-register via Laravel's package discovery.

Publish Configuration (Optional)

php artisan vendor:publish --tag=usecase-config

๐Ÿš€ Quick Start

1. Generate a Use Case

The easiest way to create a new use case is using the artisan command:

# Basic usage
php artisan make:usecase CreateUser --module=Users --entity=User

# With database transaction support
php artisan make:usecase CreateOrder --module=Orders --entity=Order --transaction

# With caching support
php artisan make:usecase GetUserStats --module=Users --entity=User --cache

# With atomic lock support
php artisan make:usecase ProcessPayment --module=Payments --entity=Payment --lock

# Combine multiple features
php artisan make:usecase ComplexOperation --module=Operations --entity=Operation --transaction --cache --lock

This generates both the Request and Handler classes in app/UseCases/{Module}/{Entity}/{UseCaseName}/.

2. Define Your Request

<?php

namespace App\UseCases\Users\CreateUser;

use AvoqadoDev\UseCase\Contracts\Request;
use AvoqadoDev\UseCase\Contracts\UsesDatabaseTransaction;

/**
 * @see CreateUserHandler
 * @implements Request<int>
 */
final readonly class CreateUser implements Request, UsesDatabaseTransaction
{
    public function __construct(
        public string $name,
        public string $email,
        public string $password,
    ) {}

    public function transactionAttempts(): int
    {
        return 1;
    }
}

3. Implement Your Handler

<?php

namespace App\UseCases\Users\CreateUser;

use App\Models\User;
use App\Rules\EmailMustBeUnique;
use AvoqadoDev\UseCase\Contracts\Request;
use AvoqadoDev\UseCase\Contracts\RequestHandler;
use AvoqadoDev\UseCase\BusinessRules\Contracts\GuardsRules;

final readonly class CreateUserHandler implements RequestHandler
{
    public function __construct(
        private GuardsRules $guardsRules
    ) {}

    /**
     * @param CreateUser $request
     */
    public function handle(Request $request): int
    {
        // Validate business rules
        $this->guardsRules->guard(
            new EmailMustBeUnique($request->email)
        );

        // Execute business logic
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => bcrypt($request->password),
        ]);

        return $user->id;
    }
}

4. Dispatch from Anywhere

use AvoqadoDev\UseCase\Facades\Mediator;
use App\UseCases\Users\CreateUser\CreateUser;

// From a controller
$userId = Mediator::dispatch(new CreateUser(
    name: $request->validated('name'),
    email: $request->validated('email'),
    password: $request->validated('password'),
));

// From a job
$userId = Mediator::dispatch(new CreateUser(...));

// From a command
$userId = Mediator::dispatch(new CreateUser(...));

// From another use case
$userId = Mediator::dispatch(new CreateUser(...));

๐Ÿ”ง Middleware Explained

Laravel UseCase includes powerful middleware to handle cross-cutting concerns automatically. Simply implement the corresponding interface on your Request class!

1. ๐Ÿ”„ Database Transactions

What it does: Automatically wraps your use case in a database transaction with retry logic.

When to use: Any operation that modifies data and needs atomicity (create, update, delete operations).

Benefits:

  • โœ… Automatic rollback on exceptions
  • โœ… Configurable retry attempts for deadlocks
  • โœ… No manual transaction management needed
use AvoqadoDev\UseCase\Contracts\UsesDatabaseTransaction;

final readonly class CreateOrder implements Request, UsesDatabaseTransaction
{
    public function transactionAttempts(): int
    {
        return 3; // Retry up to 3 times on deadlock
    }
}

Example Use Cases:

  • Creating orders with multiple related records
  • Updating inventory across multiple tables
  • Processing payments with audit logs

2. ๐Ÿ’พ Caching

What it does: Automatically caches the result of your use case to avoid redundant computation.

When to use: Read-heavy operations with expensive computations or database queries.

Benefits:

  • โœ… Automatic cache invalidation via TTL
  • โœ… Configurable cache keys per request
  • โœ… Reduces database load significantly
use AvoqadoDev\UseCase\Contracts\Cacheable;
use DateInterval;

final readonly class GetUserStats implements Request, Cacheable
{
    public function __construct(public int $userId) {}

    public function cacheKey(): string
    {
        return "user_stats_{$this->userId}";
    }

    public function ttl(): DateInterval
    {
        return new DateInterval('PT1H'); // Cache for 1 hour
    }
}

Example Use Cases:

  • Dashboard statistics
  • Report generation
  • Complex aggregations
  • API responses with heavy processing

3. ๐Ÿ”’ Atomic Locks

What it does: Prevents concurrent execution of the same use case using Laravel's atomic locks.

When to use: Operations that must not run simultaneously (payment processing, inventory updates).

Benefits:

  • โœ… Prevents race conditions
  • โœ… Ensures data consistency
  • โœ… Configurable wait time
use AvoqadoDev\UseCase\Contracts\UsesAtomicLock;

final readonly class ProcessPayment implements Request, UsesAtomicLock
{
    public function __construct(public string $orderId) {}

    public function lockKey(): string
    {
        return "payment_{$this->orderId}";
    }

    public function lockWaitSeconds(): int
    {
        return 10; // Wait up to 10 seconds for lock
    }
}

Example Use Cases:

  • Payment processing
  • Inventory deduction
  • Seat reservation systems
  • Concurrent user actions on same resource

4. ๐Ÿ“– Read from Write Database

What it does: Forces read operations to use the write database connection (master) instead of read replicas.

When to use: When you need to read immediately after a write to avoid replication lag.

Benefits:

  • โœ… Avoids stale data from read replicas
  • โœ… Ensures read-after-write consistency
  • โœ… No manual connection switching
use AvoqadoDev\UseCase\Contracts\ReadsFromWriteDatabase;

final readonly class GetRecentOrder implements Request, ReadsFromWriteDatabase
{
    // No additional methods needed - just implement the interface!
}

Example Use Cases:

  • Reading data immediately after creation
  • Displaying confirmation pages
  • Real-time updates
  • Critical operations requiring latest data

5. ๐Ÿ“ Logger Middleware

What it does: Automatically logs use case execution (request received, success, failure).

When to use: Enabled by default for all use cases (can be disabled in config).

Benefits:

  • โœ… Automatic audit trail
  • โœ… Debug failed operations
  • โœ… Monitor use case performance
  • โœ… Configurable log channel
// Configured globally in config/usecase.php
'logging' => [
    'enabled' => env('USECASE_LOGGING_ENABLED', true),
    'channel' => env('USECASE_LOG_CHANNEL', null),
],

Log Output:

[2024-01-15 10:30:45] local.INFO: Request received: CreateUser {"payload":{"name":"John Doe","email":"john@example.com"}}
[2024-01-15 10:30:46] local.INFO: Request succeeded: CreateUser

๐ŸŽฏ Combining Middleware

You can combine multiple middleware on a single use case:

final readonly class CreateOrderWithPayment implements 
    Request, 
    UsesDatabaseTransaction,    // Wrap in transaction
    UsesAtomicLock,             // Prevent concurrent processing
    ReadsFromWriteDatabase      // Read from master DB
{
    public function transactionAttempts(): int { return 3; }
    
    public function lockKey(): string { return "order_{$this->orderId}"; }
    
    public function lockWaitSeconds(): int { return 10; }
}

Execution Order:

  1. Logger (request received)
  2. Read from Write Database
  3. Atomic Lock (acquire)
  4. Cache (check)
  5. Database Transaction (begin)
  6. Your Handler Logic
  7. Database Transaction (commit)
  8. Cache (store)
  9. Atomic Lock (release)
  10. Logger (success/failure)

๐ŸŽจ Business Rules

Business rules represent domain-specific validation that goes beyond simple input validation. They encapsulate your business logic and can be tested independently.

Creating a Business Rule

<?php

namespace App\Rules;

use App\Models\User;
use AvoqadoDev\UseCase\BusinessRules\Contracts\BusinessRule;

final readonly class EmailMustBeUnique implements BusinessRule
{
    public function __construct(
        public string $email,
        public ?int $exceptUserId = null
    ) {}

    public function passes(): bool
    {
        return User::query()
            ->where('email', $this->email)
            ->when($this->exceptUserId, fn($q) => $q->where('id', '!=', $this->exceptUserId))
            ->doesntExist();
    }

    public function message(): string
    {
        return 'The email address is already taken.';
    }

    public function code(): string
    {
        return 'email_must_be_unique';
    }

    public function context(): array
    {
        return ['email' => $this->email];
    }
}

Using Business Rules

$this->guardsRules->guard(
    new EmailMustBeUnique($request->email),
    new PasswordMustBeStrong($request->password),
    new UserMustBeAdult($request->birthdate)
);

Pattern Matching on Exceptions

try {
    Mediator::dispatch(new CreateUser(...));
} catch (BusinessRuleException $e) {
    return $e->match([
        EmailMustBeUnique::class => fn() => redirect()->back()->with('error', 'Email taken'),
        PasswordMustBeStrong::class => fn() => redirect()->back()->with('error', 'Weak password'),
    ], default: fn() => redirect()->back()->with('error', 'Validation failed'));
}

๐Ÿงช Testing

The package provides comprehensive testing utilities for both use cases and business rules.

Testing Use Cases

use AvoqadoDev\UseCase\Facades\Mediator;
use App\UseCases\Users\CreateUser\CreateUser;

it('dispatches create user use case', function () {
    Mediator::fake(CreateUser::class, 123);

    $response = $this->post('/users', [
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'password' => 'password123',
    ]);

    Mediator::assertDispatched(CreateUser::class, function ($command) {
        return $command->email === 'john@example.com';
    });
});

Testing Business Rules

use AvoqadoDev\UseCase\Testing\GuardRulesFake;
use App\Rules\EmailMustBeUnique;

it('guards against duplicate emails', function () {
    $guardRulesFake = new GuardRulesFake();
    $handler = new CreateUserHandler($guardRulesFake);

    $handler->handle(new CreateUser(...));

    $guardRulesFake->assertGuarded(function (EmailMustBeUnique $rule) {
        return $rule->email === 'john@example.com';
    });
});

Testing Broken Rules

it('throws exception when email is duplicate', function () {
    $guardRulesFake = new GuardRulesFake();
    $guardRulesFake->withBrokenRule(new EmailMustBeUnique('john@example.com'));

    $handler = new CreateUserHandler($guardRulesFake);

    expect(fn() => $handler->handle(new CreateUser(...)))
        ->toThrow(BusinessRuleException::class);
});

โš™๏ธ Configuration

Customize the package behavior in config/usecase.php:

Middleware Order

'middleware' => [
    \AvoqadoDev\UseCase\Middleware\ReadFromWriteDatabase::class,
    \AvoqadoDev\UseCase\Middleware\LoggerMiddleware::class,
    \AvoqadoDev\UseCase\Middleware\WithAtomicLock::class,
    \AvoqadoDev\UseCase\Middleware\WithCache::class,
    \AvoqadoDev\UseCase\Middleware\WithDatabaseTransaction::class,
],

Logging

'logging' => [
    'enabled' => env('USECASE_LOGGING_ENABLED', true),
    'channel' => env('USECASE_LOG_CHANNEL', null),
],

Business Rule HTTP Status

'business_rule_status_code' => 422,

๐Ÿ’– Support & Contribution

โญ Star This Repository

If you find this package useful, please consider giving it a star on GitHub! It helps others discover the package and motivates us to continue improving it.

GitHub Stars

๐Ÿค Contributing

We welcome contributions! Whether it's:

  • ๐Ÿ› Bug reports
  • ๐Ÿ’ก Feature requests
  • ๐Ÿ“– Documentation improvements
  • ๐Ÿ”ง Code contributions

Please see CONTRIBUTING.md for details on our code of conduct and development process.

๐Ÿ“ฃ Spread the Word

Help us grow the community:

  • Share on Twitter/X
  • Write a blog post about your experience
  • Mention it in your Laravel projects
  • Recommend it to your team

๐Ÿ’ฌ Get Help

๐Ÿ“„ License

MIT License - see LICENSE file for details.

๐Ÿ‘จโ€๐Ÿ’ป Credits

Created and maintained by:

Made with โค๏ธ for the Laravel community

โญ Star us on GitHub โ€ข ๐Ÿ› Report Bug โ€ข ๐Ÿ’ฌ Discussions