laravel-usecase maintained by avoqado-dev
🎯 Laravel UseCase
📖 Table of Contents
- What is Laravel UseCase?
- Why Use This Package?
- Key Features
- Installation
- Quick Start
- Middleware Explained
- Business Rules
- Testing
- Configuration
- Support & Contribution
🎯 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:
- Logger (request received)
- Read from Write Database
- Atomic Lock (acquire)
- Cache (check)
- Database Transaction (begin)
- Your Handler Logic
- Database Transaction (commit)
- Cache (store)
- Atomic Lock (release)
- 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.
🤝 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
- Issues: GitHub Issues
- Discussions: GitHub Discussions
📄 License
MIT License - see LICENSE file for details.
👨💻 Credits
Created and maintained by: